fix: remove old files

This commit is contained in:
Skillz4Killz
2022-08-25 20:14:57 +00:00
committed by GitHub
parent 61614babba
commit 84d3ca2575
42 changed files with 0 additions and 2868 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
{
"imports": {
"/": "./",
"./": "./",
"@deps": "./deps.ts",
"@configs": "./configs.ts"
}
}

View File

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

View File

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

View File

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

View File

@@ -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!");

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
import { ArgumentDefinition, Command } from "../../../types/command.ts";
export function createCommand<T extends readonly ArgumentDefinition[]>(
command: Command<T>,
) {
return command;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
export * from "./languages.ts";
export * from "./translate.ts";

View File

@@ -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]>
: [];

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
export const SNOWFLAKE_REGEX = /[0-9]{17,19}/;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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