mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-02 08:50:07 +00:00
fix: remove old files
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
# Get this from https://discord.com/developers/applications/${applicationId}/bot
|
||||
DISCORD_TOKEN=
|
||||
|
||||
# Sharding setup. This is largely dependant on your system specs.
|
||||
# For **development purposes** the defaults are fine.
|
||||
MAX_SHARDS=1
|
||||
FIRST_SHARD_ID=0
|
||||
LAST_SHARD_ID=0
|
||||
SHARDS_PER_CLUSTER=10
|
||||
MAX_CLUSTERS=10
|
||||
|
||||
# For the event handler process, change the key!
|
||||
# (url is fine unless hosted on a different machine)
|
||||
EVENT_HANDLER_PORT=1235
|
||||
EVENT_HANDLER_SECRET_KEY=secreteventhandlerkey
|
||||
EVENT_HANDLER_URL=localhost
|
||||
|
||||
# For the gateway process, change the key!
|
||||
REST_PORT=1236
|
||||
REST_AUTHORIZATION_KEY=secretrestkey
|
||||
|
||||
# For the gateway process, change the key!
|
||||
# (url is fine unless hosted on a different machine)
|
||||
GATEWAY_PORT=1237
|
||||
GATEWAY_SECRET_KEY=secretgatewaykey
|
||||
GATEWAY_PROXY_URL=localhost
|
||||
|
||||
# Change to false for production use
|
||||
DEVELOPMENT=true
|
||||
|
||||
# Optional, but very helpful for development.
|
||||
MISSING_TRANSLATION_WEBHOOK=
|
||||
DEV_GUILD_ID=
|
||||
14
template/bigbot/old/.vscode/settings.json
vendored
14
template/bigbot/old/.vscode/settings.json
vendored
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.importMap": "./importMap.json",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
},
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"deno.suggest.imports.hosts": {
|
||||
"https://deno.land": true
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# Discordeno Big Bot Template
|
||||
|
||||
Support: <https://discord.gg/ddeno>
|
||||
|
||||
This template is designed for bots that aim or are already in millions of Discord servers.
|
||||
|
||||
## Setup
|
||||
|
||||
- Clone this repository can move this directory into your desired project location.
|
||||
- Move all files from the `bigbot` folder to the root of the project.
|
||||
- You may encounter an issue with .vscode but force move the files to the root of the project. We have setup special
|
||||
import maps in this template that should override the general .vscode folder already in the root folder.
|
||||
- Rename the `.env.example` file to `.env`
|
||||
- Fill out the `.env` file
|
||||
- Go to `configs.ts` file and remove all the intents you don't want in your bot.
|
||||
|
||||
## Usage
|
||||
|
||||
> Note: Please install at least `deno@^1.22` on your system. (This is due to the requirement of the `Deno` namespace in
|
||||
> workers for the `gateway` process.)
|
||||
|
||||
- Always run the `rest` process first with `deno task rest`.
|
||||
- Start the `bot` process next with `deno task bot`. (If you are developing a bot, use `deno task watch-bot` instead to
|
||||
auto reload any changes. This won't restart any other processes, just your bot.)
|
||||
- Lastly, start the `gateway` process with `deno task gateway`.
|
||||
|
||||
> Important: The `gateway` process and `rest` are designed not to be shut off. So once those are on, the only thing you
|
||||
> should be doing is restarting your `bot` process. This saves API requests
|
||||
|
||||
## Details
|
||||
|
||||
### Translating Application Commands
|
||||
|
||||
The template supports translations for application commands. This is possible using guild commands. If you use global
|
||||
commands, translations will not work and will default to english.
|
||||
|
||||
If you prefer a different default(not english), please use the Find And Replace to change the `'english'` everywhere
|
||||
necessary.
|
||||
|
||||
#### Autocomplete & Type Checking
|
||||
|
||||
One cool thing about the translations is that you will get autocomplete and type checking built in for all the keys.
|
||||
This will ensure you do not miss a key to be translated. It will also make it easier to code by providing the
|
||||
autocomplete functionality.
|
||||
|
||||
### Updating Application Commands
|
||||
|
||||
The template is designed in a way that you will no longer need to worry about updating or maintaing your commands.
|
||||
|
||||
- Global Commands: For simplicity you can add a line in mod.ts to update them globally. This generally takes 1 call and
|
||||
isn't a deal breaker.
|
||||
- `/update global` is also available on your development server, to trigger manually.
|
||||
- Guild Commands: This is a bit more complicated. By default, our system will update guild commands on demand! Instead
|
||||
of making a million requests for all your servers, we will update them as needed.
|
||||
|
||||
**Guild Commands Kwik & Command Versioning**
|
||||
|
||||
For Global Commands you can make 1 request to api to update all commands on restart. Its not a big deal. But with Guild
|
||||
commands essentially you need to make a request per guild. This can get spammy. That would be crazy. To solve this we
|
||||
created the concept of `commandVersions`. This basically will decide whether or not guild commands should be updated.
|
||||
|
||||
Kwik is a file based database I used in order to make this setup easy and allow any dev using this template to use a
|
||||
database of their choice for their bot. I do not recommend using Kwik as your database. Please add a full database of
|
||||
your choice for your bot. You can even replace Kwik should you choose in the database folder.
|
||||
|
||||
Process:
|
||||
|
||||
1. You update your command options/args or create new commands etc...
|
||||
2. Increment the `CURRENT_SLASH_COMMAND_VERSION` in `src/database/commandVersion.ts`
|
||||
|
||||
- I recommend moving this into your database so you can build a dev command or eval and update this on the fly as you
|
||||
wish.
|
||||
|
||||
3. Now whenever a guild emits any event, this will make sure to update the guild commands if necessary. If it already
|
||||
has the latest commands, it will just ignore. If it was never updated or is using an outdated version, it will update
|
||||
it.
|
||||
|
||||
Aside from the automated system, there is also the option of `/update guild id` to update a guild manually.
|
||||
@@ -1,94 +0,0 @@
|
||||
import { dotEnvConfig, GatewayIntents } from "./deps.ts";
|
||||
|
||||
// Get the .env file that the user should have created, and load the configs.
|
||||
const env = dotEnvConfig({ export: true });
|
||||
|
||||
// TODO: REMOVE THESE! THEY ARE BAD FOR YOU! DUH! Seriously, only keep the ones your bot needs!
|
||||
export const GATEWAY_INTENTS: (keyof typeof GatewayIntents)[] = [
|
||||
"DirectMessageReactions",
|
||||
"DirectMessageTyping",
|
||||
"DirectMessages",
|
||||
"GuildBans",
|
||||
"GuildEmojis",
|
||||
"GuildIntegrations",
|
||||
"GuildInvites",
|
||||
"GuildMembers",
|
||||
"GuildMessageReactions",
|
||||
"GuildMessageTyping",
|
||||
"GuildMessages",
|
||||
"GuildPresences",
|
||||
"GuildVoiceStates",
|
||||
"GuildWebhooks",
|
||||
"Guilds",
|
||||
];
|
||||
|
||||
if (!env.DISCORD_TOKEN) {
|
||||
throw new Error("DUDE! You did not provide a Discord token!");
|
||||
}
|
||||
export const DISCORD_TOKEN = env.DISCORD_TOKEN!;
|
||||
|
||||
// Set as 0 to make it use default values. NOT RECOMMENDED TO DEFAULT FOR BIG BOTS!!!!
|
||||
export const MAX_SHARDS = env.MAX_SHARDS ? parseInt(env.MAX_SHARDS, 10) : 0;
|
||||
export const FIRST_SHARD_ID = env.FIRST_SHARD_ID ? parseInt(env.FIRST_SHARD_ID, 10) : 0;
|
||||
export const LAST_SHARD_ID = env.LAST_SHARD_ID ? parseInt(env.LAST_SHARD_ID, 10) : 0;
|
||||
// Default to 10
|
||||
export const SHARDS_PER_CLUSTER = env.SHARDS_PER_CLUSTER ? parseInt(env.SHARDS_PER_CLUSTER, 10) : 10;
|
||||
export const MAX_CLUSTERS = parseInt(env.MAX_CLUSTERS!, 10);
|
||||
if (!MAX_CLUSTERS) {
|
||||
throw new Error(
|
||||
"How many clusters can you run on your machine (MAX_CLUSTERS)? Check your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const GATEWAY_PROXY_URL = env
|
||||
.GATEWAY_PROXY_URL!;
|
||||
if (!GATEWAY_PROXY_URL) {
|
||||
throw new Error(
|
||||
"Hmm, it seems like you don't have somewhere to send gateway events to (GATEWAY_PROXY_URL). Please check your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const EVENT_HANDLER_URL = env
|
||||
.EVENT_HANDLER_URL!;
|
||||
if (!EVENT_HANDLER_URL) {
|
||||
throw new Error(
|
||||
"Hmm, it seems like you don't have somewhere to send events to (EVENT_HANDLER_URL). Please check your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const GATEWAY_SECRET_KEY = env.GATEWAY_SECRET_KEY!;
|
||||
if (!GATEWAY_SECRET_KEY) {
|
||||
throw new Error(
|
||||
"You need to add a GATEWAY_SECRET_KEY to your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const REST_AUTHORIZATION_KEY = env.REST_AUTHORIZATION_KEY!;
|
||||
if (!REST_AUTHORIZATION_KEY) {
|
||||
throw new Error(
|
||||
"You need to add a REST_AUTHORIZATION_KEY to your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const EVENT_HANDLER_SECRET_KEY = env.EVENT_HANDLER_SECRET_KEY!;
|
||||
if (!EVENT_HANDLER_SECRET_KEY) {
|
||||
throw new Error(
|
||||
"You need to add an EVENT_HANDLER_SECRET_KEY to your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const BOT_ID = BigInt(atob(env.DISCORD_TOKEN.split(".")[0]));
|
||||
if (!BOT_ID) {
|
||||
throw new Error(
|
||||
"Hmm, it seems like you didn't put in a valid DISCORD_TOKEN. Check your .env file!",
|
||||
);
|
||||
}
|
||||
|
||||
export const REST_PORT = env.REST_PORT ? parseInt(env.REST_PORT, 10) : 5000;
|
||||
export const GATEWAY_PORT = env.GATEWAY_PORT ? parseInt(env.GATEWAY_PORT, 10) : 8080;
|
||||
export const EVENT_HANDLER_PORT = env.EVENT_HANDLER_PORT ? parseInt(env.EVENT_HANDLER_PORT, 10) : 7050;
|
||||
|
||||
export const DEVELOPMENT = env.DEVELOPMENT ?? true;
|
||||
export const MISSING_TRANSLATION_WEBHOOK = env.MISSING_TRANSLATION_WEBHOOK ||
|
||||
"";
|
||||
export const DEV_GUILD_ID = env.DEV_GUILD_ID ? BigInt(env.DEV_GUILD_ID) : 0n;
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"tasks": {
|
||||
"rest": "deno run -A --unstable --import-map ./importMap.json ./src/rest/mod.ts",
|
||||
"bot": "deno run -A --unstable --import-map ./importMap.json ./src/bot/mod.ts",
|
||||
"watch-bot": "deno run --watch -A --unstable --import-map ./importMap.json ./src/bot/mod.ts",
|
||||
"gateway": "deno run -A --unstable --import-map ./importMap.json ./src/gateway/mod.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from "https://deno.land/x/discordeno@13.0.0-rc31/mod.ts";
|
||||
export * from "https://deno.land/x/discordeno@13.0.0-rc31/plugins/mod.ts";
|
||||
|
||||
// Terminal Colors!
|
||||
export * from "https://deno.land/std@0.117.0/fmt/colors.ts";
|
||||
// Get data from .env files
|
||||
export { config as dotEnvConfig } from "https://deno.land/x/dotenv@v3.1.0/mod.ts";
|
||||
export * from "https://deno.land/x/kwik@v1.2.3/mod.ts";
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"imports": {
|
||||
"/": "./",
|
||||
"./": "./",
|
||||
"@deps": "./deps.ts",
|
||||
"@configs": "./configs.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
rest:
|
||||
deno run -A --unstable --import-map ./importMap.json ./src/rest/mod.ts
|
||||
|
||||
gateway:
|
||||
deno run -A --unstable --import-map ./importMap.json ./src/gateway/mod.ts
|
||||
|
||||
bot:
|
||||
deno run -A --unstable --import-map ./importMap.json ./src/bot/mod.ts
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Bot, Collection } from "../../deps.ts";
|
||||
|
||||
/** These are custom properties you want to add to `bot` and have accessible everywhere. */
|
||||
export interface BotClient extends Bot {
|
||||
commandVersions: Collection<bigint, number>;
|
||||
}
|
||||
|
||||
export function setupBotClient(bot: BotClient) {
|
||||
bot.commandVersions = new Collection();
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { BotClient } from "../botClient.ts";
|
||||
import { commandVersions } from "./kwik.ts";
|
||||
|
||||
export const CURRENT_SLASH_COMMAND_VERSION = 1;
|
||||
|
||||
/** Whether the guild has the latest slash command version */
|
||||
export async function usesLatestCommandVersion(
|
||||
bot: BotClient,
|
||||
guildId: bigint,
|
||||
): Promise<boolean> {
|
||||
return (await getCurrentCommandVersion(bot, guildId)) ===
|
||||
CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
/** Get the current slash command version for this guild */
|
||||
export async function getCurrentCommandVersion(
|
||||
bot: BotClient,
|
||||
guildId: bigint,
|
||||
): Promise<number> {
|
||||
const current = await commandVersions.get(guildId.toString());
|
||||
if (current) return current.version;
|
||||
|
||||
await commandVersions.set(
|
||||
guildId.toString(),
|
||||
{ version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
);
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
export async function updateCommandVersion(
|
||||
bot: BotClient,
|
||||
guildId: bigint,
|
||||
): Promise<number> {
|
||||
// UPDATE THE VERSION SAVED IN THE DB
|
||||
await commandVersions.set(guildId.toString(), {
|
||||
version: CURRENT_SLASH_COMMAND_VERSION,
|
||||
});
|
||||
// UPDATE THE CACHED VERSION FOR NEXT CHECK
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { logger } from "../../utils/logger.ts";
|
||||
import { decode, encode, Kwik, KwikTable } from "../../../deps.ts";
|
||||
|
||||
const log = logger({ name: "DB" });
|
||||
|
||||
log.info("Initializing KwikDB Database.");
|
||||
|
||||
interface CommandVersionsSchema {
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const kwik = new Kwik();
|
||||
export const commandVersions = new KwikTable<CommandVersionsSchema>(
|
||||
kwik,
|
||||
"commandVersions",
|
||||
);
|
||||
|
||||
// Add BigInt Support
|
||||
kwik.msgpackExtensionCodec.register({
|
||||
type: 0,
|
||||
encode: (object: unknown): Uint8Array | null => {
|
||||
if (typeof object === "bigint") {
|
||||
if (
|
||||
object <= Number.MAX_SAFE_INTEGER && object >= Number.MIN_SAFE_INTEGER
|
||||
) {
|
||||
return encode(parseInt(object.toString(), 10), {});
|
||||
} else {
|
||||
return encode(object.toString(), {});
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
decode: (data: Uint8Array) => {
|
||||
return BigInt(decode(data, {}) as string);
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize the Database
|
||||
await kwik.init();
|
||||
|
||||
log.info("KwikDB Initialized!");
|
||||
@@ -1,32 +0,0 @@
|
||||
import { InteractionTypes, MessageComponentTypes } from "../../../../deps.ts";
|
||||
import { bot } from "../../mod.ts";
|
||||
import { executeSlashCommand } from "../interactions/executeSlashCommand.ts";
|
||||
import { logger, LogLevels } from "../../../utils/logger.ts";
|
||||
|
||||
const log = logger({ name: "InteractionHandler" });
|
||||
|
||||
export function setInteractionCreateEvent() {
|
||||
log.info("Adding `bot.events.interactionCreate` handler.");
|
||||
bot.events.interactionCreate = async (_, interaction) => {
|
||||
log.debug("New event fired:\n", interaction);
|
||||
// SLASH COMMAND
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
log.debug("Slash Command Fired!");
|
||||
return await executeSlashCommand(bot, interaction);
|
||||
}
|
||||
|
||||
if (interaction.type === InteractionTypes.MessageComponent) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
// THE INTERACTION CAME FROM A BUTTON
|
||||
if (
|
||||
interaction.data.componentType ===
|
||||
MessageComponentTypes.Button
|
||||
) {
|
||||
log.debug("Button Event!");
|
||||
// processButtonCollectors(bot, interaction)
|
||||
}
|
||||
}
|
||||
};
|
||||
log.debug("All handlers:\n", bot.events);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import {
|
||||
bgBlack,
|
||||
bgGreen,
|
||||
bgMagenta,
|
||||
bgYellow,
|
||||
black,
|
||||
green,
|
||||
Interaction,
|
||||
InteractionResponseTypes,
|
||||
red,
|
||||
sendPrivateInteractionResponse,
|
||||
white,
|
||||
} from "../../../../deps.ts";
|
||||
import { optionParser, translateOptionNames } from "../../../utils/options.ts";
|
||||
import { privateReplyToInteraction, replyToInteraction } from "../../../utils/replies.ts";
|
||||
import slashLogWebhook from "../../../utils/slashWebhook.ts";
|
||||
import { BotClient } from "../../botClient.ts";
|
||||
import { loadLanguage, serverLanguages, translate } from "../../languages/translate.ts";
|
||||
import { Command, ConvertArgumentDefinitionsToArgs } from "../../types/command.ts";
|
||||
import commands from "./mod.ts";
|
||||
import { logger, LogLevels } from "../../../utils/logger.ts";
|
||||
|
||||
const log = logger({ name: "CommandHandler" });
|
||||
|
||||
function logCommand(
|
||||
info: Interaction,
|
||||
type: "Failure" | "Success" | "Trigger" | "Slowmode" | "Missing" | "Inhibit",
|
||||
commandName: string,
|
||||
) {
|
||||
const command = `[COMMAND: ${bgYellow(black(commandName || "Unknown"))} - ${
|
||||
bgBlack(
|
||||
["Failure", "Slowmode", "Missing"].includes(type) ? red(type) : type === "Success" ? green(type) : white(type),
|
||||
)
|
||||
}]`;
|
||||
|
||||
const user = bgGreen(
|
||||
black(
|
||||
`${info.user.username}#${info.user.discriminator}(${info.id})`,
|
||||
),
|
||||
);
|
||||
const guild = bgMagenta(
|
||||
black(`${info.guildId ? `Guild ID: (${info.guildId})` : "DM"}`),
|
||||
);
|
||||
|
||||
log.info(`${command} by ${user} in ${guild} with MessageID: ${info.id}`);
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(
|
||||
bot: BotClient,
|
||||
interaction: Interaction,
|
||||
) {
|
||||
log.debug(`New interaction:\n`, interaction);
|
||||
|
||||
const data = interaction.data;
|
||||
const name = data?.name as keyof typeof commands;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const command: Command<any> | undefined = commands[name];
|
||||
|
||||
// Command could not be found
|
||||
if (!command?.execute) {
|
||||
return await sendPrivateInteractionResponse(
|
||||
bot,
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: translate(
|
||||
bot,
|
||||
interaction.guildId!,
|
||||
"EXECUTE_COMMAND_NOT_FOUND",
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
.catch(log.error);
|
||||
}
|
||||
|
||||
// HAVE TO CONVERT OUTSIDE OF TRY SO IT CAN BE USED IN CATCH TOO
|
||||
try {
|
||||
logCommand(interaction, "Trigger", name);
|
||||
|
||||
// Load the language for this guild
|
||||
if (interaction.guildId && !serverLanguages.has(interaction.guildId)) {
|
||||
// TODO: Check if this is deferrable
|
||||
await replyToInteraction(bot, interaction, {
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
loadLanguage(interaction.guildId);
|
||||
} // Load the language for this guild
|
||||
else if (command.acknowledge) {
|
||||
// Acknowledge the command
|
||||
await replyToInteraction(bot, interaction, {
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
|
||||
// FIRST GET THE TRANSLATIONS FOR ALL OPTIONS
|
||||
const translatedOptionNames = interaction.guildId && command.options
|
||||
? translateOptionNames(bot, interaction.guildId, command.options)
|
||||
: {};
|
||||
|
||||
// PARSE THE OPTIONS TO A NICE OBJECT AND TRANSLATE THE KEYS TO ENGLISH
|
||||
const parsedArguments = optionParser(
|
||||
interaction.data?.options,
|
||||
interaction.data?.resolved,
|
||||
translatedOptionNames,
|
||||
);
|
||||
|
||||
await command.execute(
|
||||
bot,
|
||||
interaction,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
parsedArguments as ConvertArgumentDefinitionsToArgs<any>,
|
||||
);
|
||||
logCommand(interaction, "Success", name);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
logCommand(interaction, "Failure", name);
|
||||
await slashLogWebhook(bot, interaction, name).catch(log.error);
|
||||
return await privateReplyToInteraction(bot, interaction, {
|
||||
content: translate(bot, interaction.id, "EXECUTE_COMMAND_ERROR"),
|
||||
}).catch(log.error);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Command } from "../../types/command.ts";
|
||||
import ping from "./slash/general/ping.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export const commands: Record<string, Command<any>> = {
|
||||
ping,
|
||||
};
|
||||
|
||||
export default commands;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ArgumentDefinition, Command } from "../../../types/command.ts";
|
||||
|
||||
export function createCommand<T extends readonly ArgumentDefinition[]>(
|
||||
command: Command<T>,
|
||||
) {
|
||||
return command;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ApplicationCommandOptionTypes } from "../../../../../../deps.ts";
|
||||
import { replyToInteraction } from "../../../../../utils/replies.ts";
|
||||
import { updateGlobalCommands, updateGuildCommands } from "../../../../../utils/updateSlash.ts";
|
||||
import { createCommand } from "../createCommand.ts";
|
||||
|
||||
const command = createCommand({
|
||||
name: "UPDATE_NAME",
|
||||
description: "UPDATE_DESCRIPTION",
|
||||
dev: true,
|
||||
acknowledge: true,
|
||||
options: [
|
||||
{
|
||||
name: "UPDATE_GLOBAL_NAME",
|
||||
description: "UPDATE_GLOBAL_DESCRIPTION",
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
},
|
||||
{
|
||||
name: "UPDATE_GUILD_NAME",
|
||||
description: "UPDATE_GUILD_DESCRIPTION",
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: "UPDATE_GUILD_ID_NAME",
|
||||
description: "UPDATE_GUILD_ID_DESCRIPTION",
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const,
|
||||
execute: async function (bot, interaction, args) {
|
||||
if (args.global) {
|
||||
await updateGlobalCommands(bot);
|
||||
return await replyToInteraction(
|
||||
bot,
|
||||
interaction,
|
||||
"Updated Global Commands!",
|
||||
);
|
||||
}
|
||||
|
||||
if (args.guild) {
|
||||
// GUILD COMMANDS
|
||||
await updateGuildCommands(bot, bot.transformers.snowflake(args.guild.id));
|
||||
return await replyToInteraction(
|
||||
bot,
|
||||
interaction,
|
||||
`Updated Guild Commands for Guild ID: ${args.guild.id}!`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default command;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { snowflakeToTimestamp } from "../../../../../utils/helpers.ts";
|
||||
import { replyToInteraction } from "../../../../../utils/replies.ts";
|
||||
import { translate } from "../../../../languages/translate.ts";
|
||||
import { createCommand } from "../createCommand.ts";
|
||||
|
||||
const command = createCommand({
|
||||
name: "PING_NAME",
|
||||
dev: true,
|
||||
description: "PING_DESCRIPTION",
|
||||
execute: async function (bot, interaction) {
|
||||
return await replyToInteraction(
|
||||
bot,
|
||||
interaction,
|
||||
translate(
|
||||
bot,
|
||||
interaction.guildId!,
|
||||
"PING_RESPONSE_WITH_TIME",
|
||||
Date.now() - snowflakeToTimestamp(interaction.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default command;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { DEV_GUILD_ID } from "../../../../../configs.ts";
|
||||
import { DiscordGatewayPayload, DiscordUnavailableGuild } from "../../../../../deps.ts";
|
||||
import logger from "../../../../utils/logger.ts";
|
||||
import { updateGuildCommands } from "../../../../utils/updateSlash.ts";
|
||||
import { BotClient } from "../../../botClient.ts";
|
||||
import { usesLatestCommandVersion } from "../../../database/commandVersion.ts";
|
||||
import { commandVersions } from "../../../database/kwik.ts";
|
||||
|
||||
export async function setGuildCommands(
|
||||
bot: BotClient,
|
||||
data: DiscordGatewayPayload,
|
||||
) {
|
||||
if (!data.t) return;
|
||||
|
||||
if (data.t === "GUILD_DELETE") {
|
||||
const id = (data.d as DiscordUnavailableGuild).id;
|
||||
await commandVersions.delete(id);
|
||||
bot.commandVersions.delete(bot.transformers.snowflake(id));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = bot.transformers.snowflake(
|
||||
(["GUILD_CREATE", "GUILD_UPDATE"].includes(data.t)
|
||||
? // deno-lint-ignore no-explicit-any
|
||||
(data.d as any).id
|
||||
: // deno-lint-ignore no-explicit-any
|
||||
(data.d as any).guild_id ?? "") ?? "",
|
||||
);
|
||||
|
||||
// IF NO ID FOUND CANCEL. IF ALREADY ON LATEST VERSION CANCEL.
|
||||
if (!id || await usesLatestCommandVersion(bot, id)) return;
|
||||
|
||||
// DEV GUILD SHOULD BE IGNORED
|
||||
if (id === DEV_GUILD_ID) return;
|
||||
|
||||
// NEW GUILD AVAILABLE OR NOT USING LATEST VERSION
|
||||
logger.info(
|
||||
`[Slash Setup] Installing slash commands on Guild ${id} for Event ${data.t}`,
|
||||
);
|
||||
await updateGuildCommands(bot, id);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { setInteractionCreateEvent } from "./handlers/interactionCreate.ts";
|
||||
import { logger } from "../../utils/logger.ts";
|
||||
|
||||
const log = logger({ name: "EventHandlers" });
|
||||
|
||||
export function setupEventHandlers() {
|
||||
log.debug("Adding Event Handlers!");
|
||||
setInteractionCreateEvent();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
const english = {
|
||||
// Execute Command
|
||||
EXECUTE_COMMAND_NOT_FOUND: "Something went wrong. I was not able to find this command.",
|
||||
EXECUTE_COMMAND_ERROR: "Something went wrong. The command execution has thrown an error.",
|
||||
|
||||
// Ping Command
|
||||
PING_NAME: "ping",
|
||||
PING_DESCRIPTION: "🏓 Check whether the bot is online and responsive.",
|
||||
PING_RESPONSE: "🏓 Pong! I am online and responsive! :clock10:",
|
||||
PING_RESPONSE_WITH_TIME: (time: number) => `🏓 Pong! ${time / 1000} seconds! I am online and responsive! :clock10:`,
|
||||
|
||||
// Update Command
|
||||
UPDATE_NAME: "update",
|
||||
UPDATE_DESCRIPTION: "🎉 Update the commands for the bot.",
|
||||
UPDATE_GLOBAL_NAME: "global",
|
||||
UPDATE_GLOBAL_DESCRIPTION: "Update the global commands.",
|
||||
UPDATE_GUILD_NAME: "guild",
|
||||
UPDATE_GUILD_DESCRIPTION: "Update guild commands for a guild.",
|
||||
UPDATE_GUILD_ID_NAME: "id",
|
||||
UPDATE_GUILD_ID_DESCRIPTION: "The guild id you wish to manually update.",
|
||||
} as const;
|
||||
|
||||
export default english;
|
||||
@@ -1,13 +0,0 @@
|
||||
import english from "./english.ts";
|
||||
|
||||
const languages: Record<string, Language> = {
|
||||
english,
|
||||
};
|
||||
|
||||
export default languages;
|
||||
|
||||
export type Language = Record<
|
||||
string,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
string | string[] | ((...args: any[]) => string)
|
||||
>;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./languages.ts";
|
||||
export * from "./translate.ts";
|
||||
@@ -1,92 +0,0 @@
|
||||
import { MISSING_TRANSLATION_WEBHOOK } from "../../../configs.ts";
|
||||
import { Bot } from "../../../deps.ts";
|
||||
import Embeds from "../../utils/Embeds.ts";
|
||||
import logger from "../../utils/logger.ts";
|
||||
import english from "./english.ts";
|
||||
import languages from "./languages.ts";
|
||||
|
||||
/** This should hold the language names per guild id. <guildId, language> */
|
||||
export const serverLanguages = new Map<bigint, keyof typeof languages>();
|
||||
|
||||
export function translate<K extends translationKeys>(
|
||||
bot: Bot,
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
key: K,
|
||||
...params: getArgs<K>
|
||||
): string {
|
||||
const language = getLanguage(guildIdOrLanguage);
|
||||
// deno-lint-ignore no-explicit-any
|
||||
let value: string | ((...any: any[]) => string) | string[] = languages[language][key];
|
||||
|
||||
// Was not able to be translated
|
||||
if (!value) {
|
||||
// Check if this key is available in english
|
||||
if (language !== "english") {
|
||||
value = languages.english[key];
|
||||
}
|
||||
|
||||
// Still not found in english so default to using the KEY_ITSELF
|
||||
if (!value) value = key;
|
||||
|
||||
// Send a log webhook so the devs know sth is missing
|
||||
missingTranslation(bot, language, key);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) return value.join("\n");
|
||||
|
||||
if (typeof value === "function") return value(...(params || []));
|
||||
|
||||
return value as string;
|
||||
}
|
||||
|
||||
/** Get the language this guild has set, will always return "english" if it is not in cache */
|
||||
export function getLanguage(
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
) {
|
||||
return typeof guildIdOrLanguage === "string"
|
||||
? guildIdOrLanguage
|
||||
: serverLanguages.get(guildIdOrLanguage) ?? "english";
|
||||
}
|
||||
|
||||
export function loadLanguage(guildId: bigint) {
|
||||
// TODO: add this settings
|
||||
// const settings = await database.findOne('guilds', guildId)
|
||||
const settings = { language: "undefined" };
|
||||
|
||||
if (settings?.language && languages[settings.language]) {
|
||||
serverLanguages.set(guildId, settings.language);
|
||||
} else serverLanguages.set(guildId, "english");
|
||||
}
|
||||
|
||||
const [id, token] = MISSING_TRANSLATION_WEBHOOK.substring(
|
||||
MISSING_TRANSLATION_WEBHOOK.indexOf("webhooks/") + 9,
|
||||
).split(
|
||||
"/",
|
||||
);
|
||||
/** Send a webhook for a missing translation key */
|
||||
export async function missingTranslation(
|
||||
bot: Bot,
|
||||
language: keyof typeof languages,
|
||||
key: string,
|
||||
) {
|
||||
if (!id || !token) return;
|
||||
|
||||
const embeds = new Embeds(bot)
|
||||
.setTitle("Missing Translation")
|
||||
.setColor("RANDOM")
|
||||
.addField("Language", language, true)
|
||||
.addField("Key", key, true);
|
||||
|
||||
await bot.helpers
|
||||
.sendWebhook(bot.transformers.snowflake(id), token, {
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(logger.error);
|
||||
}
|
||||
|
||||
// type translationKeys = keyof typeof english | string
|
||||
export type translationKeys = keyof typeof english;
|
||||
type getArgs<K extends translationKeys> = typeof english[K] extends // deno-lint-ignore no-explicit-any
|
||||
(...any: any[]) => unknown ? Parameters<typeof english[K]>
|
||||
: [];
|
||||
@@ -1,106 +0,0 @@
|
||||
import {
|
||||
BOT_ID,
|
||||
DEVELOPMENT,
|
||||
DISCORD_TOKEN,
|
||||
EVENT_HANDLER_PORT,
|
||||
EVENT_HANDLER_SECRET_KEY,
|
||||
GATEWAY_INTENTS,
|
||||
REST_AUTHORIZATION_KEY,
|
||||
REST_PORT,
|
||||
} from "../../configs.ts";
|
||||
import { createBot, createRestManager, DiscordGatewayPayload } from "../../deps.ts";
|
||||
import logger from "../utils/logger.ts";
|
||||
import { updateDevCommands } from "../utils/updateSlash.ts";
|
||||
import { BotClient, setupBotClient } from "./botClient.ts";
|
||||
import { setGuildCommands } from "./events/interactions/slash/setGuildCommands.ts";
|
||||
import { setupEventHandlers } from "./events/mod.ts";
|
||||
|
||||
export const bot = createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
botId: BOT_ID,
|
||||
events: {},
|
||||
intents: GATEWAY_INTENTS,
|
||||
}) as BotClient;
|
||||
|
||||
setupEventHandlers();
|
||||
// customizeBotInternals(bot);
|
||||
setupBotClient(bot);
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION_KEY,
|
||||
customUrl: `http://localhost:${REST_PORT}`,
|
||||
});
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
logger.info(`[DEV MODE] Updating slash commands for dev server.`);
|
||||
await updateDevCommands(bot);
|
||||
} else {
|
||||
// THIS WILL UPDATE ALL YOUR GLOBAL COMMANDS ON STARTUP
|
||||
// await updateGlobalCommands(bot);
|
||||
}
|
||||
|
||||
// Start listening on localhost.
|
||||
const server = Deno.listen({ port: EVENT_HANDLER_PORT });
|
||||
logger.info(
|
||||
`HTTP webserver running. Access it at: http://localhost:${EVENT_HANDLER_PORT}/`,
|
||||
);
|
||||
|
||||
// Connections to the server will be yielded up as an async iterable.
|
||||
for await (const conn of server) {
|
||||
// In order to not be blocking, we need to handle each connection individually
|
||||
// in its own async function.
|
||||
handleRequest(conn);
|
||||
}
|
||||
|
||||
async function handleRequest(conn: Deno.Conn) {
|
||||
// This "upgrades" a network connection into an HTTP connection.
|
||||
const httpConn = Deno.serveHttp(conn);
|
||||
// Each request sent over the HTTP connection will be yielded as an async
|
||||
// iterator from the HTTP connection.
|
||||
for await (const requestEvent of httpConn) {
|
||||
if (
|
||||
!EVENT_HANDLER_SECRET_KEY ||
|
||||
EVENT_HANDLER_SECRET_KEY !==
|
||||
requestEvent.request.headers.get("AUTHORIZATION")
|
||||
) {
|
||||
return requestEvent.respondWith(
|
||||
new Response(JSON.stringify({ error: "Invalid secret key." }), {
|
||||
status: 401,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (requestEvent.request.method !== "POST") {
|
||||
return requestEvent.respondWith(
|
||||
new Response(JSON.stringify({ error: "Method not allowed." }), {
|
||||
status: 405,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const json = (await requestEvent.request.json()) as {
|
||||
data: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
// EMITS RAW EVENT
|
||||
bot.events.raw(bot, json.data, json.shardId);
|
||||
|
||||
if (json.data.t && json.data.t !== "RESUMED") {
|
||||
// When a guild or something isn't in cache this will fetch it before doing anything else
|
||||
if (!["READY", "GUILD_LOADED_DD"].includes(json.data.t)) {
|
||||
await bot.events.dispatchRequirements(bot, json.data, json.shardId);
|
||||
// WE ALSO WANT TO UPDATE GUILD SLASH IF NECESSARY AT THIS POINT
|
||||
await setGuildCommands(bot, json.data);
|
||||
}
|
||||
|
||||
bot.handlers[json.data.t]?.(bot, json.data, json.shardId);
|
||||
}
|
||||
|
||||
requestEvent.respondWith(
|
||||
new Response(undefined, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import {
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Channel,
|
||||
Interaction,
|
||||
Member,
|
||||
PermissionStrings,
|
||||
Role,
|
||||
User,
|
||||
} from "../../../deps.ts";
|
||||
import { PermissionLevelHandlers } from "../../utils/permLevels.ts";
|
||||
import { BotClient } from "../botClient.ts";
|
||||
import english from "../languages/english.ts";
|
||||
import { translationKeys } from "../languages/translate.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
||||
|
||||
type Identity<T> = { [P in keyof T]: T[P] };
|
||||
|
||||
// TODO: make required by default true
|
||||
// Define each of the types here
|
||||
type BaseDefinition = {
|
||||
description: translationKeys;
|
||||
};
|
||||
|
||||
// Subcommand
|
||||
type SubcommandArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommand;
|
||||
// options: Omit<ArgumentDefinition, 'SubcommandArgumentDefinition' | 'SubcommandGroupArgumentDefinition'>[]
|
||||
options?: readonly ArgumentDefinition[];
|
||||
};
|
||||
|
||||
// SubcommandGroup
|
||||
type SubcommandGroupArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommandGroup;
|
||||
options: readonly SubcommandArgumentDefinition[];
|
||||
};
|
||||
|
||||
// String
|
||||
type StringArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: readonly { name: string; value: string }[];
|
||||
required?: true;
|
||||
};
|
||||
type StringOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: readonly { name: string; value: string }[];
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Integer
|
||||
type IntegerArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: readonly { name: string; value: number }[];
|
||||
required: true;
|
||||
};
|
||||
type IntegerOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: readonly { name: string; value: number }[];
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// BOOLEAN
|
||||
type BooleanArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required: true;
|
||||
};
|
||||
type BooleanOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// USER
|
||||
type UserArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required: true;
|
||||
};
|
||||
type UserOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// CHANNEL
|
||||
type ChannelArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required: true;
|
||||
};
|
||||
type ChannelOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// ROLE
|
||||
type RoleArgumentDefinition<N extends translationKeys = translationKeys> =
|
||||
& BaseDefinition
|
||||
& {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required: true;
|
||||
};
|
||||
type RoleOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// MENTIONABLE
|
||||
type MentionableArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required: true;
|
||||
};
|
||||
type MentionableOptionalArgumentDefinition<
|
||||
N extends translationKeys = translationKeys,
|
||||
> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Add each of known ArgumentDefinitions to this union.
|
||||
export type ArgumentDefinition =
|
||||
| StringArgumentDefinition
|
||||
| StringOptionalArgumentDefinition
|
||||
| IntegerArgumentDefinition
|
||||
| IntegerOptionalArgumentDefinition
|
||||
| BooleanArgumentDefinition
|
||||
| BooleanOptionalArgumentDefinition
|
||||
| UserArgumentDefinition
|
||||
| UserOptionalArgumentDefinition
|
||||
| ChannelArgumentDefinition
|
||||
| ChannelOptionalArgumentDefinition
|
||||
| RoleArgumentDefinition
|
||||
| RoleOptionalArgumentDefinition
|
||||
| MentionableArgumentDefinition
|
||||
| MentionableOptionalArgumentDefinition
|
||||
| SubcommandArgumentDefinition
|
||||
| SubcommandGroupArgumentDefinition;
|
||||
|
||||
type getName<K extends translationKeys> = typeof english[K] extends string ? typeof english[K]
|
||||
: never;
|
||||
|
||||
// OPTIONALS MUST BE FIRST!!!
|
||||
export type ConvertArgumentDefinitionsToArgs<
|
||||
T extends readonly ArgumentDefinition[],
|
||||
> = Identity<
|
||||
UnionToIntersection<
|
||||
{
|
||||
[P in keyof T]: T[P] extends StringOptionalArgumentDefinition<infer N> // STRING
|
||||
? {
|
||||
[_ in getName<N>]?: T[P]["choices"] extends readonly { name: string; value: string }[] ? // @ts-ignore ts being dumb
|
||||
T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
: T[P] extends StringArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: T[P]["choices"] extends readonly { name: string; value: string }[] ? // @ts-ignore ts being dumb
|
||||
T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
: // INTEGER
|
||||
T[P] extends IntegerOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: T[P]["choices"] extends readonly { name: string; value: number }[] ? // @ts-ignore ts being dumb
|
||||
T[P]["choices"][number]["value"]
|
||||
: number;
|
||||
}
|
||||
: T[P] extends IntegerArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: T[P]["choices"] extends readonly { name: string; value: number }[] ? // @ts-ignore ts being dumb
|
||||
T[P]["choices"][number]["value"]
|
||||
: number;
|
||||
}
|
||||
: // BOOLEAN
|
||||
T[P] extends BooleanOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: boolean }
|
||||
: T[P] extends BooleanArgumentDefinition<infer N> ? { [_ in getName<N>]: boolean }
|
||||
: // USER
|
||||
T[P] extends UserOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends UserArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: // CHANNEL
|
||||
T[P] extends ChannelOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: Channel }
|
||||
: T[P] extends ChannelArgumentDefinition<infer N> ? { [_ in getName<N>]: Channel }
|
||||
: // ROLE
|
||||
T[P] extends RoleOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: Role }
|
||||
: T[P] extends RoleArgumentDefinition<infer N> ? { [_ in getName<N>]: Role }
|
||||
: // MENTIONABLE
|
||||
T[P] extends MentionableOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: Role | {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends MentionableArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: Role | {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: // SUBCOMMAND
|
||||
T[P] extends SubcommandArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: T[P]["options"] extends readonly ArgumentDefinition[] ? // @ts-ignore ignore this for a bit
|
||||
ConvertArgumentDefinitionsToArgs<T[P]["options"]>
|
||||
: // deno-lint-ignore ban-types
|
||||
{};
|
||||
}
|
||||
: // SUBCOMMAND GROUP
|
||||
T[P] extends SubcommandGroupArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: ConvertArgumentDefinitionsToArgs<
|
||||
T[P]["options"]
|
||||
>;
|
||||
}
|
||||
: never;
|
||||
}[number]
|
||||
>
|
||||
>;
|
||||
|
||||
export interface Command<T extends readonly ArgumentDefinition[]> {
|
||||
/** The name of the command, used for both slash and message commands. */
|
||||
name: translationKeys;
|
||||
/** The type of command. */
|
||||
type?: ApplicationCommandTypes;
|
||||
/** The description of the command*/
|
||||
description: translationKeys;
|
||||
// TODO: consider type being a string like "number" | "user" for better ux
|
||||
/** The options for the command, used for both slash and message commands. */
|
||||
// options?: ApplicationCommandOption[];
|
||||
options?: T;
|
||||
execute: (
|
||||
bot: BotClient,
|
||||
data: Interaction,
|
||||
args: ConvertArgumentDefinitionsToArgs<T>,
|
||||
) => unknown;
|
||||
subcommands?: Record<
|
||||
string,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
Omit<Command<any>, "subcommands"> & { group?: string }
|
||||
>;
|
||||
/** Whether the command should have a cooldown */
|
||||
cooldown?: {
|
||||
/** How long the user needs to wait after the first execution until he can use the command again */
|
||||
seconds: number;
|
||||
/** How often the user is allowed to use the command until he is in cooldown */
|
||||
allowedUses?: number;
|
||||
};
|
||||
nsfw?: boolean;
|
||||
/** By default false */
|
||||
global?: boolean;
|
||||
/** Dm only by default false */
|
||||
dmOnly?: boolean;
|
||||
|
||||
advanced?: boolean;
|
||||
|
||||
/** Whether or not this slash command should be enabled right now. Defaults to true. */
|
||||
enabled?: boolean;
|
||||
/** Whether or not this command is still in development and should be setup in the dev server for testing. */
|
||||
dev?: boolean;
|
||||
/** Whether or not this command will take longer than 3s and need to acknowledge to discord. */
|
||||
acknowledge?: boolean;
|
||||
|
||||
permissionLevels?:
|
||||
| (keyof typeof PermissionLevelHandlers)[]
|
||||
| ((
|
||||
data: Interaction,
|
||||
command: Command<T>,
|
||||
) => boolean | Promise<boolean>);
|
||||
botServerPermissions?: PermissionStrings[];
|
||||
botChannelPermissions?: PermissionStrings[];
|
||||
userServerPermissions?: PermissionStrings[];
|
||||
userChannelPermissions?: PermissionStrings[];
|
||||
}
|
||||
|
||||
export enum PermissionLevels {
|
||||
Member,
|
||||
Moderator,
|
||||
Admin,
|
||||
ServerOwner,
|
||||
BotSupporter,
|
||||
BotDev,
|
||||
BotOwner,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export enum Milliseconds {
|
||||
Year = 1000 * 60 * 60 * 24 * 30 * 12,
|
||||
Month = 1000 * 60 * 60 * 24 * 30,
|
||||
Week = 1000 * 60 * 60 * 24 * 7,
|
||||
Day = 1000 * 60 * 60 * 24,
|
||||
Hour = 1000 * 60 * 60,
|
||||
Minute = 1000 * 60,
|
||||
Second = 1000,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const SNOWFLAKE_REGEX = /[0-9]{17,19}/;
|
||||
@@ -1,169 +0,0 @@
|
||||
import { Collection, createGatewayManager, createRestManager, endpoints } from "../../deps.ts";
|
||||
import { DISCORD_TOKEN, EVENT_HANDLER_SECRET_KEY, REST_AUTHORIZATION_KEY, REST_PORT } from "../../configs.ts";
|
||||
import { logger } from "../utils/logger.ts";
|
||||
|
||||
const log = logger({ name: "Gateway" });
|
||||
|
||||
// CREATE A SIMPLE MANAGER FOR REST
|
||||
const rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION_KEY,
|
||||
customUrl: `http://localhost:${REST_PORT}`,
|
||||
});
|
||||
|
||||
const gateway = createGatewayManager({
|
||||
// THE AUTHORIZATION WE WILL USE ON OUR EVENT HANDLER PROCESS
|
||||
secretKey: EVENT_HANDLER_SECRET_KEY,
|
||||
token: DISCORD_TOKEN,
|
||||
intents: ["GuildMessages", "Guilds"],
|
||||
// THIS WILL BASICALLY BE YOUR HANDLER FOR YOUR EVENTS.
|
||||
handleDiscordPayload: async (_, data, shardId) => {},
|
||||
});
|
||||
|
||||
const workers = new Collection<number, Worker>();
|
||||
|
||||
async function startGateway() {
|
||||
// CALL THE REST PROCESS TO GET GATEWAY DATA
|
||||
const result = await rest.runMethod(rest, "get", endpoints.GATEWAY_BOT())
|
||||
.then((res) => ({
|
||||
url: res.url,
|
||||
shards: res.shards,
|
||||
sessionStartLimit: {
|
||||
total: res.session_start_limit.total,
|
||||
remaining: res.session_start_limit.remaining,
|
||||
resetAfter: res.session_start_limit.reset_after,
|
||||
maxConcurrency: res.session_start_limit.max_concurrency,
|
||||
},
|
||||
}));
|
||||
|
||||
// LOAD DATA FROM DISCORDS RECOMMENDATIONS OR YOUR OWN CUSTOM ONES HERE
|
||||
gateway.shardsRecommended = result.shards;
|
||||
gateway.sessionStartLimitTotal = result.sessionStartLimit.total;
|
||||
gateway.sessionStartLimitRemaining = result.sessionStartLimit.remaining;
|
||||
gateway.sessionStartLimitResetAfter = result.sessionStartLimit.resetAfter;
|
||||
gateway.maxConcurrency = result.sessionStartLimit.maxConcurrency;
|
||||
gateway.maxShards = result.shards;
|
||||
gateway.lastShardId = result.shards;
|
||||
|
||||
// PREPARE BUCKETS FOR IDENTIFYING
|
||||
gateway.prepareBuckets(gateway, 0, result.shards);
|
||||
|
||||
function startWorker(
|
||||
workerId: number,
|
||||
bucketId: number,
|
||||
firstShardId: number,
|
||||
lastShardId: number,
|
||||
) {
|
||||
const worker = workers.get(workerId);
|
||||
if (!worker) return;
|
||||
|
||||
// TRIGGER IDENTIFY IN WORKER
|
||||
worker.postMessage(
|
||||
JSON.stringify({
|
||||
type: "IDENTIFY",
|
||||
shardId: firstShardId,
|
||||
shardsRecommended: result.shards,
|
||||
sessionStartLimitTotal: result.sessionStartLimit.total,
|
||||
sessionStartLimitRemaining: result.sessionStartLimit.remaining,
|
||||
sessionStartLimitResetAfter: result.sessionStartLimit.resetAfter,
|
||||
maxConcurrency: result.sessionStartLimit.maxConcurrency,
|
||||
maxShards: gateway.maxShards,
|
||||
lastShardId: lastShardId,
|
||||
workerId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
gateway.buckets.forEach((bucket, bucketId) => {
|
||||
for (let i = 0; i < bucket.workers.length; i++) {
|
||||
const workerId = bucket.workers[i][0];
|
||||
const worker = new Worker(
|
||||
new URL("./worker.ts", import.meta.url).href,
|
||||
{
|
||||
name: `w-${workerId}-b${bucketId}`,
|
||||
type: "module",
|
||||
},
|
||||
);
|
||||
workers.set(workerId, worker);
|
||||
|
||||
if (bucket.workers[i + 1]) {
|
||||
worker.onmessage = function (message) {
|
||||
const data = JSON.parse(message.data);
|
||||
if (data.type === "ALL_SHARDS_READY") {
|
||||
const queue = bucket.workers[i + 1];
|
||||
if (queue) {
|
||||
startWorker(
|
||||
queue[0],
|
||||
bucketId,
|
||||
queue[1],
|
||||
queue[queue.length - 1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === "RESHARDED") {
|
||||
const nextWorker = workers.get(workerId + 1);
|
||||
if (nextWorker) {
|
||||
nextWorker.postMessage(
|
||||
JSON.stringify({
|
||||
type: "RESHARD",
|
||||
results: data.results,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// THIS IS FINAL WORKER
|
||||
worker.onmessage = function (message) {
|
||||
const data = JSON.parse(message.data);
|
||||
if (data.type === "RESHARDED") {
|
||||
// THERE IS NO NEXT WORKER SO TELL ALL WORKERS TO CLOSE OLD GATEWAYS
|
||||
workers.forEach((workerx) => {
|
||||
workerx.postMessage(
|
||||
JSON.stringify({
|
||||
type: "RESHARDED-CLOSEOLD",
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const queue = bucket.workers[0];
|
||||
startWorker(queue[0], bucketId, queue[1], queue[queue.length - 1]);
|
||||
});
|
||||
}
|
||||
|
||||
startGateway();
|
||||
|
||||
setInterval(async () => {
|
||||
console.log("GW DEBUG", "[Resharding] Checking if resharding is needed.");
|
||||
|
||||
const results = await rest.runMethod(rest, "get", endpoints.GATEWAY_BOT())
|
||||
.then((res) => ({
|
||||
url: res.url,
|
||||
shards: res.shards,
|
||||
sessionStartLimit: {
|
||||
total: res.session_start_limit.total,
|
||||
remaining: res.session_start_limit.remaining,
|
||||
resetAfter: res.session_start_limit.reset_after,
|
||||
maxConcurrency: res.session_start_limit.max_concurrency,
|
||||
},
|
||||
}));
|
||||
const percentage = ((results.shards - gateway.maxShards) / gateway.maxShards) * 100;
|
||||
// Less than necessary% being used so do nothing
|
||||
if (percentage < gateway.reshardPercentage) return;
|
||||
|
||||
// Don't have enough identify rate limits to reshard
|
||||
if (results.sessionStartLimit.remaining < results.shards) return;
|
||||
|
||||
workers.first()?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "RESHARD",
|
||||
results,
|
||||
}),
|
||||
);
|
||||
// DAILY
|
||||
}, 1000 * 60 * 60 * 24);
|
||||
@@ -1,300 +0,0 @@
|
||||
import {
|
||||
DEVELOPMENT,
|
||||
DISCORD_TOKEN,
|
||||
EVENT_HANDLER_PORT,
|
||||
EVENT_HANDLER_SECRET_KEY,
|
||||
EVENT_HANDLER_URL,
|
||||
} from "../../configs.ts";
|
||||
import { Collection, createGatewayManager, DiscordReady, GatewayManager, GetGatewayBot } from "../../deps.ts";
|
||||
import { logger } from "../utils/logger.ts";
|
||||
|
||||
let gateway: GatewayManager;
|
||||
// FOR RESHARDED
|
||||
let gatewayPendingClosing: GatewayManager;
|
||||
let workerId: number;
|
||||
|
||||
function spawnGateway(shardId: number, options: Partial<GatewayManager>) {
|
||||
const log = logger({
|
||||
name: `GatewayWorker: ${workerId}`,
|
||||
});
|
||||
|
||||
log.info(
|
||||
`Spawning the worker gateway for shard #${shardId}\n`,
|
||||
options,
|
||||
);
|
||||
gateway = createGatewayManager({
|
||||
// LOAD DATA FROM DISCORDS RECOMMENDATIONS OR YOUR OWN CUSTOM ONES HERE
|
||||
shardsRecommended: options.shardsRecommended,
|
||||
sessionStartLimitTotal: options.sessionStartLimitTotal,
|
||||
sessionStartLimitRemaining: options.sessionStartLimitRemaining,
|
||||
sessionStartLimitResetAfter: options.sessionStartLimitResetAfter,
|
||||
maxConcurrency: options.maxConcurrency,
|
||||
maxShards: options.maxShards,
|
||||
// SET STARTING SHARD ID
|
||||
firstShardId: shardId,
|
||||
// SET LAST SHARD ID
|
||||
lastShardId: options.lastShardId ?? shardId,
|
||||
// THE AUTHORIZATION WE WILL USE ON OUR EVENT HANDLER PROCESS
|
||||
secretKey: EVENT_HANDLER_SECRET_KEY,
|
||||
token: DISCORD_TOKEN,
|
||||
intents: ["GuildMessages", "Guilds", "GuildMembers"],
|
||||
handleDiscordPayload: async function (_, data, shardId) {
|
||||
// TRIGGER RAW EVENT
|
||||
if (!data.t) return;
|
||||
|
||||
const id = (data.t &&
|
||||
["GUILD_CREATE", "GUILD_DELETE", "GUILD_UPDATE"].includes(data.t)
|
||||
? (data.d as any)?.id
|
||||
: (data.d as any)?.guild_id) ?? "000000000000000000";
|
||||
|
||||
// IF FINAL SHARD BECAME READY TRIGGER NEXT WORKER
|
||||
if (data.t === "READY") {
|
||||
log.info(
|
||||
`Shard online`,
|
||||
);
|
||||
|
||||
if (shardId === gateway.lastShardId) {
|
||||
// @ts-ignore
|
||||
postMessage(
|
||||
JSON.stringify({
|
||||
type: "ALL_SHARDS_READY",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DONT SEND THESE EVENTS USELESS TO BOT
|
||||
if (["GUILD_LOADED_DD"].includes(data.t)) return;
|
||||
|
||||
// Debug mode only
|
||||
log.debug(`New Event:\n`, data);
|
||||
|
||||
await fetch(`http://${EVENT_HANDLER_URL}:${EVENT_HANDLER_PORT}`, {
|
||||
headers: {
|
||||
Authorization: gateway.secretKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
shardId,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
// BELOW IS FOR DENO MEMORY LEAK
|
||||
return res.text();
|
||||
})
|
||||
.catch((err) => log.error("Error Sending Event:\n", err));
|
||||
},
|
||||
});
|
||||
|
||||
// START THE GATEWAY
|
||||
gateway.spawnShards(gateway, shardId);
|
||||
|
||||
return gateway;
|
||||
}
|
||||
|
||||
interface IdentifyPayload {
|
||||
type: "IDENTIFY";
|
||||
shardId: number;
|
||||
shards: number;
|
||||
sessionStartLimit: {
|
||||
total: number;
|
||||
remaining: number;
|
||||
resetAfter: number;
|
||||
maxConcurrency: number;
|
||||
};
|
||||
shardsRecommended: number;
|
||||
sessionStartLimitTotal: number;
|
||||
sessionStartLimitRemaining: number;
|
||||
sessionStartLimitResetAfter: number;
|
||||
maxConcurrency: number;
|
||||
maxShards: number;
|
||||
lastShardId: number;
|
||||
workerId: number;
|
||||
}
|
||||
|
||||
interface ReshardPayload {
|
||||
type: "RESHARD";
|
||||
results: GetGatewayBot;
|
||||
}
|
||||
|
||||
interface FullyReshardedPayload {
|
||||
type: "RESHARDED-CLOSEOLD";
|
||||
}
|
||||
|
||||
// @ts-ignore this should not be erroring
|
||||
self.onmessage = async function (message: MessageEvent<string>) {
|
||||
const log = logger({
|
||||
name: `GatewayWorker${JSON.parse(message.data).workerId ? `: ${JSON.parse(message.data).workerId}` : undefined}`,
|
||||
});
|
||||
|
||||
log.debug(`New Message:\n`, message.data);
|
||||
|
||||
const data = JSON.parse(message.data) as
|
||||
| IdentifyPayload
|
||||
| ReshardPayload
|
||||
| FullyReshardedPayload;
|
||||
|
||||
if (data.type === "IDENTIFY") {
|
||||
workerId = data.workerId;
|
||||
|
||||
gateway = spawnGateway(data.shardId, {
|
||||
shardsRecommended: data.shardsRecommended,
|
||||
sessionStartLimitTotal: data.sessionStartLimitTotal,
|
||||
sessionStartLimitRemaining: data.sessionStartLimitRemaining,
|
||||
sessionStartLimitResetAfter: data.sessionStartLimitResetAfter,
|
||||
maxConcurrency: data.maxConcurrency,
|
||||
maxShards: data.maxShards,
|
||||
lastShardId: data.lastShardId,
|
||||
spawnShardDelay: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "RESHARDED-CLOSEOLD") {
|
||||
log.info("[Resharding] Closing old gateways.");
|
||||
await gateway.resharding.closeOldShards(gatewayPendingClosing);
|
||||
}
|
||||
|
||||
if (data.type === "RESHARD") {
|
||||
log.info("[Worker] Resharding the worker.");
|
||||
gateway.resharding.isPending = async function (gateway: GatewayManager) {
|
||||
for (let i = gateway.firstShardId; i < gateway.lastShardId; i++) {
|
||||
const shard = gateway.shards.get(i);
|
||||
if (!shard?.ready) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
async function processResharding(
|
||||
oldGateway: GatewayManager,
|
||||
results: GetGatewayBot,
|
||||
) {
|
||||
oldGateway.debug(
|
||||
"GW DEBUG",
|
||||
"[Resharding] Starting the reshard process.",
|
||||
);
|
||||
|
||||
const gateway = createGatewayManager({
|
||||
...oldGateway,
|
||||
// RESET THE SETS AND COLLECTIONS
|
||||
cache: {
|
||||
guildIds: new Set(),
|
||||
loadingGuildIds: new Set(),
|
||||
editedMessages: new Collection(),
|
||||
},
|
||||
shards: new Collection(),
|
||||
loadingShards: new Collection(),
|
||||
buckets: new Collection(),
|
||||
utf8decoder: new TextDecoder(),
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(oldGateway)) {
|
||||
if (key === "handleDiscordPayload") {
|
||||
gateway.handleDiscordPayload = async function (_, data, shardId) {
|
||||
if (data.t === "READY") {
|
||||
const payload = data.d as DiscordReady;
|
||||
log.info(
|
||||
`Shard #${payload.shard?.[0]} online`,
|
||||
);
|
||||
if (shardId === gateway.lastShardId) {
|
||||
// @ts-ignore
|
||||
postMessage(
|
||||
JSON.stringify({
|
||||
type: "RESHARDED",
|
||||
results,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await gateway.resharding.markNewGuildShardId(
|
||||
payload.guilds.map((g) => BigInt(g.id)),
|
||||
shardId,
|
||||
);
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// DON"T OVERRIDE THESE
|
||||
if (
|
||||
["cache", "shards", "loadingShards", "buckets", "utf8decoder"]
|
||||
.includes(key)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// USE ANY CUSTOMIZED OPTIONS FROM OLD GATEWAY
|
||||
// @ts-ignore silly ts error
|
||||
gateway[key] = oldGateway[key as keyof typeof oldGateway];
|
||||
}
|
||||
|
||||
// Begin resharding
|
||||
// If more than 100K servers, begin switching to 16x sharding
|
||||
if (gateway.useOptimalLargeBotSharding) {
|
||||
log.info(
|
||||
"[Resharding] Using optimal large bot sharding solution.",
|
||||
);
|
||||
gateway.maxShards = gateway.calculateMaxShards(
|
||||
results.shards,
|
||||
results.sessionStartLimit.maxConcurrency,
|
||||
);
|
||||
} else {
|
||||
gateway.maxShards = results.shards;
|
||||
}
|
||||
|
||||
// FOR MANUAL SHARD CONTROL, OVERRIDE THIS SHARD ID!
|
||||
gateway.lastShardId = oldGateway.lastShardId === oldGateway.maxShards - 1
|
||||
? gateway.maxShards - 1
|
||||
: oldGateway.lastShardId;
|
||||
gateway.shardsRecommended = results.shards;
|
||||
gateway.sessionStartLimitTotal = results.sessionStartLimit.total;
|
||||
gateway.sessionStartLimitRemaining = results.sessionStartLimit.remaining;
|
||||
gateway.sessionStartLimitResetAfter = results.sessionStartLimit.resetAfter;
|
||||
gateway.maxConcurrency = results.sessionStartLimit.maxConcurrency;
|
||||
|
||||
gateway.spawnShards(gateway, gateway.firstShardId);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// TIMER TO KEEP CHECKING WHEN ALL SHARDS HAVE RESHARDED
|
||||
const timer = setInterval(async () => {
|
||||
const pending = await gateway.resharding.isPending(
|
||||
gateway,
|
||||
oldGateway,
|
||||
);
|
||||
// STILL PENDING ON SOME SHARDS TO BE CREATED
|
||||
if (pending) return;
|
||||
|
||||
// ENABLE EVENTS ON NEW SHARDS AND IGNORE EVENTS ON OLD
|
||||
const oldHandler = oldGateway.handleDiscordPayload;
|
||||
gateway.handleDiscordPayload = oldHandler;
|
||||
oldGateway.handleDiscordPayload = function (og, data, shardId) {
|
||||
// ALLOW EXCEPTION FOR CHUNKING TO PREVENT REQUESTS FREEZING
|
||||
if (data.t !== "GUILD_MEMBERS_CHUNK") return;
|
||||
oldHandler(og, data, shardId);
|
||||
};
|
||||
|
||||
// STOP TIMER
|
||||
clearInterval(timer);
|
||||
await gateway.resharding.editGuildShardIds();
|
||||
gatewayPendingClosing = oldGateway;
|
||||
gateway.debug("GW DEBUG", "[Resharding] Complete.");
|
||||
resolve(gateway);
|
||||
}, 30000);
|
||||
}) as Promise<GatewayManager>;
|
||||
}
|
||||
|
||||
gateway = await processResharding(gateway, data.results);
|
||||
log.info(`Resharded the worker.`);
|
||||
// @ts-ignore this should not be erroring
|
||||
postMessage(
|
||||
JSON.stringify({
|
||||
type: "RESHARDED",
|
||||
results: data.results,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
// START FILE FOR REST PROCESS
|
||||
import { DISCORD_TOKEN, REST_AUTHORIZATION_KEY, REST_PORT } from "../../configs.ts";
|
||||
import { BASE_URL, createRestManager } from "../../deps.ts";
|
||||
import { logger } from "../utils/logger.ts";
|
||||
|
||||
const log = logger({ name: "REST" });
|
||||
|
||||
// CREATES THE FUNCTIONALITY FOR MANAGING THE REST REQUESTS
|
||||
const rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION_KEY,
|
||||
customUrl: `http://localhost:${REST_PORT}`,
|
||||
debug: console.log,
|
||||
});
|
||||
|
||||
// START LISTENING TO THE URL(localhost)
|
||||
const server = Deno.listen({ port: REST_PORT });
|
||||
log.info(
|
||||
`HTTP webserver running. Access it at: http://localhost:${REST_PORT}/`,
|
||||
);
|
||||
|
||||
// Connections to the server will be yielded up as an async iterable.
|
||||
for await (const conn of server) {
|
||||
// In order to not be blocking, we need to handle each connection individually
|
||||
// in its own async function.
|
||||
handleRequest(conn);
|
||||
}
|
||||
|
||||
async function handleRequest(conn: Deno.Conn) {
|
||||
// This "upgrades" a network connection into an HTTP connection.
|
||||
const httpConn = Deno.serveHttp(conn);
|
||||
// Each request sent over the HTTP connection will be yielded as an async
|
||||
// iterator from the HTTP connection.
|
||||
for await (const requestEvent of httpConn) {
|
||||
if (
|
||||
!REST_AUTHORIZATION_KEY ||
|
||||
REST_AUTHORIZATION_KEY !==
|
||||
requestEvent.request.headers.get("AUTHORIZATION")
|
||||
) {
|
||||
return requestEvent.respondWith(
|
||||
new Response(JSON.stringify({ error: "Invalid authorization key." }), {
|
||||
status: 401,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const json = requestEvent.request.body ? (await requestEvent.request.json()) : undefined;
|
||||
|
||||
try {
|
||||
const result = await rest.runMethod(
|
||||
rest,
|
||||
requestEvent.request.method as RequestMethod,
|
||||
`${BASE_URL}${
|
||||
requestEvent.request.url.substring(
|
||||
`http://localhost:${REST_PORT}`.length,
|
||||
)
|
||||
}`,
|
||||
json,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
requestEvent.respondWith(
|
||||
new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
requestEvent.respondWith(
|
||||
new Response(undefined, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
requestEvent.respondWith(
|
||||
new Response(JSON.stringify(error), {
|
||||
status: error.code,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RequestMethod = "post" | "put" | "delete" | "patch";
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ActionRow, ButtonStyles, MessageComponentTypes } from "../../deps.ts";
|
||||
import { SNOWFLAKE_REGEX } from "../constants/regexes.ts";
|
||||
|
||||
export class Components extends Array<ActionRow> {
|
||||
constructor(...args: ActionRow[]) {
|
||||
super(...args);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addActionRow() {
|
||||
// Don't allow more than 5 Action Rows
|
||||
if (this.length === 5) return this;
|
||||
|
||||
this.push({
|
||||
type: 1,
|
||||
components: [] as unknown as ActionRow["components"],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addButton(
|
||||
label: string,
|
||||
style: keyof typeof ButtonStyles,
|
||||
customIdOrLink: string,
|
||||
options?: { emoji?: string | bigint; disabled?: boolean },
|
||||
) {
|
||||
// No Action Row has been created so do it
|
||||
if (!this.length) this.addActionRow();
|
||||
|
||||
// Get the last Action Row
|
||||
let row = this[this.length - 1];
|
||||
|
||||
// If the Action Row already has 5 buttons create a new one
|
||||
if (row.components.length === 5) {
|
||||
this.addActionRow();
|
||||
row = this[this.length - 1];
|
||||
|
||||
// Apparently there are already 5 Full Action Rows so don't add the button
|
||||
if (row.components.length === 5) return this;
|
||||
}
|
||||
|
||||
row.components.push({
|
||||
type: MessageComponentTypes.Button,
|
||||
label: label,
|
||||
customId: style !== "Link" ? customIdOrLink : undefined,
|
||||
style: ButtonStyles[style],
|
||||
emoji: this.#stringToEmoji(options?.emoji),
|
||||
url: style === "Link" ? customIdOrLink : undefined,
|
||||
disabled: options?.disabled,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
#stringToEmoji(emoji?: string | bigint) {
|
||||
if (!emoji) return;
|
||||
|
||||
emoji = emoji.toString();
|
||||
|
||||
// A snowflake id was provided
|
||||
if (SNOWFLAKE_REGEX.test(emoji)) {
|
||||
return {
|
||||
id: BigInt(emoji.match(SNOWFLAKE_REGEX)![0]),
|
||||
};
|
||||
}
|
||||
|
||||
// A unicode emoji was provided
|
||||
return {
|
||||
name: emoji,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import { Bot, Embed, User } from "../../deps.ts";
|
||||
|
||||
const embedLimits = {
|
||||
title: 256,
|
||||
description: 4096,
|
||||
fieldName: 256,
|
||||
fieldValue: 1024,
|
||||
footerText: 2048,
|
||||
authorName: 256,
|
||||
fields: 25,
|
||||
total: 6000,
|
||||
};
|
||||
|
||||
export class Embeds extends Array<Embed> {
|
||||
/** The amount of characters in the embed. */
|
||||
currentTotal = 0;
|
||||
/** Whether the limits should be enforced or not. */
|
||||
enforceLimits = true;
|
||||
/** If a file is attached to the message it will be added here. */
|
||||
file?: EmbedFile;
|
||||
bot: Bot;
|
||||
|
||||
constructor(bot: Bot, enforceLimits = true) {
|
||||
super();
|
||||
this.bot = bot;
|
||||
// By default we will always want to enforce discord limits but this option allows us to bypass for whatever reason.
|
||||
if (!enforceLimits) this.enforceLimits = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fitData(data: string, max: number) {
|
||||
// If the string is bigger then the allowed max shorten it.
|
||||
if (data.length > max) data = data.substring(0, max);
|
||||
// Check the amount of characters left for this embed
|
||||
const availableCharacters = embedLimits.total - this.currentTotal;
|
||||
// If it is maxed out already return empty string as nothing can be added anymore
|
||||
if (!availableCharacters) return ``;
|
||||
// If the string breaks the maximum embed limit then shorten it.
|
||||
if (this.currentTotal + data.length > embedLimits.total) {
|
||||
return data.substring(0, availableCharacters);
|
||||
}
|
||||
// Return the data as is with no changes.
|
||||
return data;
|
||||
}
|
||||
|
||||
setAuthor(name: string, iconUrl?: string | User, url?: string) {
|
||||
const embed = this.#getLastEmbed();
|
||||
const finalName = this.enforceLimits ? this.fitData(name, embedLimits.authorName) : name;
|
||||
|
||||
if (typeof iconUrl === "string") {
|
||||
embed.author = { name: finalName, iconUrl, url };
|
||||
} else if (iconUrl) {
|
||||
embed.author = {
|
||||
name: finalName,
|
||||
iconUrl: this.bot.helpers.avatarURL(
|
||||
iconUrl.id,
|
||||
iconUrl?.discriminator,
|
||||
{
|
||||
avatar: iconUrl.avatar!,
|
||||
},
|
||||
),
|
||||
url,
|
||||
};
|
||||
} else {
|
||||
embed.author = { name: finalName, url };
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
this.#getLastEmbed().color = color.toLowerCase() === `random`
|
||||
? // Random color
|
||||
Math.floor(Math.random() * (0xffffff + 1))
|
||||
: // Convert the hex to a acceptable color for discord
|
||||
parseInt(color.replace("#", ""), 16);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setDescription(description: string | string[]) {
|
||||
if (Array.isArray(description)) description = description.join("\n");
|
||||
this.#getLastEmbed().description = this.fitData(
|
||||
description,
|
||||
embedLimits.description,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addField(name: string, value: string, inline = false) {
|
||||
const embed = this.#getLastEmbed();
|
||||
|
||||
if (embed.fields!.length >= 25) return this;
|
||||
|
||||
embed.fields!.push({
|
||||
name: this.fitData(name, embedLimits.fieldName),
|
||||
value: this.fitData(value, embedLimits.fieldValue),
|
||||
inline,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addBlankField(inline = false) {
|
||||
return this.addField("\u200B", "\u200B", inline);
|
||||
}
|
||||
|
||||
attachFile(file: unknown, name: string) {
|
||||
this.file = {
|
||||
blob: file,
|
||||
name,
|
||||
};
|
||||
this.setImage(`attachment://${name}`);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFooter(text: string, icon?: string) {
|
||||
this.#getLastEmbed().footer = {
|
||||
text: this.fitData(text, embedLimits.footerText),
|
||||
iconUrl: icon,
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setImage(url: string | User) {
|
||||
if (typeof url === "string") this.#getLastEmbed().image = { url };
|
||||
else {
|
||||
this.#getLastEmbed().image = {
|
||||
url: this.bot.helpers.avatarURL(url.id, url.discriminator, {
|
||||
avatar: url.avatar!,
|
||||
size: 2048,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setTimestamp(time: number | string = Date.now()) {
|
||||
this.#getLastEmbed().timestamp = typeof time === "string" ? Date.parse(time) : time;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setTitle(title: string, url?: string) {
|
||||
this.#getLastEmbed().title = this.fitData(title, embedLimits.title);
|
||||
if (url) this.#getLastEmbed().url = url;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setThumbnail(url: string) {
|
||||
this.#getLastEmbed().thumbnail = { url };
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addEmbed(embed?: Embed) {
|
||||
if (this.length === 10) return this;
|
||||
|
||||
this.push({ ...embed, fields: embed?.fields ?? [] });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Get the last Embed, if there is no it will create one */
|
||||
#getLastEmbed() {
|
||||
if (this.length) return this[this.length - 1];
|
||||
|
||||
this.push({
|
||||
fields: [],
|
||||
});
|
||||
|
||||
return this[0];
|
||||
}
|
||||
}
|
||||
|
||||
export interface EmbedFile {
|
||||
blob: unknown;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default Embeds;
|
||||
@@ -1,148 +0,0 @@
|
||||
import { User } from "../../deps.ts";
|
||||
import { Milliseconds } from "../constants/milliseconds.ts";
|
||||
|
||||
export function chooseRandom<T>(array: T[]) {
|
||||
return array[Math.floor(Math.random() * array.length)]!;
|
||||
}
|
||||
|
||||
export function getUserTag(user: User) {
|
||||
return `${user.username}#${user.discriminator}`;
|
||||
}
|
||||
|
||||
export function toTitleCase(text: string) {
|
||||
return text
|
||||
.split(" ")
|
||||
.map((
|
||||
word,
|
||||
) => (word[0] ? `${word[0].toUpperCase()}${word.substring(1).toLowerCase()}` : word))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/** This function should be used when you want to convert milliseconds to a human readable format like 1d5h. */
|
||||
export function humanizeMilliseconds(milliseconds: number) {
|
||||
const years = Math.floor(milliseconds / Milliseconds.Year);
|
||||
const months = Math.floor(
|
||||
(milliseconds % Milliseconds.Year) / Milliseconds.Month,
|
||||
);
|
||||
const weeks = Math.floor(
|
||||
((milliseconds % Milliseconds.Year) % Milliseconds.Month) /
|
||||
Milliseconds.Week,
|
||||
);
|
||||
const days = Math.floor(
|
||||
(((milliseconds % Milliseconds.Year) % Milliseconds.Month) %
|
||||
Milliseconds.Week) / Milliseconds.Day,
|
||||
);
|
||||
const hours = Math.floor(
|
||||
((((milliseconds % Milliseconds.Year) % Milliseconds.Month) %
|
||||
Milliseconds.Week) % Milliseconds.Day) /
|
||||
Milliseconds.Hour,
|
||||
);
|
||||
const minutes = Math.floor(
|
||||
(((((milliseconds % Milliseconds.Year) % Milliseconds.Month) %
|
||||
Milliseconds.Week) % Milliseconds.Day) %
|
||||
Milliseconds.Hour) /
|
||||
Milliseconds.Minute,
|
||||
);
|
||||
const seconds = Math.floor(
|
||||
((((((milliseconds % Milliseconds.Year) % Milliseconds.Month) %
|
||||
Milliseconds.Week) % Milliseconds.Day) %
|
||||
Milliseconds.Hour) %
|
||||
Milliseconds.Minute) /
|
||||
Milliseconds.Second,
|
||||
);
|
||||
|
||||
const YearString = years ? `${years}y ` : "";
|
||||
const monthString = months ? `${months}mo ` : "";
|
||||
const weekString = weeks ? `${weeks}w ` : "";
|
||||
const dayString = days ? `${days}d ` : "";
|
||||
const hourString = hours ? `${hours}h ` : "";
|
||||
const minuteString = minutes ? `${minutes}m ` : "";
|
||||
const secondString = seconds ? `${seconds}s ` : "";
|
||||
|
||||
return (
|
||||
`${YearString}${monthString}${weekString}${dayString}${hourString}${minuteString}${secondString}`
|
||||
.trimEnd() || "1s"
|
||||
);
|
||||
}
|
||||
|
||||
/** This function helps convert a string like 1d5h to milliseconds. */
|
||||
export function stringToMilliseconds(text: string) {
|
||||
const matches = text.match(/\d+(y|mo|w|d|h|m|s){1}/gi);
|
||||
if (!matches) return;
|
||||
|
||||
let total = 0;
|
||||
|
||||
for (const match of matches) {
|
||||
// Finds the first of these letters
|
||||
const validMatch = /(y|mo|w|d|h|m|s)/.exec(match);
|
||||
// if none of them were found cancel
|
||||
if (!validMatch) return;
|
||||
// Get the number which should be before the index of that match
|
||||
const number = match.substring(0, validMatch.index);
|
||||
// Get the letter that was found
|
||||
const [letter] = validMatch;
|
||||
if (!number || !letter) return;
|
||||
|
||||
let multiplier = Milliseconds.Second;
|
||||
switch (letter.toLowerCase()) {
|
||||
case "y":
|
||||
multiplier = Milliseconds.Year;
|
||||
break;
|
||||
case "mo":
|
||||
multiplier = Milliseconds.Month;
|
||||
break;
|
||||
case "w":
|
||||
multiplier = Milliseconds.Week;
|
||||
break;
|
||||
case "d":
|
||||
multiplier = Milliseconds.Day;
|
||||
break;
|
||||
case "h":
|
||||
multiplier = Milliseconds.Hour;
|
||||
break;
|
||||
case "m":
|
||||
multiplier = Milliseconds.Minute;
|
||||
break;
|
||||
}
|
||||
|
||||
const amount = number ? parseInt(number, 10) : undefined;
|
||||
if (!amount) return;
|
||||
|
||||
total += amount * multiplier;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
export function chunkStrings(
|
||||
array: string[],
|
||||
size = 2000,
|
||||
lineSeparator = "\n",
|
||||
) {
|
||||
const responses: string[] = [];
|
||||
let response = "";
|
||||
for (const text of array) {
|
||||
const nextText = response.length && lineSeparator ? `${lineSeparator}${text}` : text;
|
||||
if (response.length + nextText.length >= size) {
|
||||
responses.push(response);
|
||||
response = "";
|
||||
}
|
||||
response += nextText;
|
||||
}
|
||||
responses.push(response);
|
||||
return responses;
|
||||
}
|
||||
|
||||
export const timestamps = {
|
||||
ShortTime: "t",
|
||||
LongTime: "T",
|
||||
ShortDate: "d",
|
||||
LongDate: "D",
|
||||
ShortDateTime: "f",
|
||||
LongDateTime: "F",
|
||||
Relative: "R",
|
||||
} as const;
|
||||
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from "../../deps.ts";
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Debug, "DEBUG"],
|
||||
[LogLevels.Info, "INFO"],
|
||||
[LogLevels.Warn, "WARN"],
|
||||
[LogLevels.Error, "ERROR"],
|
||||
[LogLevels.Fatal, "FATAL"],
|
||||
]);
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, gray],
|
||||
[LogLevels.Info, cyan],
|
||||
[LogLevels.Warn, yellow],
|
||||
[LogLevels.Error, (str: string) => red(str)],
|
||||
[LogLevels.Fatal, (str: string) => red(bold(italic(str)))],
|
||||
]);
|
||||
|
||||
export function logger({
|
||||
logLevel = LogLevels.Info,
|
||||
name,
|
||||
}: {
|
||||
logLevel?: LogLevels;
|
||||
name?: string;
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
if (level < logLevel) return;
|
||||
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
|
||||
const date = new Date();
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || "DEBUG"),
|
||||
name ? `${name} >` : ">",
|
||||
...args,
|
||||
];
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log);
|
||||
case LogLevels.Info:
|
||||
return console.info(...log);
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log);
|
||||
case LogLevels.Error:
|
||||
return console.error(...log);
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log);
|
||||
default:
|
||||
return console.log(...log);
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
log(LogLevels.Debug, ...args);
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
log(LogLevels.Info, ...args);
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
log(LogLevels.Warn, ...args);
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
log(LogLevels.Error, ...args);
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
log(LogLevels.Fatal, ...args);
|
||||
}
|
||||
|
||||
return {
|
||||
log,
|
||||
setLevel,
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
};
|
||||
}
|
||||
|
||||
export default logger();
|
||||
@@ -1,288 +0,0 @@
|
||||
import {
|
||||
ApplicationCommandOption,
|
||||
ApplicationCommandOptionTypes,
|
||||
Bot,
|
||||
Channel,
|
||||
ChannelTypes,
|
||||
Collection,
|
||||
InteractionDataOption,
|
||||
Member,
|
||||
Message,
|
||||
Role,
|
||||
User,
|
||||
} from "../../deps.ts";
|
||||
import { getLanguage, translate } from "../bot/languages/translate.ts";
|
||||
import { SNOWFLAKE_REGEX } from "../constants/regexes.ts";
|
||||
|
||||
// Mapped by `language-commandName`
|
||||
const translatedOptionNamesCache = new Map<string, Record<string, string>>();
|
||||
|
||||
/** Translates all options of the command to an object: translatedOptionName: optionName */
|
||||
export function translateOptionNames(
|
||||
bot: Bot,
|
||||
guildId: bigint,
|
||||
options: ApplicationCommandOption[],
|
||||
commandName?: string,
|
||||
): Record<string, string> {
|
||||
const language = getLanguage(guildId);
|
||||
// RETURN THE ALREADY TRANSLATED OPTIONS WHICH ARE IN CACHE
|
||||
if (
|
||||
commandName && translatedOptionNamesCache.has(`${language}-${commandName}`)
|
||||
) {
|
||||
return translatedOptionNamesCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
// TRANSLATE ALL OPTIONS
|
||||
let translated: Record<string, string> = {};
|
||||
for (const option of options) {
|
||||
// @ts-ignore ts being dumb
|
||||
translated[translate(bot, guildId, option.name).toLowerCase()] = translate(
|
||||
bot,
|
||||
"english",
|
||||
// @ts-ignore ts being dumb
|
||||
option.name,
|
||||
);
|
||||
if (option.options) {
|
||||
translated = {
|
||||
...translated,
|
||||
...translateOptionNames(bot, guildId, option.options),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SAVE THE TRANSLATED OPTIONS IN CACHE FOR FASTER ACCESS
|
||||
if (commandName) {
|
||||
translatedOptionNamesCache.set(`${language}-${commandName}`, translated);
|
||||
}
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
function convertOptionValue(
|
||||
option: InteractionDataOption,
|
||||
resolved?: {
|
||||
/** The Ids and Message objects */
|
||||
messages?: Collection<bigint, Message>;
|
||||
/** The Ids and User objects */
|
||||
users?: Collection<bigint, User>;
|
||||
/** The Ids and partial Member objects */
|
||||
members?: Collection<bigint, Member>;
|
||||
/** The Ids and Role objects */
|
||||
roles?: Collection<bigint, Role>;
|
||||
/** The Ids and partial Channel objects */
|
||||
channels?: Collection<
|
||||
bigint,
|
||||
{
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
>;
|
||||
},
|
||||
translateOptions?: Record<string, string>,
|
||||
): [
|
||||
string,
|
||||
(
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
),
|
||||
] {
|
||||
const value = typeof option.value === "string" && SNOWFLAKE_REGEX.test(option.value) ? BigInt(option.value) : 0n;
|
||||
// THE OPTION IS A CHANNEL
|
||||
if (option.type === ApplicationCommandOptionTypes.Channel) {
|
||||
const channel = resolved?.channels?.get(value);
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, channel!];
|
||||
}
|
||||
|
||||
// THE OPTION IS A ROLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Role) {
|
||||
const role = resolved?.roles?.get(value);
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, role!];
|
||||
}
|
||||
|
||||
// THE OPTION IS A USER
|
||||
if (option.type === ApplicationCommandOptionTypes.User) {
|
||||
const user = resolved?.users?.get(value);
|
||||
const member = resolved?.members?.get(value);
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [
|
||||
translateOptions?.[option.name] ?? option.name,
|
||||
{
|
||||
member: member!,
|
||||
user: user!,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// THE OPTION IS A MENTIONABLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Mentionable) {
|
||||
const role = resolved?.roles?.get(value);
|
||||
const user = resolved?.users?.get(value);
|
||||
const member = resolved?.members?.get(value);
|
||||
|
||||
const final = user && member ? { user, member } : role!;
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, final];
|
||||
}
|
||||
|
||||
// THE REST OF OPTIONS DON'T NEED ANY CONVERSION
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
// @ts-ignore ts leave me alone
|
||||
return [translateOptions?.[option.name] ?? option.name, option.value];
|
||||
}
|
||||
|
||||
/** Parse the options to a nice object.
|
||||
* NOTE: this does not work with subcommands
|
||||
*/
|
||||
export function optionParser(
|
||||
options?: InteractionDataOption[],
|
||||
resolved?: {
|
||||
/** The Ids and Message objects */
|
||||
messages?: Collection<bigint, Message>;
|
||||
/** The Ids and User objects */
|
||||
users?: Collection<bigint, User>;
|
||||
/** The Ids and partial Member objects */
|
||||
members?: Collection<bigint, Member>;
|
||||
/** The Ids and Role objects */
|
||||
roles?: Collection<bigint, Role>;
|
||||
/** The Ids and partial Channel objects */
|
||||
channels?: Collection<
|
||||
bigint,
|
||||
{
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
>;
|
||||
},
|
||||
translateOptions?: Record<string, string>,
|
||||
):
|
||||
| InteractionCommandArgs
|
||||
| { [key: string]: InteractionCommandArgs }
|
||||
| { [key: string]: { [key: string]: InteractionCommandArgs } } {
|
||||
// OPTIONS CAN BE UNDEFINED SO WE JUST RETURN AN EMPTY OBJECT
|
||||
if (!options) return {};
|
||||
|
||||
// A SUBCOMMAND WAS USED
|
||||
if (options[0].type === ApplicationCommandOptionTypes.SubCommand) {
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of options[0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(
|
||||
option,
|
||||
resolved,
|
||||
translateOptions,
|
||||
);
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-ignore ts leave me alone
|
||||
return {
|
||||
[translateOptions?.[options[0].name] ?? options[0].name]: convertedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// A SUBCOMMAND GROUP WAS USED
|
||||
if (options[0].type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
| Member
|
||||
| Role
|
||||
| Channel
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of options[0].options![0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(
|
||||
option,
|
||||
resolved,
|
||||
translateOptions,
|
||||
);
|
||||
// @ts-ignore ts leave me alone
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-ignore ts leave me alone
|
||||
return {
|
||||
[translateOptions?.[options[0].name] ?? options[0].name]: {
|
||||
[
|
||||
translateOptions?.[options[0].options![0].name] ??
|
||||
options[0].options![0].name
|
||||
]: convertedOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// A NORMAL COMMAND WAS USED
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
| Member
|
||||
| Role
|
||||
| Record<
|
||||
string,
|
||||
Pick<Channel, "id" | "name" | "type" | "permissions">
|
||||
>
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
> = {};
|
||||
for (const option of options ?? []) {
|
||||
const [name, value] = convertOptionValue(
|
||||
option,
|
||||
resolved,
|
||||
translateOptions,
|
||||
);
|
||||
// @ts-ignore ts leave me alone
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
return convertedOptions;
|
||||
}
|
||||
|
||||
/** The interaction arguments.
|
||||
* Important the members `deaf` and `mute` properties will always be false.
|
||||
*/
|
||||
export type InteractionCommandArgs = Record<
|
||||
string,
|
||||
| Member
|
||||
| Role
|
||||
| Record<
|
||||
string,
|
||||
Pick<Channel, "id" | "name" | "type" | "permissions">
|
||||
>
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
>;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Interaction, validatePermissions } from "../../deps.ts";
|
||||
import { Command } from "../bot/types/command.ts";
|
||||
|
||||
export default async function hasPermissionLevel(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
command: Command<any>,
|
||||
payload: Interaction,
|
||||
) {
|
||||
// This command doesn't require a perm level so allow the command.
|
||||
if (!command.permissionLevels) return true;
|
||||
|
||||
// If a custom function was provided
|
||||
if (typeof command.permissionLevels === "function") {
|
||||
return await command.permissionLevels(payload, command);
|
||||
}
|
||||
|
||||
// If an array of perm levels was provided
|
||||
for (const permLevel of command.permissionLevels) {
|
||||
// If this user has one of the allowed perm level, the loop is canceled and command is allowed.
|
||||
if (await PermissionLevelHandlers[permLevel](payload, command)) return true;
|
||||
}
|
||||
|
||||
// None of the perm levels were met. So cancel the command
|
||||
return false;
|
||||
}
|
||||
|
||||
export const PermissionLevelHandlers: Record<
|
||||
keyof typeof PermissionLevels,
|
||||
(
|
||||
payload: Interaction,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
command: Command<any>,
|
||||
) => boolean | Promise<boolean>
|
||||
> = {
|
||||
MEMBER: () => true,
|
||||
MODERATOR: (payload) =>
|
||||
Boolean(payload.member?.permissions) &&
|
||||
validatePermissions(payload.member!.permissions!, ["MANAGE_GUILD"]),
|
||||
ADMIN: (payload) =>
|
||||
Boolean(payload.member?.permissions) &&
|
||||
validatePermissions(payload.member!.permissions!, ["ADMINISTRATOR"]),
|
||||
SERVER_OWNER: () => false,
|
||||
BOT_SUPPORT: () => false,
|
||||
BOT_DEVS: () => false,
|
||||
BOT_OWNERS: () => false,
|
||||
};
|
||||
|
||||
export enum PermissionLevels {
|
||||
MEMBER,
|
||||
MODERATOR,
|
||||
ADMIN,
|
||||
SERVER_OWNER,
|
||||
BOT_SUPPORT,
|
||||
BOT_DEVS,
|
||||
BOT_OWNERS,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Bot, Interaction, InteractionApplicationCommandCallbackData, InteractionResponseTypes } from "../../deps.ts";
|
||||
|
||||
export async function replyToInteraction(
|
||||
bot: Bot,
|
||||
payload: Interaction,
|
||||
options:
|
||||
| string
|
||||
| (InteractionApplicationCommandCallbackData & {
|
||||
/** Type of the reply */
|
||||
type?: InteractionResponseTypes;
|
||||
}),
|
||||
) {
|
||||
if (typeof options === "string") options = { content: options };
|
||||
|
||||
return await bot.helpers.sendInteractionResponse(payload.id, payload.token, {
|
||||
type: options.type ?? InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function privateReplyToInteraction(
|
||||
bot: Bot,
|
||||
payload: Interaction,
|
||||
options:
|
||||
| string
|
||||
| (InteractionApplicationCommandCallbackData & {
|
||||
/** Type of the reply */
|
||||
type?: InteractionResponseTypes;
|
||||
}),
|
||||
) {
|
||||
if (typeof options === "string") options = { content: options };
|
||||
// SET PRIVATE
|
||||
options.flags = 64;
|
||||
|
||||
return await replyToInteraction(bot, payload, { ...options });
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Bot, Interaction } from "../../deps.ts";
|
||||
import Embeds from "./Embeds.ts";
|
||||
import { getUserTag } from "./helpers.ts";
|
||||
import logger from "./logger.ts";
|
||||
|
||||
export async function slashLogWebhook(
|
||||
bot: Bot,
|
||||
payload: Interaction,
|
||||
name: string,
|
||||
) {
|
||||
const webhook = Deno.env.get("DISCORD_LOGS_WEBHOOK");
|
||||
if (!webhook) return;
|
||||
|
||||
const [id, token] = webhook.substring(webhook.indexOf("webhooks/") + 9).split(
|
||||
"/",
|
||||
);
|
||||
|
||||
const embeds = new Embeds(bot)
|
||||
.setAuthor(`${getUserTag(payload.user)} used ${name}`, payload.user)
|
||||
.addField(
|
||||
"Channel",
|
||||
payload.channelId?.toString() || "Channel ID unavailable",
|
||||
true,
|
||||
)
|
||||
.addField(
|
||||
"Guild",
|
||||
payload.guildId?.toString() || "Guild ID unavailable",
|
||||
true,
|
||||
);
|
||||
|
||||
await bot.helpers
|
||||
.sendWebhook(bot.transformers.snowflake(id), token, {
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(logger.error);
|
||||
}
|
||||
|
||||
export default slashLogWebhook;
|
||||
@@ -1,165 +0,0 @@
|
||||
import { DEV_GUILD_ID } from "../../configs.ts";
|
||||
import { ApplicationCommandOption, ApplicationCommandTypes, Bot } from "../../deps.ts";
|
||||
import { BotClient } from "../bot/botClient.ts";
|
||||
import { updateCommandVersion } from "../bot/database/commandVersion.ts";
|
||||
import commands from "../bot/events/interactions/mod.ts";
|
||||
import { serverLanguages, translate } from "../bot/languages/translate.ts";
|
||||
import { ArgumentDefinition } from "../bot/types/command.ts";
|
||||
|
||||
export async function updateDevCommands(bot: Bot) {
|
||||
if (!DEV_GUILD_ID) return;
|
||||
|
||||
const cmds = Object.entries(commands)
|
||||
// ONLY DEV COMMANDS
|
||||
.filter(([_name, command]) => command.dev);
|
||||
|
||||
if (!cmds.length) return;
|
||||
|
||||
// DEV RELATED COMMANDS
|
||||
await bot.helpers.upsertApplicationCommands(
|
||||
cmds.map(([name, command]) => {
|
||||
const translatedName = translate(bot, DEV_GUILD_ID, command.name);
|
||||
const translatedDescription = command.description ? translate(bot, DEV_GUILD_ID, command.description) : "";
|
||||
|
||||
if (command.type && command.type !== ApplicationCommandTypes.ChatInput) {
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
type: command.type,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
description: translatedDescription || command!.description,
|
||||
options: command.options ? createOptions(bot, DEV_GUILD_ID, command.options, command.name) : undefined,
|
||||
};
|
||||
}),
|
||||
DEV_GUILD_ID,
|
||||
);
|
||||
}
|
||||
|
||||
// USED TO CACHE CONVERTED COMMANDS AFTER START TO PREVENT UNNECESSARY LOOPS
|
||||
const convertedCache = new Map<string, ApplicationCommandOption[]>();
|
||||
|
||||
/** Creates the commands options including subcommands. Also translates them. */
|
||||
function createOptions(
|
||||
bot: Bot,
|
||||
guildId: bigint | "english",
|
||||
options: ArgumentDefinition[],
|
||||
commandName?: string,
|
||||
): ApplicationCommandOption[] | undefined {
|
||||
const language = guildId === "english" ? "english" : serverLanguages.get(guildId) ?? "english";
|
||||
if (commandName && convertedCache.has(`${language}-${commandName}`)) {
|
||||
return convertedCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
const newOptions: ApplicationCommandOption[] = [];
|
||||
|
||||
for (const option of options || []) {
|
||||
const optionName = translate(bot, guildId, option.name);
|
||||
const optionDescription = translate(bot, guildId, option.description);
|
||||
|
||||
// TODO: remove this ts ignore
|
||||
// @ts-ignore ts stop being dumb
|
||||
const choices = option.choices?.map((choice) => ({
|
||||
...choice,
|
||||
name: translate(bot, guildId, choice.name),
|
||||
}));
|
||||
|
||||
newOptions.push({
|
||||
...option,
|
||||
name: optionName.toLowerCase(),
|
||||
description: optionDescription || "No description available.",
|
||||
choices,
|
||||
// @ts-ignore fix this
|
||||
options: option.options
|
||||
? // @ts-ignore fix this
|
||||
createOptions(bot, guildId, option.options)
|
||||
: undefined,
|
||||
} as ApplicationCommandOption);
|
||||
}
|
||||
if (commandName) convertedCache.set(`${language}-${commandName}`, newOptions);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
export async function updateGlobalCommands(bot: Bot) {
|
||||
// UPDATE GLOBAL COMMANDS
|
||||
await bot.helpers.upsertApplicationCommands(
|
||||
Object.entries(commands)
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter(([_name, command]) => command?.global && !command.dev)
|
||||
.map(([name, command]) => {
|
||||
return {
|
||||
name,
|
||||
description: translate(bot, "english", command.description),
|
||||
options: createOptions(bot, "english", command.options, command.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateGuildCommands(bot: BotClient, guildId: bigint) {
|
||||
if (guildId === DEV_GUILD_ID) return await updateDevCommands(bot);
|
||||
|
||||
await updateCommandVersion(bot, guildId);
|
||||
|
||||
Object.entries(commands)
|
||||
// ONLY GUILD COMMANDS
|
||||
.filter(([_name, command]) => !command.global && !command.dev)
|
||||
.map(([name, command]) => {
|
||||
// USER OPTED TO USE BASIC VERSION ONLY
|
||||
if (command.advanced === false) {
|
||||
return {
|
||||
name,
|
||||
description: translate(bot, "english", command.description),
|
||||
options: command.options,
|
||||
};
|
||||
}
|
||||
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(bot, guildId, command.name);
|
||||
const translatedDescription = translate(
|
||||
bot,
|
||||
guildId,
|
||||
command.description,
|
||||
);
|
||||
|
||||
return {
|
||||
name: translatedName.toLowerCase(),
|
||||
description: translatedDescription,
|
||||
options: createOptions(bot, guildId, command.options, command.name),
|
||||
};
|
||||
}),
|
||||
// GUILD RELATED COMMANDS
|
||||
await bot.helpers.upsertApplicationCommands(
|
||||
Object.entries(commands)
|
||||
// ONLY GUILD COMMANDS
|
||||
.filter(([_name, command]) => !command.global && !command.dev)
|
||||
.map(([name, command]) => {
|
||||
// USER OPTED TO USE BASIC VERSION ONLY
|
||||
if (command.advanced === false) {
|
||||
return {
|
||||
name,
|
||||
description: command!.description || "No description available.",
|
||||
options: command!.options,
|
||||
};
|
||||
}
|
||||
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(bot, guildId, command.name);
|
||||
const translatedDescription = translate(
|
||||
bot,
|
||||
guildId,
|
||||
command.description,
|
||||
);
|
||||
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
description: translatedDescription || command!.description,
|
||||
options: createOptions(bot, guildId, command.options, command.name),
|
||||
};
|
||||
}),
|
||||
guildId,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user