mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 08:47:22 +00:00
cleanup cleanup cleanup on isle dd (#2792)
* cleanup cleanup cleanup on isle dd * fix: rest manager import in test
This commit is contained in:
+12
-14
@@ -1,7 +1,5 @@
|
||||
import { configs } from './configs.ts.js';
|
||||
import type {
|
||||
BotWithCache,
|
||||
BotWithHelpersPlugin} from './deps.ts.js';
|
||||
import { configs } from './configs.ts.js'
|
||||
import type { BotWithCache, BotWithHelpersPlugin } from './deps.ts.js'
|
||||
import {
|
||||
Collection,
|
||||
createBot,
|
||||
@@ -10,8 +8,8 @@ import {
|
||||
enableHelpersPlugin,
|
||||
enablePermissionsPlugin,
|
||||
GatewayIntents,
|
||||
} from './deps.ts.js';
|
||||
import type { Command } from './src/types/commands.ts.js';
|
||||
} from './deps.ts.js'
|
||||
import type { Command } from './src/types/commands.ts.js'
|
||||
|
||||
// MAKE THE BASIC BOT OBJECT
|
||||
const bot = createBot({
|
||||
@@ -19,19 +17,19 @@ const bot = createBot({
|
||||
botId: configs.botId,
|
||||
intents: GatewayIntents.Guilds,
|
||||
events: {},
|
||||
});
|
||||
})
|
||||
|
||||
// ENABLE ALL THE PLUGINS THAT WILL HELP MAKE IT EASIER TO CODE YOUR BOT
|
||||
enableHelpersPlugin(bot);
|
||||
enableCachePlugin(bot);
|
||||
enableCacheSweepers(bot as BotWithCache);
|
||||
enablePermissionsPlugin(bot as BotWithCache);
|
||||
enableHelpersPlugin(bot)
|
||||
enableCachePlugin(bot)
|
||||
enableCacheSweepers(bot as BotWithCache)
|
||||
enablePermissionsPlugin(bot as BotWithCache)
|
||||
|
||||
export interface BotClient extends BotWithCache<BotWithHelpersPlugin> {
|
||||
commands: Collection<string, Command>;
|
||||
commands: Collection<string, Command>
|
||||
}
|
||||
|
||||
// THIS IS THE BOT YOU WANT TO USE EVERYWHERE IN YOUR CODE! IT HAS EVERYTHING BUILT INTO IT!
|
||||
export const Bot = bot as BotClient;
|
||||
export const Bot = bot as BotClient
|
||||
// PREPARE COMMANDS HOLDER
|
||||
Bot.commands = new Collection();
|
||||
Bot.commands = new Collection()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { dotEnvConfig } from './deps.ts.js';
|
||||
import { dotEnvConfig } from './deps.ts.js'
|
||||
|
||||
// Get the .env file that the user should have created, and get the token
|
||||
const env = dotEnvConfig({ export: true, path: "./.env" });
|
||||
const token = env.BOT_TOKEN || "";
|
||||
const env = dotEnvConfig({ export: true, path: './.env' })
|
||||
const token = env.BOT_TOKEN || ''
|
||||
|
||||
export interface Config {
|
||||
token: string;
|
||||
botId: bigint;
|
||||
token: string
|
||||
botId: bigint
|
||||
}
|
||||
|
||||
export const configs = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
/** Get the BotId from the token */
|
||||
botId: BigInt(atob(token.split(".")[0])),
|
||||
botId: BigInt(atob(token.split('.')[0])),
|
||||
/** The server id where you develop your bot and want dev commands created. */
|
||||
devGuildId: BigInt(env.DEV_GUILD_ID!),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/mod.ts";
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/plugins/mod.ts";
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/mod.ts'
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/plugins/mod.ts'
|
||||
|
||||
// Terminal Colors!
|
||||
export * from "https://deno.land/std@0.117.0/fmt/colors.ts";
|
||||
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 { config as dotEnvConfig } from 'https://deno.land/x/dotenv@v3.1.0/mod.ts'
|
||||
// Database, thx Tri!
|
||||
export { decode as KwikDecode, encode as KwikEncode, Kwik } from "https://deno.land/x/kwik@v1.3.1/mod.ts";
|
||||
export { decode as KwikDecode, encode as KwikEncode, Kwik } from 'https://deno.land/x/kwik@v1.3.1/mod.ts'
|
||||
|
||||
+13
-13
@@ -1,25 +1,25 @@
|
||||
import { startBot } from './deps.ts.js';
|
||||
import log from './src/utils/logger.ts.js';
|
||||
import { fileLoader, importDirectory } from './src/utils/loader.ts.js';
|
||||
import { updateApplicationCommands } from './src/utils/updateCommands.ts.js';
|
||||
import { startBot } from './deps.ts.js'
|
||||
import log from './src/utils/logger.ts.js'
|
||||
import { fileLoader, importDirectory } from './src/utils/loader.ts.js'
|
||||
import { updateApplicationCommands } from './src/utils/updateCommands.ts.js'
|
||||
// setup db
|
||||
import './src/database/mod.ts.js';
|
||||
import { Bot } from './bot.ts.js';
|
||||
import './src/database/mod.ts.js'
|
||||
import { Bot } from './bot.ts.js'
|
||||
|
||||
log.info("Starting bot...");
|
||||
log.info('Starting bot...')
|
||||
|
||||
// Forces deno to read all the files which will fill the commands/inhibitors cache etc.
|
||||
await Promise.all(
|
||||
[
|
||||
"./src/commands",
|
||||
"./src/events",
|
||||
'./src/commands',
|
||||
'./src/events',
|
||||
// "./src/tasks",
|
||||
].map((path) => importDirectory(Deno.realPathSync(path))),
|
||||
);
|
||||
await fileLoader();
|
||||
)
|
||||
await fileLoader()
|
||||
|
||||
// UPDATES YOUR COMMANDS TO LATEST COMMANDS
|
||||
await updateApplicationCommands();
|
||||
await updateApplicationCommands()
|
||||
|
||||
// STARTS THE CONNECTION TO DISCORD
|
||||
await startBot(Bot);
|
||||
await startBot(Bot)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import type { Command } from '../types/commands.ts.js';
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import type { Command } from '../types/commands.ts.js'
|
||||
|
||||
export function createCommand(command: Command) {
|
||||
Bot.commands.set(command.name, command);
|
||||
Bot.commands.set(command.name, command)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js';
|
||||
import { snowflakeToTimestamp } from '../utils/helpers.ts.js';
|
||||
import { createCommand } from './mod.ts.js';
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js'
|
||||
import { snowflakeToTimestamp } from '../utils/helpers.ts.js'
|
||||
import { createCommand } from './mod.ts.js'
|
||||
|
||||
createCommand({
|
||||
name: "ping",
|
||||
description: "Ping the Bot!",
|
||||
name: 'ping',
|
||||
description: 'Ping the Bot!',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
execute: async (Bot, interaction) => {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
await Bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! ${ping}ms`,
|
||||
},
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
await Bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! ${ping}ms`,
|
||||
},
|
||||
);
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { Kwik, KwikDecode, KwikEncode } from '../../deps.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
import { Kwik, KwikDecode, KwikEncode } from '../../deps.ts.js'
|
||||
import { logger } from '../utils/logger.ts.js'
|
||||
|
||||
const log = logger({ name: "DB Manager" });
|
||||
const log = logger({ name: 'DB Manager' })
|
||||
|
||||
log.info("Initializing Database");
|
||||
log.info('Initializing Database')
|
||||
|
||||
const kwik = new Kwik();
|
||||
const kwik = new Kwik()
|
||||
|
||||
// Add BigInt Support
|
||||
kwik.msgpackExtensionCodec.register({
|
||||
type: 0,
|
||||
encode: (object: unknown): Uint8Array | null => {
|
||||
if (typeof object === "bigint") {
|
||||
if (typeof object === 'bigint') {
|
||||
if (object <= Number.MAX_SAFE_INTEGER && object >= Number.MIN_SAFE_INTEGER) {
|
||||
return KwikEncode(parseInt(object.toString(), 10), {});
|
||||
return KwikEncode(parseInt(object.toString(), 10), {})
|
||||
} else {
|
||||
return KwikEncode(object.toString(), {});
|
||||
return KwikEncode(object.toString(), {})
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
},
|
||||
decode: (data: Uint8Array) => {
|
||||
return BigInt(KwikDecode(data, {}) as string);
|
||||
return BigInt(KwikDecode(data, {}) as string)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Initialize the Database
|
||||
await kwik.init();
|
||||
await kwik.init()
|
||||
|
||||
log.info("Database Initialized!");
|
||||
log.info('Database Initialized!')
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import { InteractionTypes } from '../../deps.ts.js';
|
||||
import log from '../utils/logger.ts.js';
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import { InteractionTypes } from '../../deps.ts.js'
|
||||
import log from '../utils/logger.ts.js'
|
||||
|
||||
Bot.events.interactionCreate = (_, interaction) => {
|
||||
if (!interaction.data) return;
|
||||
if (!interaction.data) return
|
||||
|
||||
switch (interaction.type) {
|
||||
case InteractionTypes.ApplicationCommand:
|
||||
log.info(
|
||||
`[Application Command] ${interaction.data.name} command executed.`,
|
||||
);
|
||||
Bot.commands.get(interaction.data.name!)?.execute(Bot, interaction);
|
||||
break;
|
||||
log.info(`[Application Command] ${interaction.data.name} command executed.`)
|
||||
Bot.commands.get(interaction.data.name!)?.execute(Bot, interaction)
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import log from '../utils/logger.ts.js';
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import log from '../utils/logger.ts.js'
|
||||
|
||||
Bot.events.ready = (_, payload) => {
|
||||
log.info(`[READY] Shard ID ${payload.shardId} of ${Bot.gateway.lastShardId + 1} shards is ready!`);
|
||||
log.info(`[READY] Shard ID ${payload.shardId} of ${Bot.gateway.lastShardId + 1} shards is ready!`)
|
||||
|
||||
if (payload.shardId === Bot.gateway.lastShardId) {
|
||||
botFullyReady();
|
||||
botFullyReady()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// This function lets you run custom code when all your bot's shards are online.
|
||||
function botFullyReady() {
|
||||
// DO STUFF YOU WANT HERE ONCE BOT IS FULLY ONLINE.
|
||||
log.info("[READY] Bot is fully online.");
|
||||
log.info('[READY] Bot is fully online.')
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { BotClient } from '../../bot.ts.js';
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Interaction } from '../../deps.ts.js';
|
||||
import type { BotClient } from '../../bot.ts.js'
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Interaction } from '../../deps.ts.js'
|
||||
|
||||
export interface Command {
|
||||
/** The name of this command. */
|
||||
name: string;
|
||||
name: string
|
||||
/** What does this command do? */
|
||||
description: string;
|
||||
description: string
|
||||
/** The type of command this is. */
|
||||
type: ApplicationCommandTypes;
|
||||
type: ApplicationCommandTypes
|
||||
/** Whether or not this command is for the dev server only. */
|
||||
devOnly?: boolean;
|
||||
devOnly?: boolean
|
||||
/** The options for this command */
|
||||
options?: ApplicationCommandOption[];
|
||||
options?: ApplicationCommandOption[]
|
||||
/** This will be executed when the command is run. */
|
||||
execute: (bot: BotClient, interaction: Interaction) => unknown;
|
||||
execute: (bot: BotClient, interaction: Interaction) => unknown
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file will export all of the types in this directory.
|
||||
|
||||
export * from './commands.ts.js';
|
||||
export * from './commands.ts.js'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
return Number(id / 4194304n + 1420070400000n)
|
||||
}
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
import log from './logger.ts.js';
|
||||
import log from './logger.ts.js'
|
||||
|
||||
// Very important to make sure files are reloaded properly
|
||||
let uniqueFilePathCounter = 0;
|
||||
let paths: string[] = [];
|
||||
let uniqueFilePathCounter = 0
|
||||
let paths: string[] = []
|
||||
|
||||
/** This function allows reading all files in a folder. Useful for loading/reloading commands, monitors etc */
|
||||
export async function importDirectory(path: string) {
|
||||
path = path.replaceAll("\\", "/");
|
||||
const files = Deno.readDirSync(Deno.realPathSync(path));
|
||||
const folder = path.substring(path.indexOf("/src/") + 5);
|
||||
path = path.replaceAll('\\', '/')
|
||||
const files = Deno.readDirSync(Deno.realPathSync(path))
|
||||
const folder = path.substring(path.indexOf('/src/') + 5)
|
||||
|
||||
if (!folder.includes("/")) log.info(`Loading ${folder}...`);
|
||||
if (!folder.includes('/')) log.info(`Loading ${folder}...`)
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.name) continue;
|
||||
if (!file.name) continue
|
||||
|
||||
const currentPath = `${path}/${file.name}`;
|
||||
const currentPath = `${path}/${file.name}`
|
||||
if (file.isFile) {
|
||||
if (!currentPath.endsWith(".ts")) continue;
|
||||
if (!currentPath.endsWith('.ts')) continue
|
||||
paths.push(
|
||||
`import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/${
|
||||
currentPath.substring(
|
||||
currentPath.indexOf("src/"),
|
||||
)
|
||||
}#${uniqueFilePathCounter}";`,
|
||||
);
|
||||
continue;
|
||||
`import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf('/'))}/${currentPath.substring(
|
||||
currentPath.indexOf('src/'),
|
||||
)}#${uniqueFilePathCounter}";`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
await importDirectory(currentPath);
|
||||
await importDirectory(currentPath)
|
||||
}
|
||||
|
||||
uniqueFilePathCounter++;
|
||||
uniqueFilePathCounter++
|
||||
}
|
||||
|
||||
/** Imports all everything in fileloader.ts */
|
||||
export async function fileLoader() {
|
||||
await Deno.writeTextFile(
|
||||
"fileloader.ts",
|
||||
paths.join("\n").replaceAll("\\", "/"),
|
||||
);
|
||||
await import(
|
||||
`${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/fileloader.ts#${uniqueFilePathCounter}`
|
||||
);
|
||||
paths = [];
|
||||
await Deno.writeTextFile('fileloader.ts', paths.join('\n').replaceAll('\\', '/'))
|
||||
await import(`${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf('/'))}/fileloader.ts#${uniqueFilePathCounter}`)
|
||||
paths = []
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js';
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js'
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
@@ -10,81 +10,81 @@ export enum LogLevels {
|
||||
}
|
||||
|
||||
const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Debug, "DEBUG"],
|
||||
[LogLevels.Info, "INFO"],
|
||||
[LogLevels.Warn, "WARN"],
|
||||
[LogLevels.Error, "ERROR"],
|
||||
[LogLevels.Fatal, "FATAL"],
|
||||
]);
|
||||
[LogLevels.Debug, 'DEBUG'],
|
||||
[LogLevels.Info, 'INFO'],
|
||||
[LogLevels.Warn, 'WARN'],
|
||||
[LogLevels.Error, 'ERROR'],
|
||||
[LogLevels.Fatal, 'FATAL'],
|
||||
])
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
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;
|
||||
logLevel?: LogLevels
|
||||
name?: string
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
if (level < logLevel) return;
|
||||
if (level < logLevel) return
|
||||
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
let color = colorFunctions.get(level)
|
||||
if (!color) color = noColor
|
||||
|
||||
const date = new Date();
|
||||
const date = new Date()
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || "DEBUG"),
|
||||
name ? `${name} >` : ">",
|
||||
color(prefixes.get(level) || 'DEBUG'),
|
||||
name ? `${name} >` : '>',
|
||||
...args,
|
||||
];
|
||||
]
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log);
|
||||
return console.debug(...log)
|
||||
case LogLevels.Info:
|
||||
return console.info(...log);
|
||||
return console.info(...log)
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log);
|
||||
return console.warn(...log)
|
||||
case LogLevels.Error:
|
||||
return console.error(...log);
|
||||
return console.error(...log)
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log);
|
||||
return console.error(...log)
|
||||
default:
|
||||
return console.log(...log);
|
||||
return console.log(...log)
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
logLevel = level;
|
||||
logLevel = level
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
log(LogLevels.Debug, ...args);
|
||||
log(LogLevels.Debug, ...args)
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
log(LogLevels.Info, ...args);
|
||||
log(LogLevels.Info, ...args)
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
log(LogLevels.Warn, ...args);
|
||||
log(LogLevels.Warn, ...args)
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
log(LogLevels.Error, ...args);
|
||||
log(LogLevels.Error, ...args)
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
log(LogLevels.Fatal, ...args);
|
||||
log(LogLevels.Fatal, ...args)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -95,8 +95,8 @@ export function logger({
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const log = logger({ name: "Main" });
|
||||
export default log;
|
||||
export const log = logger({ name: 'Main' })
|
||||
export default log
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import { configs } from '../../configs.ts.js';
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import { configs } from '../../configs.ts.js'
|
||||
|
||||
export async function updateApplicationCommands() {
|
||||
await Bot.helpers.upsertGlobalApplicationCommands(
|
||||
@@ -7,7 +7,7 @@ export async function updateApplicationCommands() {
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !command.devOnly)
|
||||
.array(),
|
||||
);
|
||||
)
|
||||
|
||||
await Bot.helpers.upsertGuildApplicationCommands(
|
||||
configs.devGuildId,
|
||||
@@ -15,5 +15,5 @@ export async function updateApplicationCommands() {
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !!command.devOnly)
|
||||
.array(),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InfluxDB, Point } from "@influxdata/influxdb-client";
|
||||
import type { RestManager } from "discordeno/rest";
|
||||
import { InfluxDB, Point } from '@influxdata/influxdb-client';
|
||||
import type { RestManager } from 'discordeno/rest';
|
||||
|
||||
const INFLUX_ORG = process.env.INFLUX_ORG as string;
|
||||
const INFLUX_BUCKET = process.env.INFLUX_BUCKET as string;
|
||||
@@ -10,46 +10,46 @@ export const influxDB = INFLUX_URL && INFLUX_TOKEN ? new InfluxDB({ url: INFLUX_
|
||||
export const Influx = influxDB?.getWriteApi(INFLUX_ORG, INFLUX_BUCKET);
|
||||
|
||||
export const setupAnalyticsHooks = (rest: RestManager) => {
|
||||
// If influxdb data is provided, enable analytics in this proxy.
|
||||
if (Influx) {
|
||||
rest.fetching = function (options) {
|
||||
Influx?.writePoint(
|
||||
new Point("restEvents")
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField("type", "REQUEST_FETCHING")
|
||||
.tag("method", options.method)
|
||||
.tag("url", options.url)
|
||||
.tag("bucket", options.bucketId ?? "NA"),
|
||||
);
|
||||
};
|
||||
// If influxdb data is provided, enable analytics in this proxy.
|
||||
if (Influx) {
|
||||
rest.fetching = function (options) {
|
||||
Influx?.writePoint(
|
||||
new Point('restEvents')
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField('type', 'REQUEST_FETCHING')
|
||||
.tag('method', options.method)
|
||||
.tag('url', options.url)
|
||||
.tag('bucket', options.bucketId ?? 'NA'),
|
||||
);
|
||||
};
|
||||
|
||||
rest.fetched = function (options, response) {
|
||||
Influx?.writePoint(
|
||||
new Point("restEvents")
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField("type", "REQUEST_FETCHED")
|
||||
.tag("method", options.method)
|
||||
.tag("url", options.url)
|
||||
.tag("bucket", options.bucketId ?? "NA")
|
||||
.intField("status", response.status)
|
||||
.tag("statusText", response.statusText),
|
||||
);
|
||||
};
|
||||
rest.fetched = function (options, response) {
|
||||
Influx?.writePoint(
|
||||
new Point('restEvents')
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField('type', 'REQUEST_FETCHED')
|
||||
.tag('method', options.method)
|
||||
.tag('url', options.url)
|
||||
.tag('bucket', options.bucketId ?? 'NA')
|
||||
.intField('status', response.status)
|
||||
.tag('statusText', response.statusText),
|
||||
);
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
console.log(`[Influx - REST] Saving events...`);
|
||||
Influx?.flush()
|
||||
.then(() => {
|
||||
console.log(`[Influx - REST] Saved events!`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`[Influx - REST] Error saving events!`, error);
|
||||
});
|
||||
// Every 30seconds
|
||||
}, 30000);
|
||||
}
|
||||
setInterval(() => {
|
||||
console.log(`[Influx - REST] Saving events...`);
|
||||
Influx?.flush()
|
||||
.then(() => {
|
||||
console.log(`[Influx - REST] Saved events!`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`[Influx - REST] Error saving events!`, error);
|
||||
});
|
||||
// Every 30seconds
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import type { Bot} from "discordeno";
|
||||
import { Collection, createBot, createRestManager } from "discordeno";
|
||||
import enableHelpersPlugin from "discordeno/helpers-plugin";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import { setupAnalyticsHooks } from "../analytics.js";
|
||||
import { INTENTS, REST_URL } from "../configs.js";
|
||||
import { setupEventHandlers } from "./events/mod.js";
|
||||
import type { MessageCollector } from "./utils/collectors.js";
|
||||
import { customizeInternals } from "./utils/internals/mod.js";
|
||||
import type { Bot } from 'discordeno';
|
||||
import { Collection, createBot, createRestManager } from 'discordeno';
|
||||
import enableHelpersPlugin from 'discordeno/helpers-plugin';
|
||||
import { createLogger } from 'discordeno/logger';
|
||||
import { setupAnalyticsHooks } from '../analytics.js';
|
||||
import { INTENTS, REST_URL } from '../configs.js';
|
||||
import { setupEventHandlers } from './events/mod.js';
|
||||
import type { MessageCollector } from './utils/collectors.js';
|
||||
import { customizeInternals } from './utils/internals/mod.js';
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
|
||||
export const bot = enableHelpersPlugin(
|
||||
customizeBot(
|
||||
createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
}),
|
||||
),
|
||||
customizeBot(
|
||||
createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/** Add custom props to your `bot` here */
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the type below. As an example, i have added a `logger` property. You can add any useful methods or props you wish to have easily available.
|
||||
function customizeBot<B extends Bot = Bot>(bot: B): BotWithCustomProps {
|
||||
const customized = bot as unknown as BotWithCustomProps;
|
||||
customized.logger = createLogger({ name: "[Bot]" });
|
||||
customized.collectors = {
|
||||
messages: new Collection(),
|
||||
};
|
||||
customized.commandVersions = new Collection();
|
||||
const customized = bot as unknown as BotWithCustomProps;
|
||||
customized.logger = createLogger({ name: '[Bot]' });
|
||||
customized.collectors = {
|
||||
messages: new Collection(),
|
||||
};
|
||||
customized.commandVersions = new Collection();
|
||||
|
||||
return customized;
|
||||
return customized;
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the function above. Run a find all and change this to your Bot's name. For example, if your bot's name is Gamer change BotWithCustomProps to Gamer. This way whenever you need to provide the type for the Bot with your custom props it is your bots name.
|
||||
// Note: ALWAYS edit the function above first before adding the type here.
|
||||
export type BotWithCustomProps<B extends Bot = Bot> = B & {
|
||||
/** A easy to use logger to make clean log messages. */
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
/** Collectors that can be used to get input from users. */
|
||||
collectors: {
|
||||
/** Holds the pending messages collectors that users can respond to. */
|
||||
messages: Collection<bigint, MessageCollector>;
|
||||
};
|
||||
/** The command versions for each guild id. */
|
||||
commandVersions: Collection<bigint, number>;
|
||||
/** A easy to use logger to make clean log messages. */
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
/** Collectors that can be used to get input from users. */
|
||||
collectors: {
|
||||
/** Holds the pending messages collectors that users can respond to. */
|
||||
messages: Collection<bigint, MessageCollector>;
|
||||
};
|
||||
/** The command versions for each guild id. */
|
||||
commandVersions: Collection<bigint, number>;
|
||||
};
|
||||
|
||||
// Example of how to customize internal discordeno stuff easily.
|
||||
@@ -54,9 +54,9 @@ customizeInternals(bot);
|
||||
setupEventHandlers();
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
});
|
||||
|
||||
// Add send fetching analytics hook to rest
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ApplicationCommandOptionTypes } from "discordeno";
|
||||
import { prisma } from "../../prisma.js";
|
||||
import languages from "../languages/languages.js";
|
||||
import { serverLanguages, translate } from "../languages/translate.js";
|
||||
import { createCommand } from "../utils/slash/createCommand.js";
|
||||
import { ApplicationCommandOptionTypes } from 'discordeno';
|
||||
import { prisma } from '../../prisma.js';
|
||||
import languages from '../languages/languages.js';
|
||||
import { serverLanguages, translate } from '../languages/translate.js';
|
||||
import { createCommand } from '../utils/slash/createCommand.js';
|
||||
|
||||
export default createCommand({
|
||||
name: "LANGUAGE_NAME",
|
||||
description: "LANGUAGE_DESCRIPTION",
|
||||
options: [
|
||||
{
|
||||
name: "LANGUAGE_KEY_NAME",
|
||||
description: "LANGUAGE_KEY_DESCRIPTION",
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
choices: Object.keys(languages).map((key) => ({ name: key, value: key })),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
execute: async function (_, interaction, args) {
|
||||
if (!interaction.guildId) return;
|
||||
name: 'LANGUAGE_NAME',
|
||||
description: 'LANGUAGE_DESCRIPTION',
|
||||
options: [
|
||||
{
|
||||
name: 'LANGUAGE_KEY_NAME',
|
||||
description: 'LANGUAGE_KEY_DESCRIPTION',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
choices: Object.keys(languages).map((key) => ({ name: key, value: key })),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
execute: async function (_, interaction, args) {
|
||||
if (!interaction.guildId) return;
|
||||
|
||||
// Set the new language in cache
|
||||
serverLanguages.set(interaction.guildId, args.name);
|
||||
// Let the user know its been updated.
|
||||
await interaction.reply(translate(interaction.guildId!, "LANGUAGE_UPDATED", args.name));
|
||||
// Update the db
|
||||
return await prisma.guilds.upsert({
|
||||
where: { id: interaction.guildId },
|
||||
create: { language: args.name, id: interaction.guildId },
|
||||
update: { language: args.name },
|
||||
});
|
||||
},
|
||||
// Set the new language in cache
|
||||
serverLanguages.set(interaction.guildId, args.name);
|
||||
// Let the user know its been updated.
|
||||
await interaction.reply(translate(interaction.guildId!, 'LANGUAGE_UPDATED', args.name));
|
||||
// Update the db
|
||||
return await prisma.guilds.upsert({
|
||||
where: { id: interaction.guildId },
|
||||
create: { language: args.name, id: interaction.guildId },
|
||||
update: { language: args.name },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import language from "./language.js";
|
||||
import ping from "./ping.js";
|
||||
import language from './language.js';
|
||||
import ping from './ping.js';
|
||||
|
||||
export const COMMANDS = {
|
||||
language,
|
||||
ping,
|
||||
language,
|
||||
ping,
|
||||
};
|
||||
|
||||
export default COMMANDS;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { translate } from "../languages/translate.js";
|
||||
import { createCommand } from "../utils/slash/createCommand.js";
|
||||
import { translate } from '../languages/translate.js';
|
||||
import { createCommand } from '../utils/slash/createCommand.js';
|
||||
|
||||
export default createCommand({
|
||||
name: "PING_NAME",
|
||||
description: "PING_DESCRIPTION",
|
||||
execute: async function (_, interaction) {
|
||||
return await interaction.reply(
|
||||
translate(interaction.guildId!, "PING_RESPONSE_WITH_TIME", Date.now() - snowflakeToTimestamp(interaction.id)),
|
||||
);
|
||||
},
|
||||
name: 'PING_NAME',
|
||||
description: 'PING_DESCRIPTION',
|
||||
execute: async function (_, interaction) {
|
||||
return await interaction.reply(
|
||||
translate(interaction.guildId!, 'PING_RESPONSE_WITH_TIME', Date.now() - snowflakeToTimestamp(interaction.id)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: This should be deleted once this is available in the helpers plugin.
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import type { Interaction } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
|
||||
export async function executeButtonClick(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Button] The ${interaction.data.customId} button was clicked in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
bot.logger.info(
|
||||
`[Button] The ${interaction.data.customId} button was clicked in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
|
||||
@@ -1,121 +1,116 @@
|
||||
import { bgBlack, bgGreen, bgMagenta, bgYellow, black, green, red, white } from "colorette";
|
||||
import { bgBlack, bgGreen, bgMagenta, bgYellow, black, green, red, white } from 'colorette';
|
||||
import type {
|
||||
ApplicationCommandOption,
|
||||
Bot,
|
||||
Channel,
|
||||
ChannelTypes,
|
||||
Interaction,
|
||||
InteractionDataOption,
|
||||
Member,
|
||||
Role,
|
||||
User} from "discordeno";
|
||||
import {
|
||||
ApplicationCommandOptionTypes,
|
||||
InteractionResponseTypes
|
||||
} from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import { bot } from "../../bot.js";
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
import type { translationKeys } from "../../languages/translate.js";
|
||||
import { getLanguage, loadLanguage, serverLanguages, translate } from "../../languages/translate.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import type { Command, ConvertArgumentDefinitionsToArgs } from "../../utils/slash/createCommand.js";
|
||||
ApplicationCommandOption,
|
||||
Bot,
|
||||
Channel,
|
||||
ChannelTypes,
|
||||
Interaction,
|
||||
InteractionDataOption,
|
||||
Member,
|
||||
Role,
|
||||
User,
|
||||
} from 'discordeno';
|
||||
import { ApplicationCommandOptionTypes, InteractionResponseTypes } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
import { bot } from '../../bot.js';
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
import type { translationKeys } from '../../languages/translate.js';
|
||||
import { getLanguage, loadLanguage, serverLanguages, translate } from '../../languages/translate.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import type { Command, ConvertArgumentDefinitionsToArgs } from '../../utils/slash/createCommand.js';
|
||||
|
||||
function logCommand(
|
||||
info: Interaction,
|
||||
type: "Failure" | "Success" | "Trigger" | "Slowmode" | "Missing" | "Inhibit",
|
||||
commandName: string,
|
||||
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 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.toString().padStart(4, "0")}(${info.id})`),
|
||||
);
|
||||
const guild = bgMagenta(black(`${info.guildId ? `Guild ID: (${info.guildId})` : "DM"}`));
|
||||
const user = bgGreen(
|
||||
black(`${info.user.username}#${info.user.discriminator.toString().padStart(4, '0')}(${info.id})`),
|
||||
);
|
||||
const guild = bgMagenta(black(`${info.guildId ? `Guild ID: (${info.guildId})` : 'DM'}`));
|
||||
|
||||
bot.logger.info(`${command} by ${user} in ${guild} with MessageID: ${info.id}`);
|
||||
bot.logger.info(`${command} by ${user} in ${guild} with MessageID: ${info.id}`);
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(bot: BotWithCustomProps, interaction: InteractionWithCustomProps) {
|
||||
const data = interaction.data;
|
||||
const name = data?.name as keyof typeof COMMANDS;
|
||||
const data = interaction.data;
|
||||
const name = data?.name as keyof typeof COMMANDS;
|
||||
|
||||
const command: Command<any> | undefined = COMMANDS[name];
|
||||
const command: Command<any> | undefined = COMMANDS[name];
|
||||
|
||||
// Command could not be found
|
||||
if (!command?.execute) {
|
||||
return await interaction
|
||||
.reply(translate(interaction.guildId!, "EXECUTE_COMMAND_NOT_FOUND"))
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
// Command could not be found
|
||||
if (!command?.execute) {
|
||||
return await interaction
|
||||
.reply(translate(interaction.guildId!, 'EXECUTE_COMMAND_NOT_FOUND'))
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// HAVE TO CONVERT OUTSIDE OF TRY SO IT CAN BE USED IN CATCH TOO
|
||||
try {
|
||||
logCommand(interaction, "Trigger", name);
|
||||
// HAVE TO CONVERT OUTSIDE OF TRY SO IT CAN BE USED IN CATCH TOO
|
||||
try {
|
||||
logCommand(interaction, 'Trigger', name);
|
||||
|
||||
// Check subcommand permissions and options
|
||||
if (!(await commandAllowed(interaction, command))) return;
|
||||
// Check subcommand permissions and options
|
||||
if (!(await commandAllowed(interaction, command))) return;
|
||||
|
||||
// Load the language for this guild
|
||||
if (interaction.guildId && !serverLanguages.has(interaction.guildId)) {
|
||||
// Todo: make command.execute reply change to editReply after running this
|
||||
// await interaction.reply({
|
||||
// type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
// });
|
||||
await loadLanguage(interaction.guildId);
|
||||
} // Load the language for this guild
|
||||
else if (command.acknowledge) {
|
||||
// Acknowledge the command
|
||||
await interaction.reply({
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
// Load the language for this guild
|
||||
if (interaction.guildId && !serverLanguages.has(interaction.guildId)) {
|
||||
// Todo: make command.execute reply change to editReply after running this
|
||||
// await interaction.reply({
|
||||
// type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
// });
|
||||
await loadLanguage(interaction.guildId);
|
||||
} // Load the language for this guild
|
||||
else if (command.acknowledge) {
|
||||
// Acknowledge the command
|
||||
await interaction.reply({
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
|
||||
// FIRST GET THE TRANSLATIONS FOR ALL OPTIONS
|
||||
const translatedOptionNames = interaction.guildId && command.options
|
||||
? translateOptionNames(bot, interaction.guildId, command.options)
|
||||
: {};
|
||||
// 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, translatedOptionNames);
|
||||
// PARSE THE OPTIONS TO A NICE OBJECT AND TRANSLATE THE KEYS TO ENGLISH
|
||||
const parsedArguments = optionParser(interaction, translatedOptionNames);
|
||||
|
||||
await command.execute(bot, interaction, parsedArguments as ConvertArgumentDefinitionsToArgs<any>);
|
||||
logCommand(interaction, "Success", name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logCommand(interaction, "Failure", name);
|
||||
await command.execute(bot, interaction, parsedArguments as ConvertArgumentDefinitionsToArgs<any>);
|
||||
logCommand(interaction, 'Success', name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logCommand(interaction, 'Failure', name);
|
||||
|
||||
try {
|
||||
console.log("try");
|
||||
// try to reply the interaction, becuase we don't know if it replied or deffered
|
||||
return await interaction.reply(translate(interaction.id, "EXECUTE_COMMAND_ERROR"));
|
||||
} catch {
|
||||
console.log("catch");
|
||||
// edit the reply or deffered reply of interaction
|
||||
return await interaction.editReply(translate(interaction.id, "EXECUTE_COMMAND_ERROR")).catch(bot.logger.error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log('try');
|
||||
// try to reply the interaction, becuase we don't know if it replied or deffered
|
||||
return await interaction.reply(translate(interaction.id, 'EXECUTE_COMMAND_ERROR'));
|
||||
} catch {
|
||||
console.log('catch');
|
||||
// edit the reply or deffered reply of interaction
|
||||
return await interaction.editReply(translate(interaction.id, 'EXECUTE_COMMAND_ERROR')).catch(bot.logger.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the inhibitors to see if a command is allowed to run. */
|
||||
export async function commandAllowed(interaction: InteractionWithCustomProps, command: Command<any>) {
|
||||
// CHECK WHETHER THE USER/GUILD IS VIP
|
||||
if (command.vipOnly) {
|
||||
// SETUP-DD-TEMP: Check if this server/user is a vip.
|
||||
const isVIP = true;
|
||||
// CHECK WHETHER THE USER/GUILD IS VIP
|
||||
if (command.vipOnly) {
|
||||
// SETUP-DD-TEMP: Check if this server/user is a vip.
|
||||
const isVIP = true;
|
||||
|
||||
if (!isVIP) {
|
||||
await interaction.reply(translate(interaction.id, "NEED_VIP")).catch(bot.logger.error);
|
||||
if (!isVIP) {
|
||||
await interaction.reply(translate(interaction.id, 'NEED_VIP')).catch(bot.logger.error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mapped by `language-commandName`
|
||||
@@ -123,189 +118,187 @@ 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,
|
||||
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}`)!;
|
||||
}
|
||||
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) {
|
||||
translated[translate(guildId, option.name as translationKeys).toLowerCase()] = translate(
|
||||
"english",
|
||||
option.name as translationKeys,
|
||||
);
|
||||
if (option.options) {
|
||||
translated = {
|
||||
...translated,
|
||||
...translateOptionNames(bot, guildId, option.options),
|
||||
};
|
||||
}
|
||||
}
|
||||
// TRANSLATE ALL OPTIONS
|
||||
let translated: Record<string, string> = {};
|
||||
for (const option of options) {
|
||||
translated[translate(guildId, option.name as translationKeys).toLowerCase()] = translate(
|
||||
'english',
|
||||
option.name as translationKeys,
|
||||
);
|
||||
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);
|
||||
}
|
||||
// SAVE THE TRANSLATED OPTIONS IN CACHE FOR FASTER ACCESS
|
||||
if (commandName) {
|
||||
translatedOptionNamesCache.set(`${language}-${commandName}`, translated);
|
||||
}
|
||||
|
||||
return translated;
|
||||
return translated;
|
||||
}
|
||||
|
||||
function convertOptionValue(
|
||||
interaction: Interaction,
|
||||
option: InteractionDataOption,
|
||||
translateOptions?: Record<string, string>,
|
||||
interaction: Interaction,
|
||||
option: InteractionDataOption,
|
||||
translateOptions?: Record<string, string>,
|
||||
): [
|
||||
string,
|
||||
(
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
),
|
||||
string,
|
||||
(
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
),
|
||||
] {
|
||||
// THE OPTION IS A CHANNEL
|
||||
if (option.type === ApplicationCommandOptionTypes.Channel) {
|
||||
const channel = interaction.data?.resolved?.channels?.get(BigInt(option.value as string));
|
||||
// THE OPTION IS A CHANNEL
|
||||
if (option.type === ApplicationCommandOptionTypes.Channel) {
|
||||
const channel = interaction.data?.resolved?.channels?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, channel];
|
||||
}
|
||||
// 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 = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
// THE OPTION IS A ROLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Role) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, role];
|
||||
}
|
||||
// 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 = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
// THE OPTION IS A USER
|
||||
if (option.type === ApplicationCommandOptionTypes.User) {
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [
|
||||
translateOptions?.[option.name] ?? option.name,
|
||||
{
|
||||
member,
|
||||
user,
|
||||
},
|
||||
];
|
||||
}
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [
|
||||
translateOptions?.[option.name] ?? option.name,
|
||||
{
|
||||
member,
|
||||
user,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// THE OPTION IS A MENTIONABLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Mentionable) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
// THE OPTION IS A MENTIONABLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Mentionable) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
const final = user && member ? { user, member } : role;
|
||||
const final = user && member ? { user, member } : role;
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, final];
|
||||
}
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, final];
|
||||
}
|
||||
|
||||
// THE REST OF OPTIONS DON'T NEED ANY CONVERTION
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
// @ts-expect-error
|
||||
return [translateOptions?.[option.name] ?? option.name, option.value];
|
||||
// THE REST OF OPTIONS DON'T NEED ANY CONVERTION
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
// @ts-expect-error
|
||||
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(
|
||||
interaction: Interaction,
|
||||
translateOptions?: Record<string, string>,
|
||||
interaction: Interaction,
|
||||
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 (!interaction.data?.options) return {};
|
||||
| InteractionCommandArgs
|
||||
| { [key: string]: InteractionCommandArgs }
|
||||
| { [key: string]: { [key: string]: InteractionCommandArgs } } {
|
||||
// OPTIONS CAN BE UNDEFINED SO WE JUST RETURN AN EMPTY OBJECT
|
||||
if (!interaction.data?.options) return {};
|
||||
|
||||
// A SUBCOMMAND WAS USED
|
||||
if (interaction.data.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 interaction.data.options[0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
// A SUBCOMMAND WAS USED
|
||||
if (interaction.data.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 interaction.data.options[0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: convertedOptions,
|
||||
};
|
||||
}
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: convertedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// A SUBCOMMAND GROUP WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
const convertedOptions: Record<string, Member | Role | Channel | boolean | string | number> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0]?.options![0]?.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
// A SUBCOMMAND GROUP WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
const convertedOptions: Record<string, Member | Role | Channel | boolean | string | number> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0]?.options![0]?.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: {
|
||||
[
|
||||
translateOptions?.[interaction.data.options[0]!.options![0]!.name] ??
|
||||
interaction.data.options[0]!.options![0]!.name
|
||||
]: convertedOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: {
|
||||
[translateOptions?.[interaction.data.options[0]!.options![0]!.name] ??
|
||||
interaction.data.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 interaction.data.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
// 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 interaction.data.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
return convertedOptions;
|
||||
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
|
||||
string,
|
||||
Member | Role | Record<string, Pick<Channel, 'id' | 'name' | 'type' | 'permissions'>> | boolean | string | number
|
||||
>;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { InteractionTypes, MessageComponentTypes } from "discordeno";
|
||||
import { bot } from "../../bot.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import { executeButtonClick } from "./button.js";
|
||||
import { executeSlashCommand } from "./command.js";
|
||||
import { executeModalSubmit } from "./modal.js";
|
||||
import { InteractionTypes, MessageComponentTypes } from 'discordeno';
|
||||
import { bot } from '../../bot.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import { executeButtonClick } from './button.js';
|
||||
import { executeSlashCommand } from './command.js';
|
||||
import { executeModalSubmit } from './modal.js';
|
||||
|
||||
export function setInteractionCreateEvent() {
|
||||
bot.events.interactionCreate = async function (_, interaction) {
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
await executeSlashCommand(bot, interaction as InteractionWithCustomProps);
|
||||
} else if (interaction.type === InteractionTypes.MessageComponent) {
|
||||
if (!interaction.data) return;
|
||||
bot.events.interactionCreate = async function (_, interaction) {
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
await executeSlashCommand(bot, interaction as InteractionWithCustomProps);
|
||||
} else if (interaction.type === InteractionTypes.MessageComponent) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
// THE INTERACTION CAME FROM A BUTTON
|
||||
if (interaction.data.componentType === MessageComponentTypes.Button) {
|
||||
await executeButtonClick(bot, interaction);
|
||||
}
|
||||
} else if (interaction.type === InteractionTypes.ModalSubmit) {
|
||||
await executeModalSubmit(bot, interaction);
|
||||
}
|
||||
};
|
||||
// THE INTERACTION CAME FROM A BUTTON
|
||||
if (interaction.data.componentType === MessageComponentTypes.Button) {
|
||||
await executeButtonClick(bot, interaction);
|
||||
}
|
||||
} else if (interaction.type === InteractionTypes.ModalSubmit) {
|
||||
await executeModalSubmit(bot, interaction);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import type { Interaction } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
|
||||
export async function executeModalSubmit(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Modal] The ${
|
||||
interaction.data?.customId || "UNKNWON"
|
||||
} modal was submitted in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
bot.logger.info(
|
||||
`[Modal] The ${interaction.data?.customId || 'UNKNWON'} modal was submitted in Guild: ${interaction.guildId} by ${
|
||||
interaction.user.id
|
||||
}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { bot } from "../../bot.js";
|
||||
import { processMessageCollectors } from "../../utils/collectors.js";
|
||||
import { bot } from '../../bot.js';
|
||||
import { processMessageCollectors } from '../../utils/collectors.js';
|
||||
|
||||
export function setMessageCreateEvent() {
|
||||
bot.events.messageCreate = async function (_, message) {
|
||||
processMessageCollectors(message);
|
||||
bot.events.messageCreate = async function (_, message) {
|
||||
processMessageCollectors(message);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Add any functions you want to run on every message here. For example, automoderation filters.
|
||||
]).catch(console.log);
|
||||
};
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Add any functions you want to run on every message here. For example, automoderation filters.
|
||||
]).catch(console.log);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { setInteractionCreateEvent } from "./interactions/mod.js";
|
||||
import { setMessageCreateEvent } from "./messages/create.js";
|
||||
import { setRawEvent } from "./raw.js";
|
||||
import { setInteractionCreateEvent } from './interactions/mod.js';
|
||||
import { setMessageCreateEvent } from './messages/create.js';
|
||||
import { setRawEvent } from './raw.js';
|
||||
|
||||
export function setupEventHandlers() {
|
||||
setInteractionCreateEvent();
|
||||
setRawEvent();
|
||||
setMessageCreateEvent();
|
||||
setInteractionCreateEvent();
|
||||
setRawEvent();
|
||||
setMessageCreateEvent();
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import type { DiscordUnavailableGuild } from "discordeno";
|
||||
import { prisma } from "../../prisma.js";
|
||||
import { bot } from "../bot.js";
|
||||
import { updateGuildCommands, usesLatestCommandVersion } from "../utils/slash/updateCommands.js";
|
||||
import type { DiscordUnavailableGuild } from 'discordeno';
|
||||
import { prisma } from '../../prisma.js';
|
||||
import { bot } from '../bot.js';
|
||||
import { updateGuildCommands, usesLatestCommandVersion } from '../utils/slash/updateCommands.js';
|
||||
|
||||
/** To prevent updating every guild when a shard goes ready we have to ignore them using this */
|
||||
// export const initialyLoadingGuildIds = new Set<bigint>()
|
||||
|
||||
export function setRawEvent() {
|
||||
bot.events.raw = async function (_, data) {
|
||||
if (data.t === "GUILD_DELETE") {
|
||||
const id = (data.d as DiscordUnavailableGuild).id;
|
||||
bot.events.raw = async function (_, data) {
|
||||
if (data.t === 'GUILD_DELETE') {
|
||||
const id = (data.d as DiscordUnavailableGuild).id;
|
||||
|
||||
return await prisma.commands.delete({ where: { id: bot.transformers.snowflake(id) } });
|
||||
}
|
||||
return await prisma.commands.delete({ where: { id: bot.transformers.snowflake(id) } });
|
||||
}
|
||||
|
||||
const id = bot.transformers.snowflake(
|
||||
(data.t && ["GUILD_UPDATE", "GUILD_CREATE"].includes(data.t)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
? (data.d )?.id
|
||||
// deno-lint-ignore no-explicit-any
|
||||
: (data.d )?.guild_id) ?? "",
|
||||
);
|
||||
const id = bot.transformers.snowflake(
|
||||
(data.t && ['GUILD_UPDATE', 'GUILD_CREATE'].includes(data.t)
|
||||
? // deno-lint-ignore no-explicit-any
|
||||
data.d?.id
|
||||
: // deno-lint-ignore no-explicit-any
|
||||
data.d?.guild_id) ?? '',
|
||||
);
|
||||
|
||||
// The GUILD_CREATE event came from a shard loaded event so ignore it
|
||||
if (["READY", "GUILD_LOADED_DD", null].includes(data.t)) return;
|
||||
// The GUILD_CREATE event came from a shard loaded event so ignore it
|
||||
if (['READY', 'GUILD_LOADED_DD', null].includes(data.t)) return;
|
||||
|
||||
// console.log({ id, v: await usesLatestCommandVersion(id) })
|
||||
// console.log({ id, v: await usesLatestCommandVersion(id) })
|
||||
|
||||
if (!id || (await usesLatestCommandVersion(id))) return;
|
||||
// dev guild
|
||||
if (id === 547046977578336286n) return;
|
||||
if (!id || (await usesLatestCommandVersion(id))) return;
|
||||
// dev guild
|
||||
if (id === 547046977578336286n) return;
|
||||
|
||||
// NEW GUILD AVAILABLE
|
||||
bot.logger.info(`[Slash Setup] Installing Slash commands on Guild ${id} event type: ${data.t}`);
|
||||
await updateGuildCommands(bot, id).catch(bot.logger.error);
|
||||
};
|
||||
// NEW GUILD AVAILABLE
|
||||
bot.logger.info(`[Slash Setup] Installing Slash commands on Guild ${id} event type: ${data.t}`);
|
||||
await updateGuildCommands(bot, id).catch(bot.logger.error);
|
||||
};
|
||||
}
|
||||
|
||||
+115
-119
@@ -1,14 +1,14 @@
|
||||
import dotenv from "dotenv";
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import type { DiscordGatewayPayload } from "discordeno";
|
||||
import type { DiscordGatewayPayload } from 'discordeno';
|
||||
// ReferenceError: publishMessage is not defined
|
||||
// import Embeds from "discordeno/embeds";
|
||||
import amqplib from "amqplib";
|
||||
import express from "express";
|
||||
import { BOT_ID, EVENT_HANDLER_URL } from "../configs.js";
|
||||
import { bot } from "./bot.js";
|
||||
import { updateDevCommands } from "./utils/slash/updateCommands.js";
|
||||
import { webhookURLToIDAndToken } from "./utils/webhook.js";
|
||||
import amqplib from 'amqplib';
|
||||
import express from 'express';
|
||||
import { BOT_ID, EVENT_HANDLER_URL } from '../configs.js';
|
||||
import { bot } from './bot.js';
|
||||
import { updateDevCommands } from './utils/slash/updateCommands.js';
|
||||
import { webhookURLToIDAndToken } from './utils/webhook.js';
|
||||
dotenv.config();
|
||||
|
||||
const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK;
|
||||
@@ -16,23 +16,23 @@ const EVENT_HANDLER_AUTHORIZATION = process.env.EVENT_HANDLER_AUTHORIZATION as s
|
||||
const EVENT_HANDLER_PORT = process.env.EVENT_HANDLER_PORT as string;
|
||||
|
||||
process
|
||||
.on("unhandledRejection", (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
.on('unhandledRejection', (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled rejection error occurred but error was null or undefined`);
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled rejection error occurred but error was null or undefined`);
|
||||
|
||||
if (!error) return;
|
||||
if (!error) return;
|
||||
|
||||
// ReferenceError: publishMessage is not defined
|
||||
/*
|
||||
// ReferenceError: publishMessage is not defined
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
@@ -41,23 +41,23 @@ process
|
||||
// SEND ERROR TO THE LOG CHANNEL ON THE DEV SERVER
|
||||
return bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
})
|
||||
.on("uncaughtException", async (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
})
|
||||
.on('uncaughtException', async (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled exception occurred but error was null or undefined`);
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled exception occurred but error was null or undefined`);
|
||||
|
||||
if (!error) process.exit(1);
|
||||
if (!error) process.exit(1);
|
||||
|
||||
/*
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error.stack, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
@@ -66,124 +66,120 @@ process
|
||||
await bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.env.DEVELOPMENT === "true") {
|
||||
bot.logger.info(`[DEV MODE] Updating slash commands for dev server.`);
|
||||
updateDevCommands(bot);
|
||||
if (process.env.DEVELOPMENT === 'true') {
|
||||
bot.logger.info(`[DEV MODE] Updating slash commands for dev server.`);
|
||||
updateDevCommands(bot);
|
||||
}
|
||||
|
||||
// Handle events from the gateway
|
||||
const handleEvent = async (message: DiscordGatewayPayload, shardId: number) => {
|
||||
// EMITS RAW EVENT
|
||||
bot.events.raw(bot, message, shardId);
|
||||
// EMITS RAW EVENT
|
||||
bot.events.raw(bot, message, shardId);
|
||||
|
||||
if (message.t && message.t !== "RESUMED") {
|
||||
// When a guild or something isnt in cache this will fetch it before doing anything else
|
||||
if (!["READY", "GUILD_LOADED_DD"].includes(message.t)) {
|
||||
await bot.events.dispatchRequirements(bot, message, shardId);
|
||||
}
|
||||
if (message.t && message.t !== 'RESUMED') {
|
||||
// When a guild or something isnt in cache this will fetch it before doing anything else
|
||||
if (!['READY', 'GUILD_LOADED_DD'].includes(message.t)) {
|
||||
await bot.events.dispatchRequirements(bot, message, shardId);
|
||||
}
|
||||
|
||||
bot.handlers[message.t]?.(bot, message, shardId);
|
||||
}
|
||||
bot.handlers[message.t]?.(bot, message, shardId);
|
||||
}
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all("/", async (req, res) => {
|
||||
try {
|
||||
if (!EVENT_HANDLER_AUTHORIZATION || EVENT_HANDLER_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: "Invalid authorization key." });
|
||||
}
|
||||
app.all('/', async (req, res) => {
|
||||
try {
|
||||
if (!EVENT_HANDLER_AUTHORIZATION || EVENT_HANDLER_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: 'Invalid authorization key.' });
|
||||
}
|
||||
|
||||
const json = req.body as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
const json = req.body as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
await handleEvent(json.message, json.shardId);
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error: any) {
|
||||
bot.logger.error(error);
|
||||
res.status(error.code).json(error);
|
||||
}
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error: any) {
|
||||
bot.logger.error(error);
|
||||
res.status(error.code).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(EVENT_HANDLER_PORT, () => {
|
||||
console.log(`Bot is listening at ${EVENT_HANDLER_URL};`);
|
||||
console.log(`Bot is listening at ${EVENT_HANDLER_URL};`);
|
||||
});
|
||||
|
||||
const connectRabbitmq = async () => {
|
||||
let connection: amqplib.Connection | undefined;
|
||||
let connection: amqplib.Connection | undefined;
|
||||
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
|
||||
if (!connection) return;
|
||||
connection.on("error", (err) => {
|
||||
console.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
if (!connection) return;
|
||||
connection.on('error', (err) => {
|
||||
console.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
connection.on("close", () => {
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
connection.on('close', () => {
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
const channel = await connection.createChannel();
|
||||
try {
|
||||
const channel = await connection.createChannel();
|
||||
|
||||
await channel.assertExchange(
|
||||
"gatewayMessage",
|
||||
"x-message-deduplication",
|
||||
{
|
||||
durable: true,
|
||||
arguments: {
|
||||
"x-cache-size": 1000,
|
||||
"x-cache-ttl": 500,
|
||||
},
|
||||
},
|
||||
);
|
||||
await channel.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-cache-size': 1000,
|
||||
'x-cache-ttl': 500,
|
||||
},
|
||||
});
|
||||
|
||||
await channel.assertQueue("gatewayMessageQueue");
|
||||
await channel.bindQueue("gatewayMessageQueue", "gatewayMessage", "");
|
||||
await channel.consume(
|
||||
"gatewayMessageQueue",
|
||||
async (msg) => {
|
||||
if (!msg) return;
|
||||
const json = JSON.parse(msg.content.toString()) as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
await channel.assertQueue('gatewayMessageQueue');
|
||||
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '');
|
||||
await channel.consume(
|
||||
'gatewayMessageQueue',
|
||||
async (msg) => {
|
||||
if (!msg) return;
|
||||
const json = JSON.parse(msg.content.toString()) as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
await handleEvent(json.message, json.shardId);
|
||||
|
||||
await channel.ack(msg);
|
||||
},
|
||||
{
|
||||
noAck: false,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await channel.ack(msg);
|
||||
},
|
||||
{
|
||||
noAck: false,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.MESSAGEQUEUE_ENABLE === "true") {
|
||||
connectRabbitmq();
|
||||
if (process.env.MESSAGEQUEUE_ENABLE === 'true') {
|
||||
connectRabbitmq();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
const english = {
|
||||
// Permissions
|
||||
NEED_VIP: "❌ Only VIP users or servers can use this feature.",
|
||||
// Permissions
|
||||
NEED_VIP: '❌ Only VIP users or servers can use this feature.',
|
||||
|
||||
// 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.",
|
||||
// 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.',
|
||||
|
||||
// Language Command
|
||||
LANGUAGE_NAME: "language",
|
||||
LANGUAGE_DESCRIPTION: "⚙️ Change the bots language.",
|
||||
LANGUAGE_KEY_NAME: "name",
|
||||
LANGUAGE_KEY_DESCRIPTION: "What language would you like to set?",
|
||||
LANGUAGE_UPDATED: (language: string) => `The language has been updated to ${language}`,
|
||||
// Language Command
|
||||
LANGUAGE_NAME: 'language',
|
||||
LANGUAGE_DESCRIPTION: '⚙️ Change the bots language.',
|
||||
LANGUAGE_KEY_NAME: 'name',
|
||||
LANGUAGE_KEY_DESCRIPTION: 'What language would you like to set?',
|
||||
LANGUAGE_UPDATED: (language: string) => `The language has been updated to ${language}`,
|
||||
|
||||
// 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:`,
|
||||
// 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:`,
|
||||
} as const;
|
||||
|
||||
export default english;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import english from "./english.js";
|
||||
import english from './english.js';
|
||||
// import french from './french'
|
||||
// import german from './german'
|
||||
// import portuguese from './portuguese'
|
||||
@@ -6,15 +6,15 @@ import english from "./english.js";
|
||||
// import spanish from './spanish'
|
||||
|
||||
const languages: Record<LanguageNames, Language> & Record<string, Language> = {
|
||||
english,
|
||||
// french,
|
||||
// german,
|
||||
// portuguese,
|
||||
// russian,
|
||||
// spanish,
|
||||
english,
|
||||
// french,
|
||||
// german,
|
||||
// portuguese,
|
||||
// russian,
|
||||
// spanish,
|
||||
};
|
||||
|
||||
export default languages;
|
||||
|
||||
export type Language = Record<string, string | string[] | ((...args: any[]) => string)>;
|
||||
export type LanguageNames = "english";
|
||||
export type LanguageNames = 'english';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Embeds from "discordeno/embeds";
|
||||
import { bot } from "../bot.js";
|
||||
import { webhookURLToIDAndToken } from "../utils/webhook.js";
|
||||
import type english from "./english.js";
|
||||
import languages from "./languages.js";
|
||||
import Embeds from 'discordeno/embeds';
|
||||
import { bot } from '../bot.js';
|
||||
import { webhookURLToIDAndToken } from '../utils/webhook.js';
|
||||
import type english from './english.js';
|
||||
import languages from './languages.js';
|
||||
|
||||
const MISSING_TRANSLATION_WEBHOOK = process.env.MISSING_TRANSLATION_WEBHOOK;
|
||||
|
||||
@@ -10,75 +10,75 @@ const MISSING_TRANSLATION_WEBHOOK = process.env.MISSING_TRANSLATION_WEBHOOK;
|
||||
export const serverLanguages = new Map<bigint, keyof typeof languages>();
|
||||
|
||||
export function translate<K extends translationKeys>(
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
key: K,
|
||||
...params: getArgs<K>
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
key: K,
|
||||
...params: getArgs<K>
|
||||
): string {
|
||||
const language = getLanguage(guildIdOrLanguage);
|
||||
let value: string | ((...any: any[]) => string) | string[] | undefined = languages[language]?.[key];
|
||||
const language = getLanguage(guildIdOrLanguage);
|
||||
let value: string | ((...any: any[]) => string) | string[] | undefined = 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];
|
||||
}
|
||||
// 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;
|
||||
// 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(language, key);
|
||||
}
|
||||
// Send a log webhook so the devs know sth is missing
|
||||
missingTranslation(language, key);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) return value.join("\n");
|
||||
if (Array.isArray(value)) return value.join('\n');
|
||||
|
||||
if (typeof value === "function") return value(...(params || []));
|
||||
if (typeof value === 'function') return value(...(params || []));
|
||||
|
||||
return value ;
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 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";
|
||||
return typeof guildIdOrLanguage === 'string'
|
||||
? guildIdOrLanguage
|
||||
: serverLanguages.get(guildIdOrLanguage) ?? 'english';
|
||||
}
|
||||
|
||||
export async function loadLanguage(guildId: bigint) {
|
||||
// TODO: add this settings
|
||||
// const settings = await database.findOne('guilds', guildId)
|
||||
const settings = { language: "undefined" };
|
||||
// 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");
|
||||
if (settings?.language && languages[settings.language]) {
|
||||
serverLanguages.set(guildId, settings.language);
|
||||
} else serverLanguages.set(guildId, 'english');
|
||||
}
|
||||
|
||||
/** Send a webhook for a missing translation key */
|
||||
export async function missingTranslation(language: keyof typeof languages, key: string) {
|
||||
if (!MISSING_TRANSLATION_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(MISSING_TRANSLATION_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
if (!MISSING_TRANSLATION_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(MISSING_TRANSLATION_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
const embeds = new Embeds()
|
||||
.setTitle("Missing Translation")
|
||||
.setColor("RANDOM")
|
||||
.addField("Language", language, true)
|
||||
.addField("Key", key, true);
|
||||
const embeds = new Embeds()
|
||||
.setTitle('Missing Translation')
|
||||
.setColor('RANDOM')
|
||||
.addField('Language', language, true)
|
||||
.addField('Key', key, true);
|
||||
|
||||
await bot.helpers
|
||||
.sendWebhookMessage(bot.transformers.snowflake(id), token, {
|
||||
// SETUP-DD-TEMP: If you wish to make it @ mention you, please edit the next line.
|
||||
// content: `<@${owner id here}>`,
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(bot.logger.error);
|
||||
await bot.helpers
|
||||
.sendWebhookMessage(bot.transformers.snowflake(id), token, {
|
||||
// SETUP-DD-TEMP: If you wish to make it @ mention you, please edit the next line.
|
||||
// content: `<@${owner id here}>`,
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// type translationKeys = keyof typeof english | string
|
||||
export type translationKeys = keyof typeof english;
|
||||
type getArgs<K extends translationKeys> = typeof english[K] extends (...any: any[]) => unknown
|
||||
? Parameters<typeof english[K]>
|
||||
: [];
|
||||
? Parameters<typeof english[K]>
|
||||
: [];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// This file allows you to tell typescript about any additions you have made to the internal discordeno objects.
|
||||
import type { Interaction, InteractionCallbackData, InteractionResponse, Message } from "discordeno";
|
||||
import type { Interaction, InteractionCallbackData, InteractionResponse, Message } from 'discordeno';
|
||||
|
||||
export interface InteractionWithCustomProps extends Interaction {
|
||||
// Normally, to send a response you would have to do something like bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: "text here" } })
|
||||
// But with this reply method we added, it is as simple as interaction.reply("text here").
|
||||
// Feel free to delete these comments once you have understood the concept.
|
||||
/** Send a reply to an interaction. */
|
||||
reply: (response: InteractionResponse | string) => Promise<Message | undefined>;
|
||||
/** Edit a deferred reply of an interaction. */
|
||||
editReply: (response: InteractionCallbackData | string) => Promise<Message | undefined>;
|
||||
// Normally, to send a response you would have to do something like bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: "text here" } })
|
||||
// But with this reply method we added, it is as simple as interaction.reply("text here").
|
||||
// Feel free to delete these comments once you have understood the concept.
|
||||
/** Send a reply to an interaction. */
|
||||
reply: (response: InteractionResponse | string) => Promise<Message | undefined>;
|
||||
/** Edit a deferred reply of an interaction. */
|
||||
editReply: (response: InteractionCallbackData | string) => Promise<Message | undefined>;
|
||||
}
|
||||
|
||||
@@ -1,151 +1,151 @@
|
||||
import type { Interaction, Member, Message } from "discordeno";
|
||||
import { bot } from "../bot.js";
|
||||
import type { Interaction, Member, Message } from 'discordeno';
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: 1 },
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: 1 },
|
||||
): Promise<Message>;
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: number },
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: number },
|
||||
): Promise<Message[]>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint): Promise<Message>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint, options?: MessageCollectorOptions) {
|
||||
const messages = await collectMessages({
|
||||
key: memberId,
|
||||
channelId,
|
||||
createdAt: Date.now(),
|
||||
filter: options?.filter || ((msg) => memberId === msg.authorId),
|
||||
amount: options?.amount || 1,
|
||||
duration: options?.duration || (1000 * 60 * 5),
|
||||
});
|
||||
const messages = await collectMessages({
|
||||
key: memberId,
|
||||
channelId,
|
||||
createdAt: Date.now(),
|
||||
filter: options?.filter || ((msg) => memberId === msg.authorId),
|
||||
amount: options?.amount || 1,
|
||||
duration: options?.duration || 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
return (options?.amount || 1) > 1 ? messages : messages[0];
|
||||
return (options?.amount || 1) > 1 ? messages : messages[0];
|
||||
}
|
||||
|
||||
export async function collectMessages(options: CollectMessagesOptions): Promise<Message[]> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
bot.collectors.messages.get(options.key)?.reject(
|
||||
"A new collector began before the user responded to the previous one.",
|
||||
);
|
||||
return await new Promise((resolve, reject) => {
|
||||
bot.collectors.messages
|
||||
.get(options.key)
|
||||
?.reject('A new collector began before the user responded to the previous one.');
|
||||
|
||||
bot.collectors.messages.set(options.key, {
|
||||
...options,
|
||||
messages: [],
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
bot.collectors.messages.set(options.key, {
|
||||
...options,
|
||||
messages: [],
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function processMessageCollectors(message: Message) {
|
||||
// IGNORE DMS
|
||||
if (!message.guildId) return;
|
||||
// IGNORE DMS
|
||||
if (!message.guildId) return;
|
||||
|
||||
const collector = bot.collectors.messages.get(message.authorId);
|
||||
// This user has no collectors pending or the message is in a different channel
|
||||
if (!collector || message.channelId !== collector.channelId) return;
|
||||
// This message is a response to a collector. Now running the filter function.
|
||||
if (!collector.filter(message)) return;
|
||||
const collector = bot.collectors.messages.get(message.authorId);
|
||||
// This user has no collectors pending or the message is in a different channel
|
||||
if (!collector || message.channelId !== collector.channelId) return;
|
||||
// This message is a response to a collector. Now running the filter function.
|
||||
if (!collector.filter(message)) return;
|
||||
|
||||
// If the necessary amount has been collected
|
||||
if (collector.amount === 1 || collector.amount === collector.messages.length + 1) {
|
||||
// Remove the collector
|
||||
bot.collectors.messages.delete(message.authorId);
|
||||
// Resolve the collector
|
||||
return collector.resolve([...collector.messages, message]);
|
||||
}
|
||||
// If the necessary amount has been collected
|
||||
if (collector.amount === 1 || collector.amount === collector.messages.length + 1) {
|
||||
// Remove the collector
|
||||
bot.collectors.messages.delete(message.authorId);
|
||||
// Resolve the collector
|
||||
return collector.resolve([...collector.messages, message]);
|
||||
}
|
||||
|
||||
// More messages still need to be collected
|
||||
collector.messages.push(message);
|
||||
// More messages still need to be collected
|
||||
collector.messages.push(message);
|
||||
}
|
||||
|
||||
export interface BaseCollectorOptions {
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface MessageCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message) => boolean;
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message) => boolean;
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ReactionCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface BaseCollectorCreateOptions {
|
||||
/** The unique key that will be used to get responses for this. Ideally, meant to be for member id. */
|
||||
key: bigint;
|
||||
/** The amount of messages to collect before resolving. */
|
||||
amount: number;
|
||||
/** The timestamp when this collector was created */
|
||||
createdAt: number;
|
||||
/** The duration in milliseconds how long this collector should last. */
|
||||
duration: number;
|
||||
/** The unique key that will be used to get responses for this. Ideally, meant to be for member id. */
|
||||
key: bigint;
|
||||
/** The amount of messages to collect before resolving. */
|
||||
amount: number;
|
||||
/** The timestamp when this collector was created */
|
||||
createdAt: number;
|
||||
/** The duration in milliseconds how long this collector should last. */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface CollectMessagesOptions extends BaseCollectorCreateOptions {
|
||||
/** The channel Id where this is listening to */
|
||||
channelId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message) => boolean;
|
||||
/** The channel Id where this is listening to */
|
||||
channelId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message) => boolean;
|
||||
}
|
||||
|
||||
export interface CollectReactionsOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface MessageCollector extends CollectMessagesOptions {
|
||||
resolve: (value: Message[] | PromiseLike<Message[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the messages are stored if the amount to collect is more than 1. */
|
||||
messages: Message[];
|
||||
resolve: (value: Message[] | PromiseLike<Message[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the messages are stored if the amount to collect is more than 1. */
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface ReactionCollector extends CollectReactionsOptions {
|
||||
resolve: (value: string[] | PromiseLike<string[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the reactions are stored if the amount to collect is more than 1. */
|
||||
reactions: string[];
|
||||
resolve: (value: string[] | PromiseLike<string[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the reactions are stored if the amount to collect is more than 1. */
|
||||
reactions: string[];
|
||||
}
|
||||
|
||||
export interface CollectButtonOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message, member?: Member) => boolean;
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollector extends CollectButtonOptions {
|
||||
resolve: (value: ButtonCollectorReturn[] | PromiseLike<ButtonCollectorReturn[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the buttons are stored if the amount to collect is more than 1. */
|
||||
buttons: ButtonCollectorReturn[];
|
||||
resolve: (value: ButtonCollectorReturn[] | PromiseLike<ButtonCollectorReturn[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the buttons are stored if the amount to collect is more than 1. */
|
||||
buttons: ButtonCollectorReturn[];
|
||||
}
|
||||
|
||||
export interface ButtonCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message, member?: Member) => boolean;
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollectorReturn {
|
||||
customId: string;
|
||||
interaction: Omit<Interaction, "member">;
|
||||
member?: Member;
|
||||
customId: string;
|
||||
interaction: Omit<Interaction, 'member'>;
|
||||
member?: Member;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import { customizeTransformers } from "./transformers/mod.js";
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
import { customizeTransformers } from './transformers/mod.js';
|
||||
|
||||
export function customizeInternals(bot: BotWithCustomProps) {
|
||||
customizeTransformers(bot);
|
||||
customizeTransformers(bot);
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
// Only keep the properties your bot uses. If your bot does not use emojis in cache, you can save all that memory.
|
||||
// This file is currently disabled, but you can enable it should you choose when you go the customizer file.
|
||||
// Feel free to delete this comment or file as you wish.
|
||||
import type { Guild } from "discordeno";
|
||||
import { Collection } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
import type { Guild } from 'discordeno';
|
||||
import { Collection } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
|
||||
export function customizeGuildTransformer(bot: BotWithCustomProps) {
|
||||
bot.transformers.guild = function (bot, payload) {
|
||||
const guildId = bot.transformers.snowflake(payload.guild.id);
|
||||
bot.transformers.guild = function (bot, payload) {
|
||||
const guildId = bot.transformers.snowflake(payload.guild.id);
|
||||
|
||||
return {
|
||||
name: payload.guild.name,
|
||||
joinedAt: payload.guild.joined_at ? Date.parse(payload.guild.joined_at) : undefined,
|
||||
memberCount: payload.guild.member_count ?? 0,
|
||||
shardId: payload.shardId,
|
||||
icon: payload.guild.icon ? bot.utils.iconHashToBigInt(payload.guild.icon) : undefined,
|
||||
roles: new Collection(
|
||||
payload.guild.roles?.map((role) => {
|
||||
const result = bot.transformers.role(bot, { role, guildId });
|
||||
return [result.id, result];
|
||||
}),
|
||||
),
|
||||
id: guildId,
|
||||
ownerId: bot.transformers.snowflake(payload.guild.owner_id),
|
||||
} as unknown as Guild;
|
||||
};
|
||||
return {
|
||||
name: payload.guild.name,
|
||||
joinedAt: payload.guild.joined_at ? Date.parse(payload.guild.joined_at) : undefined,
|
||||
memberCount: payload.guild.member_count ?? 0,
|
||||
shardId: payload.shardId,
|
||||
icon: payload.guild.icon ? bot.utils.iconHashToBigInt(payload.guild.icon) : undefined,
|
||||
roles: new Collection(
|
||||
payload.guild.roles?.map((role) => {
|
||||
const result = bot.transformers.role(bot, { role, guildId });
|
||||
return [result.id, result];
|
||||
}),
|
||||
),
|
||||
id: guildId,
|
||||
ownerId: bot.transformers.snowflake(payload.guild.owner_id),
|
||||
} as unknown as Guild;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
// SETUP-DD-TEMP: This file serves as an example, of how to customize internal discordeno objects. Feel free to use, add more or remove as desired.
|
||||
import type { InteractionCallbackData, InteractionResponse} from "discordeno";
|
||||
import { InteractionResponseTypes } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
import type { InteractionCallbackData, InteractionResponse } from 'discordeno';
|
||||
import { InteractionResponseTypes } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
|
||||
export function customizeInteractionTransformer(bot: BotWithCustomProps) {
|
||||
// Store the internal transformer function
|
||||
const oldInteraction = bot.transformers.interaction;
|
||||
// Store the internal transformer function
|
||||
const oldInteraction = bot.transformers.interaction;
|
||||
|
||||
// Overwrite the internal function.
|
||||
bot.transformers.interaction = function (_, payload) {
|
||||
// Run the old function to get the internal value.
|
||||
const interaction = oldInteraction(bot, payload);
|
||||
// Overwrite the internal function.
|
||||
bot.transformers.interaction = function (_, payload) {
|
||||
// Run the old function to get the internal value.
|
||||
const interaction = oldInteraction(bot, payload);
|
||||
|
||||
// Add anything to this object. In this case we add a Interaction.reply() method.
|
||||
Object.defineProperty(interaction, "reply", {
|
||||
value: function (response: InteractionResponse | string) {
|
||||
if (typeof response === "string") {
|
||||
response = { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: response } };
|
||||
}
|
||||
// Add anything to this object. In this case we add a Interaction.reply() method.
|
||||
Object.defineProperty(interaction, 'reply', {
|
||||
value: function (response: InteractionResponse | string) {
|
||||
if (typeof response === 'string') {
|
||||
response = { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: response } };
|
||||
}
|
||||
|
||||
return bot.helpers.sendInteractionResponse(interaction.id, interaction.token, response);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(interaction, "editReply", {
|
||||
value: function (response: InteractionCallbackData | string) {
|
||||
if (typeof response === "string") {
|
||||
response = { content: response };
|
||||
}
|
||||
return bot.helpers.sendInteractionResponse(interaction.id, interaction.token, response);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(interaction, 'editReply', {
|
||||
value: function (response: InteractionCallbackData | string) {
|
||||
if (typeof response === 'string') {
|
||||
response = { content: response };
|
||||
}
|
||||
|
||||
return bot.helpers.editOriginalInteractionResponse(interaction.token, response);
|
||||
},
|
||||
});
|
||||
// Add as many properties or methods you would like here.
|
||||
// NOTE: Whenever you add anything here, in order to get nice autocomplete you should also add it to the src/types/discordeno.ts file.
|
||||
return bot.helpers.editOriginalInteractionResponse(interaction.token, response);
|
||||
},
|
||||
});
|
||||
// Add as many properties or methods you would like here.
|
||||
// NOTE: Whenever you add anything here, in order to get nice autocomplete you should also add it to the src/types/discordeno.ts file.
|
||||
|
||||
// Return the new customized object.
|
||||
return interaction;
|
||||
};
|
||||
// Return the new customized object.
|
||||
return interaction;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// import { customizeGuildTransformer } from "./guild.js";
|
||||
import { customizeInteractionTransformer } from "./interaction.js";
|
||||
import { customizeInteractionTransformer } from './interaction.js';
|
||||
|
||||
export function customizeTransformers(bot: BotWithCustomProps) {
|
||||
customizeInteractionTransformer(bot);
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// customizeGuildTransformer(bot);
|
||||
customizeInteractionTransformer(bot);
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// customizeGuildTransformer(bot);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type {
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Bot,
|
||||
Channel,
|
||||
Interaction,
|
||||
Member,
|
||||
PermissionStrings,
|
||||
Role,
|
||||
User,
|
||||
} from "discordeno";
|
||||
import type english from "../../languages/english.js";
|
||||
import type { translationKeys } from "../../languages/translate.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import type { PermissionLevelHandlers } from "./permLevels.js";
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Bot,
|
||||
Channel,
|
||||
Interaction,
|
||||
Member,
|
||||
PermissionStrings,
|
||||
Role,
|
||||
User,
|
||||
} from 'discordeno';
|
||||
import type english from '../../languages/english.js';
|
||||
import type { translationKeys } from '../../languages/translate.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import type { PermissionLevelHandlers } from './permLevels.js';
|
||||
|
||||
export function createCommand<T extends readonly ArgumentDefinition[]>(command: Command<T>) {
|
||||
return command;
|
||||
return command;
|
||||
}
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
||||
@@ -25,268 +25,283 @@ type Identity<T> = { [P in keyof T]: T[P] };
|
||||
// TODO: make required by default true
|
||||
// Define each of the types here
|
||||
interface BaseDefinition {
|
||||
description: translationKeys;
|
||||
description: translationKeys;
|
||||
}
|
||||
|
||||
// Subcommand
|
||||
type SubcommandArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommand;
|
||||
// options: Omit<ArgumentDefinition, 'SubcommandArgumentDefinition' | 'SubcommandGroupArgumentDefinition'>[]
|
||||
options?: readonly ArgumentDefinition[];
|
||||
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[];
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommandGroup;
|
||||
options: readonly SubcommandArgumentDefinition[];
|
||||
};
|
||||
|
||||
// String
|
||||
type StringArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: true;
|
||||
};
|
||||
type StringOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Integer
|
||||
type IntegerArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required: true;
|
||||
};
|
||||
type IntegerOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// BOOLEAN
|
||||
type BooleanArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required: true;
|
||||
};
|
||||
type BooleanOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// USER
|
||||
type UserArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required: true;
|
||||
};
|
||||
type UserOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// CHANNEL
|
||||
type ChannelArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required: true;
|
||||
};
|
||||
type ChannelOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// ROLE
|
||||
type RoleArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required: true;
|
||||
};
|
||||
type RoleOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required?: false;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// MENTIONABLE
|
||||
type MentionableArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required: true;
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required: true;
|
||||
};
|
||||
type MentionableOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required?: false;
|
||||
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;
|
||||
| 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
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]?: T[P]["choices"] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
: T[P] extends StringArgumentDefinition<infer N> ? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]: T[P]["choices"] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
// INTEGER
|
||||
: T[P] extends IntegerOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: T[P]["choices"] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: number;
|
||||
}
|
||||
: T[P] extends IntegerArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: T[P]["choices"] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? 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-expect-error somehow this check does not work
|
||||
? ConvertArgumentDefinitionsToArgs<T[P]["options"]>
|
||||
: {};
|
||||
}
|
||||
// SUBCOMMANDGROUP
|
||||
: T[P] extends SubcommandGroupArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: ConvertArgumentDefinitionsToArgs<T[P]["options"]>;
|
||||
}
|
||||
: never;
|
||||
}[number]
|
||||
>
|
||||
UnionToIntersection<
|
||||
{
|
||||
[P in keyof T]: T[P] extends StringOptionalArgumentDefinition<infer N> // STRING
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]?: T[P]['choices'] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: string;
|
||||
}
|
||||
: T[P] extends StringArgumentDefinition<infer N>
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]: T[P]['choices'] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: string;
|
||||
}
|
||||
: // INTEGER
|
||||
T[P] extends IntegerOptionalArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?: T[P]['choices'] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: number;
|
||||
}
|
||||
: T[P] extends IntegerArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]: T[P]['choices'] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? 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-expect-error somehow this check does not work
|
||||
? ConvertArgumentDefinitionsToArgs<T[P]['options']>
|
||||
: {};
|
||||
}
|
||||
: // SUBCOMMANDGROUP
|
||||
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: Bot, data: InteractionWithCustomProps, args: ConvertArgumentDefinitionsToArgs<T>) => unknown;
|
||||
subcommands?: Record<string, 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;
|
||||
/** 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: Bot, data: InteractionWithCustomProps, args: ConvertArgumentDefinitionsToArgs<T>) => unknown;
|
||||
subcommands?: Record<string, 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;
|
||||
|
||||
/** VIP only by default false */
|
||||
vipOnly?: boolean;
|
||||
/** VIP only by default false */
|
||||
vipOnly?: boolean;
|
||||
|
||||
advanced?: 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;
|
||||
/** 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?:
|
||||
| Array<keyof typeof PermissionLevelHandlers>
|
||||
| ((data: Interaction, command: Command<T>) => boolean | Promise<boolean>);
|
||||
botServerPermissions?: PermissionStrings[];
|
||||
botChannelPermissions?: PermissionStrings[];
|
||||
userServerPermissions?: PermissionStrings[];
|
||||
userChannelPermissions?: PermissionStrings[];
|
||||
permissionLevels?:
|
||||
| Array<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,
|
||||
Member,
|
||||
Moderator,
|
||||
Admin,
|
||||
ServerOwner,
|
||||
BotSupporter,
|
||||
BotDev,
|
||||
BotOwner,
|
||||
}
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
|
||||
export async function validateSlashLimits() {
|
||||
const MAX_ALLOWED_CHARACTERS = 4000;
|
||||
const MAX_ALLOWED_CHARACTERS = 4000;
|
||||
|
||||
const commands = await fetch("https://cmd-counter-play.deno.dev/", {
|
||||
body: JSON.stringify(COMMANDS),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}).then(async (res) => await res.json()).catch(() => undefined);
|
||||
const commands = await fetch('https://cmd-counter-play.deno.dev/', {
|
||||
body: JSON.stringify(COMMANDS),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(async (res) => await res.json())
|
||||
.catch(() => undefined);
|
||||
|
||||
if (!commands) return;
|
||||
if (!commands) return;
|
||||
|
||||
const invalidCommandNames: string[] = [];
|
||||
const invalidCommandNames: string[] = [];
|
||||
|
||||
if (commands[0]?.characters > MAX_ALLOWED_CHARACTERS) {
|
||||
for (const command of commands) {
|
||||
if (command.characters <= MAX_ALLOWED_CHARACTERS) continue;
|
||||
if (commands[0]?.characters > MAX_ALLOWED_CHARACTERS) {
|
||||
for (const command of commands) {
|
||||
if (command.characters <= MAX_ALLOWED_CHARACTERS) continue;
|
||||
|
||||
invalidCommandNames.push(command.name);
|
||||
console.log(
|
||||
`[Invalid Command] The ${command.name} is not a valid command. It's total characters are (${command.characters}) which is more than the max allowed ${MAX_ALLOWED_CHARACTERS}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
invalidCommandNames.push(command.name);
|
||||
console.log(
|
||||
`[Invalid Command] The ${command.name} is not a valid command. It's total characters are (${command.characters}) which is more than the max allowed ${MAX_ALLOWED_CHARACTERS}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCommandNames.length) throw new Error(`[Startup] Invalid commands: ${invalidCommandNames.join(", ")}`);
|
||||
if (invalidCommandNames.length) throw new Error(`[Startup] Invalid commands: ${invalidCommandNames.join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import { validatePermissions } from "discordeno/permissions-plugin";
|
||||
import type { Command } from "./createCommand.js";
|
||||
import type { Interaction } from 'discordeno';
|
||||
import { validatePermissions } from 'discordeno/permissions-plugin';
|
||||
import type { Command } from './createCommand.js';
|
||||
|
||||
export default async function hasPermissionLevel(command: Command<any>, payload: Interaction) {
|
||||
// This command doesnt require a perm level so allow the command.
|
||||
if (!command.permissionLevels) return true;
|
||||
// This command doesnt 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 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;
|
||||
}
|
||||
// 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;
|
||||
// None of the perm levels were met. So cancel the command
|
||||
return false;
|
||||
}
|
||||
|
||||
export const PermissionLevelHandlers: Record<
|
||||
keyof typeof PermissionLevels,
|
||||
(payload: Interaction, command: Command<any>) => boolean | Promise<boolean>
|
||||
keyof typeof PermissionLevels,
|
||||
(payload: Interaction, 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"]),
|
||||
// TODO(cache): fix this
|
||||
SERVER_OWNER: () => false,
|
||||
BOT_SUPPORT: () => false,
|
||||
BOT_DEVS: () => false,
|
||||
BOT_OWNERS: (payload) => [130136895395987456n, 615542460151496705n].includes(payload.user.id),
|
||||
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']),
|
||||
// TODO(cache): fix this
|
||||
SERVER_OWNER: () => false,
|
||||
BOT_SUPPORT: () => false,
|
||||
BOT_DEVS: () => false,
|
||||
BOT_OWNERS: (payload) => [130136895395987456n, 615542460151496705n].includes(payload.user.id),
|
||||
};
|
||||
|
||||
export enum PermissionLevels {
|
||||
MEMBER,
|
||||
MODERATOR,
|
||||
ADMIN,
|
||||
SERVER_OWNER,
|
||||
BOT_SUPPORT,
|
||||
BOT_DEVS,
|
||||
BOT_OWNERS,
|
||||
MEMBER,
|
||||
MODERATOR,
|
||||
ADMIN,
|
||||
SERVER_OWNER,
|
||||
BOT_SUPPORT,
|
||||
BOT_DEVS,
|
||||
BOT_OWNERS,
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import type { ApplicationCommandOption, Bot } from "discordeno";
|
||||
import { ApplicationCommandTypes } from "discordeno";
|
||||
import { prisma } from "../../../prisma.js";
|
||||
import { bot } from "../../bot.js";
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
import { serverLanguages, translate } from "../../languages/translate.js";
|
||||
import type { ArgumentDefinition } from "./createCommand.js";
|
||||
import type { ApplicationCommandOption, Bot } from 'discordeno';
|
||||
import { ApplicationCommandTypes } from 'discordeno';
|
||||
import { prisma } from '../../../prisma.js';
|
||||
import { bot } from '../../bot.js';
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
import { serverLanguages, translate } from '../../languages/translate.js';
|
||||
import type { ArgumentDefinition } from './createCommand.js';
|
||||
|
||||
const DEV_SERVER_ID = process.env.DEV_SERVER_ID as string;
|
||||
|
||||
export async function updateDevCommands(bot: Bot) {
|
||||
const cmds = Object.entries(COMMANDS)
|
||||
// ONLY DEV COMMANDS
|
||||
.filter(([_name, command]) => command?.dev);
|
||||
const cmds = Object.entries(COMMANDS)
|
||||
// ONLY DEV COMMANDS
|
||||
.filter(([_name, command]) => command?.dev);
|
||||
|
||||
if (!cmds.length) return;
|
||||
if (!cmds.length) return;
|
||||
|
||||
// DEV RELATED COMMANDS, USE upsertGlobalApplicationCommands TO UPDATE GLOBALLY
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
bot.transformers.snowflake(DEV_SERVER_ID),
|
||||
cmds.map(([name, command]) => {
|
||||
const translatedName = translate(DEV_SERVER_ID, command.name);
|
||||
const translatedDescription = command.description ? translate(DEV_SERVER_ID, command.description) : "";
|
||||
// DEV RELATED COMMANDS, USE upsertGlobalApplicationCommands TO UPDATE GLOBALLY
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
bot.transformers.snowflake(DEV_SERVER_ID),
|
||||
cmds.map(([name, command]) => {
|
||||
const translatedName = translate(DEV_SERVER_ID, command.name);
|
||||
const translatedDescription = command.description ? translate(DEV_SERVER_ID, command.description) : '';
|
||||
|
||||
if (command.type && command.type !== ApplicationCommandTypes.ChatInput) {
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
type: command.type,
|
||||
};
|
||||
}
|
||||
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.transformers.snowflake(DEV_SERVER_ID), command.options, command.name)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
description: translatedDescription || command.description,
|
||||
options: command.options
|
||||
? createOptions(bot.transformers.snowflake(DEV_SERVER_ID), command.options, command.name)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: You can make this able to be updated dynicamally by moving this value to something in the database and having a command to update it on the fly or as part of CI.
|
||||
@@ -45,63 +45,63 @@ export const CURRENT_SLASH_COMMAND_VERSION = 1;
|
||||
|
||||
/** Whether the guild has the latest slash command version */
|
||||
export async function usesLatestCommandVersion(guildId: bigint): Promise<boolean> {
|
||||
return (await getCurrentCommandVersion(guildId)) === CURRENT_SLASH_COMMAND_VERSION;
|
||||
return (await getCurrentCommandVersion(guildId)) === CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
/** Get the current slash command version for this guild */
|
||||
export async function getCurrentCommandVersion(guildId: bigint): Promise<number> {
|
||||
if (bot.commandVersions.has(guildId)) return bot.commandVersions.get(guildId)!;
|
||||
if (bot.commandVersions.has(guildId)) return bot.commandVersions.get(guildId)!;
|
||||
|
||||
const commandVersion = await prisma.commands.findUnique({ where: { id: guildId } });
|
||||
if (commandVersion) bot.commandVersions.set(guildId, commandVersion.version);
|
||||
const commandVersion = await prisma.commands.findUnique({ where: { id: guildId } });
|
||||
if (commandVersion) bot.commandVersions.set(guildId, commandVersion.version);
|
||||
|
||||
return commandVersion?.version ?? 0;
|
||||
return commandVersion?.version ?? 0;
|
||||
}
|
||||
|
||||
export async function updateCommandVersion(guildId: bigint): Promise<number> {
|
||||
// UPDATE THE VERSION SAVED IN THE DB
|
||||
await prisma.commands.upsert({
|
||||
where: { id: guildId },
|
||||
create: { id: guildId, version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
update: { version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
});
|
||||
// UPDATE THE VERSION SAVED IN THE DB
|
||||
await prisma.commands.upsert({
|
||||
where: { id: guildId },
|
||||
create: { id: guildId, version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
update: { version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
});
|
||||
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
export async function updateGuildCommands(bot: Bot, guildId: bigint) {
|
||||
if (guildId === 547046977578336286n) return await updateDevCommands(bot);
|
||||
if (guildId === 547046977578336286n) return await updateDevCommands(bot);
|
||||
|
||||
await updateCommandVersion(guildId);
|
||||
await updateCommandVersion(guildId);
|
||||
|
||||
// GUILD RELATED COMMANDS
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
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("english", command.description),
|
||||
options: command.options ? createOptions("english", command.options, command.name) : undefined,
|
||||
};
|
||||
}
|
||||
// GUILD RELATED COMMANDS
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
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('english', command.description),
|
||||
options: command.options ? createOptions('english', command.options, command.name) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(guildId, command.name);
|
||||
const translatedDescription = translate(guildId, command.description);
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(guildId, command.name);
|
||||
const translatedDescription = translate(guildId, command.description);
|
||||
|
||||
return {
|
||||
name: translatedName.toLowerCase(),
|
||||
description: translatedDescription,
|
||||
options: command.options ? createOptions(guildId, command.options, command.name) : undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return {
|
||||
name: translatedName.toLowerCase(),
|
||||
description: translatedDescription,
|
||||
options: command.options ? createOptions(guildId, command.options, command.name) : undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// USED TO CACHE CONVERTED COMMANDS AFTER START TO PREVENT UNNECESSARY LOOPS
|
||||
@@ -109,41 +109,41 @@ const convertedCache = new Map<string, ApplicationCommandOption[]>();
|
||||
|
||||
/** Creates the commands options including subcommands. Also translates them. */
|
||||
function createOptions(
|
||||
guildId: bigint | "english",
|
||||
options: readonly ArgumentDefinition[],
|
||||
commandName?: string,
|
||||
guildId: bigint | 'english',
|
||||
options: readonly 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 language = guildId === 'english' ? 'english' : serverLanguages.get(guildId) ?? 'english';
|
||||
if (commandName && convertedCache.has(`${language}-${commandName}`)) {
|
||||
return convertedCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
const newOptions: ApplicationCommandOption[] = [];
|
||||
const newOptions: ApplicationCommandOption[] = [];
|
||||
|
||||
for (const option of options || []) {
|
||||
const optionName = translate(guildId, option.name);
|
||||
const optionDescription = translate(guildId, option.description);
|
||||
for (const option of options || []) {
|
||||
const optionName = translate(guildId, option.name);
|
||||
const optionDescription = translate(guildId, option.description);
|
||||
|
||||
// TODO: remove this ts ignore
|
||||
// @ts-expect-error
|
||||
const choices = option.choices?.map((choice) => ({
|
||||
...choice,
|
||||
name: translate(guildId, choice.name),
|
||||
}));
|
||||
// TODO: remove this ts ignore
|
||||
// @ts-expect-error
|
||||
const choices = option.choices?.map((choice) => ({
|
||||
...choice,
|
||||
name: translate(guildId, choice.name),
|
||||
}));
|
||||
|
||||
newOptions.push({
|
||||
...option,
|
||||
name: optionName.toLowerCase(),
|
||||
description: optionDescription || "No description available.",
|
||||
choices,
|
||||
// @ts-expect-error fix this
|
||||
options: option.options
|
||||
// @ts-expect-error fix this
|
||||
? createOptions(bot, guildId, option.options)
|
||||
: undefined,
|
||||
} as ApplicationCommandOption);
|
||||
}
|
||||
if (commandName) convertedCache.set(`${language}-${commandName}`, newOptions);
|
||||
newOptions.push({
|
||||
...option,
|
||||
name: optionName.toLowerCase(),
|
||||
description: optionDescription || 'No description available.',
|
||||
choices,
|
||||
// @ts-expect-error fix this
|
||||
options: option.options
|
||||
? // @ts-expect-error fix this
|
||||
createOptions(bot, guildId, option.options)
|
||||
: undefined,
|
||||
} as ApplicationCommandOption);
|
||||
}
|
||||
if (commandName) convertedCache.set(`${language}-${commandName}`, newOptions);
|
||||
|
||||
return newOptions;
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/** Get the webhook id and token from a webhook url. */
|
||||
export function webhookURLToIDAndToken(url: string) {
|
||||
const [id, token] = url.substring(url.indexOf("webhooks/") + 9).split(
|
||||
"/",
|
||||
);
|
||||
const [id, token] = url.substring(url.indexOf('webhooks/') + 9).split('/');
|
||||
|
||||
return {
|
||||
id,
|
||||
token,
|
||||
};
|
||||
return {
|
||||
id,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBotIdFromToken, Intents } from "discordeno";
|
||||
import dotenv from "dotenv";
|
||||
import { getBotIdFromToken, Intents } from 'discordeno';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
/** The bot id, derived from the bot token. */
|
||||
@@ -11,19 +11,19 @@ export const GATEWAY_URL = `http://${process.env.GATEWAY_HOST}:${process.env.GAT
|
||||
// Gateway Proxy Configurations
|
||||
/** The gateway intents you would like to use. */
|
||||
export const INTENTS: Intents =
|
||||
// SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
|
||||
Intents.DirectMessageReactions |
|
||||
Intents.DirectMessageTyping |
|
||||
Intents.DirectMessages |
|
||||
Intents.GuildBans |
|
||||
Intents.GuildEmojis |
|
||||
Intents.GuildIntegrations |
|
||||
Intents.GuildInvites |
|
||||
Intents.GuildMembers |
|
||||
Intents.GuildMessageReactions |
|
||||
Intents.GuildMessageTyping |
|
||||
Intents.GuildMessages |
|
||||
Intents.GuildPresences |
|
||||
Intents.GuildVoiceStates |
|
||||
Intents.GuildWebhooks |
|
||||
Intents.Guilds;
|
||||
// SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
|
||||
Intents.DirectMessageReactions |
|
||||
Intents.DirectMessageTyping |
|
||||
Intents.DirectMessages |
|
||||
Intents.GuildBans |
|
||||
Intents.GuildEmojis |
|
||||
Intents.GuildIntegrations |
|
||||
Intents.GuildInvites |
|
||||
Intents.GuildMembers |
|
||||
Intents.GuildMessageReactions |
|
||||
Intents.GuildMessageTyping |
|
||||
Intents.GuildMessages |
|
||||
Intents.GuildPresences |
|
||||
Intents.GuildVoiceStates |
|
||||
Intents.GuildWebhooks |
|
||||
Intents.Guilds;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import dotenv from "dotenv";
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { Collection, createBot, createGatewayManager, createRestManager } from "discordeno";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import fastify from "fastify";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Worker } from "worker_threads";
|
||||
import { EVENT_HANDLER_URL, INTENTS, REST_URL } from "../configs.js";
|
||||
import type { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.js";
|
||||
import { Collection, createBot, createGatewayManager, createRestManager } from 'discordeno';
|
||||
import { createLogger } from 'discordeno/logger';
|
||||
import fastify from 'fastify';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { EVENT_HANDLER_URL, INTENTS, REST_URL } from '../configs.js';
|
||||
import type {
|
||||
WorkerCreateData,
|
||||
WorkerGetShardInfo,
|
||||
WorkerMessage,
|
||||
WorkerShardInfo,
|
||||
WorkerShardPayload,
|
||||
} from './worker.js';
|
||||
dotenv.config();
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
@@ -18,181 +24,181 @@ const SHARDS_PER_WORKER = Number(process.env.SHARDS_PER_WORKER as string);
|
||||
const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? Number(process.env.TOTAL_SHARDS) : undefined;
|
||||
const TOTAL_WORKERS = Number(process.env.TOTAL_WORKERS as string);
|
||||
|
||||
const log = createLogger({ name: "[MANAGER]" });
|
||||
const log = createLogger({ name: '[MANAGER]' });
|
||||
|
||||
const bot = createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
token: DISCORD_TOKEN,
|
||||
});
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
});
|
||||
|
||||
const gatewayBot = await bot.helpers.getGatewayBot();
|
||||
|
||||
const gateway = createGatewayManager({
|
||||
gatewayBot,
|
||||
gatewayConfig: {
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
},
|
||||
// force the total amount of shards
|
||||
totalShards: TOTAL_SHARDS,
|
||||
shardsPerWorker: SHARDS_PER_WORKER,
|
||||
totalWorkers: TOTAL_WORKERS,
|
||||
gatewayBot,
|
||||
gatewayConfig: {
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
},
|
||||
// force the total amount of shards
|
||||
totalShards: TOTAL_SHARDS,
|
||||
shardsPerWorker: SHARDS_PER_WORKER,
|
||||
totalWorkers: TOTAL_WORKERS,
|
||||
|
||||
handleDiscordPayload: () => {},
|
||||
handleDiscordPayload: () => {},
|
||||
|
||||
tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => {
|
||||
log.info("TELL TO IDENTIFY", { workerId, shardId, _bucketId });
|
||||
tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => {
|
||||
log.info('TELL TO IDENTIFY', { workerId, shardId, _bucketId });
|
||||
|
||||
let worker = workers.get(workerId);
|
||||
if (!worker) {
|
||||
worker = createWorker(workerId);
|
||||
workers.set(workerId, worker);
|
||||
}
|
||||
let worker = workers.get(workerId);
|
||||
if (!worker) {
|
||||
worker = createWorker(workerId);
|
||||
workers.set(workerId, worker);
|
||||
}
|
||||
|
||||
const identify: WorkerMessage = {
|
||||
type: "IDENTIFY_SHARD",
|
||||
shardId,
|
||||
};
|
||||
const identify: WorkerMessage = {
|
||||
type: 'IDENTIFY_SHARD',
|
||||
shardId,
|
||||
};
|
||||
|
||||
worker.postMessage(identify);
|
||||
},
|
||||
worker.postMessage(identify);
|
||||
},
|
||||
});
|
||||
|
||||
const workers = new Collection<number, Worker>();
|
||||
const nonces = new Collection<string, (data: any) => void>();
|
||||
|
||||
function createWorker(workerId: number) {
|
||||
console.log(TOTAL_SHARDS, gateway.manager.totalShards, "SHARDS");
|
||||
console.log(TOTAL_SHARDS, gateway.manager.totalShards, 'SHARDS');
|
||||
|
||||
const workerData: WorkerCreateData = {
|
||||
intents: gateway.manager.gatewayConfig.intents ?? 0,
|
||||
token: DISCORD_TOKEN,
|
||||
handlerUrls: [EVENT_HANDLER_URL],
|
||||
handlerAuthorization: EVENT_HANDLER_AUTHORIZATION,
|
||||
path: "./worker.ts",
|
||||
totalShards: gateway.manager.totalShards,
|
||||
workerId,
|
||||
};
|
||||
const workerData: WorkerCreateData = {
|
||||
intents: gateway.manager.gatewayConfig.intents ?? 0,
|
||||
token: DISCORD_TOKEN,
|
||||
handlerUrls: [EVENT_HANDLER_URL],
|
||||
handlerAuthorization: EVENT_HANDLER_AUTHORIZATION,
|
||||
path: './worker.ts',
|
||||
totalShards: gateway.manager.totalShards,
|
||||
workerId,
|
||||
};
|
||||
|
||||
const worker = new Worker("./dist/gateway/worker.js", {
|
||||
workerData,
|
||||
});
|
||||
const worker = new Worker('./dist/gateway/worker.js', {
|
||||
workerData,
|
||||
});
|
||||
|
||||
worker.on("message", async (data: ManagerMessage) => {
|
||||
log.info({ data });
|
||||
switch (data.type) {
|
||||
case "REQUEST_IDENTIFY": {
|
||||
log.info("REQUESTING IDENTIFY #", data.shardId);
|
||||
await gateway.manager.requestIdentify(data.shardId);
|
||||
worker.on('message', async (data: ManagerMessage) => {
|
||||
log.info({ data });
|
||||
switch (data.type) {
|
||||
case 'REQUEST_IDENTIFY': {
|
||||
log.info('REQUESTING IDENTIFY #', data.shardId);
|
||||
await gateway.manager.requestIdentify(data.shardId);
|
||||
|
||||
const allowIdentify: WorkerMessage = {
|
||||
type: "ALLOW_IDENTIFY",
|
||||
shardId: data.shardId,
|
||||
};
|
||||
const allowIdentify: WorkerMessage = {
|
||||
type: 'ALLOW_IDENTIFY',
|
||||
shardId: data.shardId,
|
||||
};
|
||||
|
||||
worker.postMessage(allowIdentify);
|
||||
worker.postMessage(allowIdentify);
|
||||
|
||||
break;
|
||||
}
|
||||
case "NONCE_REPLY": {
|
||||
nonces.get(data.nonce)?.(data.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'NONCE_REPLY': {
|
||||
nonces.get(data.nonce)?.(data.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
gateway.spawnShards();
|
||||
|
||||
const server = fastify();
|
||||
|
||||
server.post("/", async (request, reply) => {
|
||||
if (request.headers.authorization !== GATEWAY_AUTHORIZATION) {
|
||||
reply.code(StatusCodes.Unauthorized);
|
||||
server.post('/', async (request, reply) => {
|
||||
if (request.headers.authorization !== GATEWAY_AUTHORIZATION) {
|
||||
reply.code(StatusCodes.Unauthorized);
|
||||
|
||||
return reply.send({ processing: false, error: false, message: "Invalid authorization header." });
|
||||
}
|
||||
return reply.send({ processing: false, error: false, message: 'Invalid authorization header.' });
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
if (!request.body) {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
|
||||
return reply.send({ processing: false, error: false, message: "Empty body." });
|
||||
}
|
||||
return reply.send({ processing: false, error: false, message: 'Empty body.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = request.body as WorkerShardPayload | Omit<WorkerGetShardInfo, "nonce">;
|
||||
switch (data.type) {
|
||||
case "SHARD_PAYLOAD": {
|
||||
const workerId = gateway.calculateWorkerId(data.shardId);
|
||||
const worker = workers.get(workerId);
|
||||
try {
|
||||
const data = request.body as WorkerShardPayload | Omit<WorkerGetShardInfo, 'nonce'>;
|
||||
switch (data.type) {
|
||||
case 'SHARD_PAYLOAD': {
|
||||
const workerId = gateway.calculateWorkerId(data.shardId);
|
||||
const worker = workers.get(workerId);
|
||||
|
||||
worker?.postMessage(data);
|
||||
worker?.postMessage(data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "GET_SHARD_INFO": {
|
||||
const infos = await Promise.all(
|
||||
workers.map(async (worker) => {
|
||||
const nonce = nanoid();
|
||||
break;
|
||||
}
|
||||
case 'GET_SHARD_INFO': {
|
||||
const infos = await Promise.all(
|
||||
workers.map(async (worker) => {
|
||||
const nonce = nanoid();
|
||||
|
||||
return await new Promise<WorkerShardInfo[]>((resolve) => {
|
||||
worker.postMessage({ type: "GET_SHARD_INFO", nonce });
|
||||
return await new Promise<WorkerShardInfo[]>((resolve) => {
|
||||
worker.postMessage({ type: 'GET_SHARD_INFO', nonce });
|
||||
|
||||
nonces.set(nonce, resolve);
|
||||
});
|
||||
}),
|
||||
).then((res) =>
|
||||
res.reduce((acc, cur) => {
|
||||
acc.push(...cur);
|
||||
return acc;
|
||||
}, [] as WorkerShardInfo[])
|
||||
);
|
||||
nonces.set(nonce, resolve);
|
||||
});
|
||||
}),
|
||||
).then((res) =>
|
||||
res.reduce((acc, cur) => {
|
||||
acc.push(...cur);
|
||||
return acc;
|
||||
}, [] as WorkerShardInfo[]),
|
||||
);
|
||||
|
||||
reply.code(StatusCodes.Ok);
|
||||
reply.code(StatusCodes.Ok);
|
||||
|
||||
return reply.send(infos);
|
||||
}
|
||||
}
|
||||
return reply.send(infos);
|
||||
}
|
||||
}
|
||||
|
||||
reply.code(StatusCodes.Ok);
|
||||
reply.code(StatusCodes.Ok);
|
||||
|
||||
return reply.send({ processing: true });
|
||||
} catch {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
return reply.send({ processing: true });
|
||||
} catch {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
|
||||
return reply.send({ processing: false, error: true, message: "Failed to parse body." });
|
||||
}
|
||||
return reply.send({ processing: false, error: true, message: 'Failed to parse body.' });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen({ port: GATEWAY_PORT }).catch((error) => {
|
||||
log.error(["[FASTIFY ERROR", error].join("\n"));
|
||||
process.exit(1);
|
||||
log.error(['[FASTIFY ERROR', error].join('\n'));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply<WorkerShardInfo[]>;
|
||||
|
||||
export interface ManagerRequestIdentify {
|
||||
type: "REQUEST_IDENTIFY";
|
||||
shardId: number;
|
||||
type: 'REQUEST_IDENTIFY';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface ManagerNonceReply<T> {
|
||||
type: "NONCE_REPLY";
|
||||
nonce: string;
|
||||
data: T;
|
||||
type: 'NONCE_REPLY';
|
||||
nonce: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
enum StatusCodes {
|
||||
Ok = 200,
|
||||
Ok = 200,
|
||||
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
|
||||
InternalServerError = 500,
|
||||
InternalServerError = 500,
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import dotenv from "dotenv";
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import amqplib from "amqplib";
|
||||
import amqplib from 'amqplib';
|
||||
import type {
|
||||
DiscordGuild,
|
||||
DiscordReady,
|
||||
DiscordUnavailableGuild,
|
||||
Shard,
|
||||
ShardSocketRequest,
|
||||
ShardState} from "discordeno";
|
||||
import {
|
||||
createShardManager,
|
||||
GatewayEventNames
|
||||
} from "discordeno";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import fetch from "node-fetch";
|
||||
import crypto from "node:crypto";
|
||||
import { parentPort, workerData } from "worker_threads";
|
||||
DiscordGuild,
|
||||
DiscordReady,
|
||||
DiscordUnavailableGuild,
|
||||
Shard,
|
||||
ShardSocketRequest,
|
||||
ShardState,
|
||||
} from 'discordeno';
|
||||
import { createShardManager, GatewayEventNames } from 'discordeno';
|
||||
import { createLogger } from 'discordeno/logger';
|
||||
import fetch from 'node-fetch';
|
||||
import crypto from 'node:crypto';
|
||||
import { parentPort, workerData } from 'worker_threads';
|
||||
import type { ManagerMessage } from './index.js';
|
||||
dotenv.config();
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error("Parent port is null");
|
||||
throw new Error('Parent port is null');
|
||||
}
|
||||
|
||||
const script: WorkerCreateData = workerData;
|
||||
@@ -31,211 +29,202 @@ const identifyPromises = new Map<number, () => void>();
|
||||
|
||||
let channel: amqplib.Channel | undefined;
|
||||
|
||||
const useMessageQueue = process.env.MESSAGEQUEUE_ENABLE === "true";
|
||||
const useMessageQueue = process.env.MESSAGEQUEUE_ENABLE === 'true';
|
||||
|
||||
// Store guild ids, loading guild ids to change GUILD_CREATE event to GUILD_LOADED_DD if needed.
|
||||
const guildIds: Set<bigint> = new Set();
|
||||
const loadingGuildIds: Set<bigint> = new Set();
|
||||
|
||||
const manager = createShardManager({
|
||||
gatewayConfig: {
|
||||
intents: script.intents,
|
||||
token: script.token,
|
||||
},
|
||||
shardIds: [],
|
||||
totalShards: script.totalShards,
|
||||
handleMessage: async (shard, message) => {
|
||||
const url = script.handlerUrls[shard.id % script.handlerUrls.length];
|
||||
if (!url) return console.log("ERROR: NO URL FOUND TO SEND MESSAGE");
|
||||
gatewayConfig: {
|
||||
intents: script.intents,
|
||||
token: script.token,
|
||||
},
|
||||
shardIds: [],
|
||||
totalShards: script.totalShards,
|
||||
handleMessage: async (shard, message) => {
|
||||
const url = script.handlerUrls[shard.id % script.handlerUrls.length];
|
||||
if (!url) return console.log('ERROR: NO URL FOUND TO SEND MESSAGE');
|
||||
|
||||
if (message.t === "READY") {
|
||||
// Marks which guilds the bot in when initial loading in cache.
|
||||
(message.d as DiscordReady).guilds.forEach((g) => loadingGuildIds.add(BigInt(g.id)));
|
||||
}
|
||||
if (message.t === 'READY') {
|
||||
// Marks which guilds the bot in when initial loading in cache.
|
||||
(message.d as DiscordReady).guilds.forEach((g) => loadingGuildIds.add(BigInt(g.id)));
|
||||
}
|
||||
|
||||
// If GUILD_CREATE event came from a shard loaded event, change event to GUILD_LOADED_DD.
|
||||
if (message.t === "GUILD_CREATE") {
|
||||
const guild = message.d as DiscordGuild;
|
||||
const id = BigInt(guild.id);
|
||||
// If GUILD_CREATE event came from a shard loaded event, change event to GUILD_LOADED_DD.
|
||||
if (message.t === 'GUILD_CREATE') {
|
||||
const guild = message.d as DiscordGuild;
|
||||
const id = BigInt(guild.id);
|
||||
|
||||
const existing = guildIds.has(id);
|
||||
if (existing) return;
|
||||
const existing = guildIds.has(id);
|
||||
if (existing) return;
|
||||
|
||||
if (loadingGuildIds.has(id)) {
|
||||
(message.t ) = "GUILD_LOADED_DD";
|
||||
if (loadingGuildIds.has(id)) {
|
||||
message.t = 'GUILD_LOADED_DD';
|
||||
|
||||
loadingGuildIds.delete(id);
|
||||
}
|
||||
loadingGuildIds.delete(id);
|
||||
}
|
||||
|
||||
guildIds.add(id);
|
||||
}
|
||||
guildIds.add(id);
|
||||
}
|
||||
|
||||
// Delete guild id from cache so GUILD_CREATE from the same guild later works properly.
|
||||
if (message.t === "GUILD_DELETE") {
|
||||
const guild = message.d as DiscordUnavailableGuild;
|
||||
// Delete guild id from cache so GUILD_CREATE from the same guild later works properly.
|
||||
if (message.t === 'GUILD_DELETE') {
|
||||
const guild = message.d as DiscordUnavailableGuild;
|
||||
|
||||
if (guild.unavailable) return;
|
||||
if (guild.unavailable) return;
|
||||
|
||||
guildIds.delete(BigInt(guild.id));
|
||||
}
|
||||
guildIds.delete(BigInt(guild.id));
|
||||
}
|
||||
|
||||
if (useMessageQueue) {
|
||||
if (!channel) return;
|
||||
await channel.publish(
|
||||
"gatewayMessage",
|
||||
"",
|
||||
Buffer.from(JSON.stringify({ shard, message })),
|
||||
{
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
"x-deduplication-header": crypto.createHash("md5").update(JSON.stringify(message.d)).digest("hex"),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message, shardId: shard.id }),
|
||||
headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
|
||||
}).catch((error) => log.error(error));
|
||||
}
|
||||
if (useMessageQueue) {
|
||||
if (!channel) return;
|
||||
await channel.publish('gatewayMessage', '', Buffer.from(JSON.stringify({ shard, message })), {
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'x-deduplication-header': crypto.createHash('md5').update(JSON.stringify(message.d)).digest('hex'),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ message, shardId: shard.id }),
|
||||
headers: { 'Content-Type': 'application/json', Authorization: script.handlerAuthorization },
|
||||
}).catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
log.debug({ shardId: shard.id, message });
|
||||
},
|
||||
requestIdentify: async function (shardId: number): Promise<void> {
|
||||
return await new Promise((resolve) => {
|
||||
identifyPromises.set(shardId, resolve);
|
||||
log.debug({ shardId: shard.id, message });
|
||||
},
|
||||
requestIdentify: async function (shardId: number): Promise<void> {
|
||||
return await new Promise((resolve) => {
|
||||
identifyPromises.set(shardId, resolve);
|
||||
|
||||
const identifyRequest: ManagerMessage = {
|
||||
type: "REQUEST_IDENTIFY",
|
||||
shardId,
|
||||
};
|
||||
const identifyRequest: ManagerMessage = {
|
||||
type: 'REQUEST_IDENTIFY',
|
||||
shardId,
|
||||
};
|
||||
|
||||
parentPort?.postMessage(identifyRequest);
|
||||
});
|
||||
},
|
||||
parentPort?.postMessage(identifyRequest);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function buildShardInfo(shard: Shard): WorkerShardInfo {
|
||||
return {
|
||||
workerId: script.workerId,
|
||||
shardId: shard.id,
|
||||
rtt: shard.heart.rtt || -1,
|
||||
state: shard.state,
|
||||
};
|
||||
return {
|
||||
workerId: script.workerId,
|
||||
shardId: shard.id,
|
||||
rtt: shard.heart.rtt || -1,
|
||||
state: shard.state,
|
||||
};
|
||||
}
|
||||
|
||||
parentPort.on("message", async (data: WorkerMessage) => {
|
||||
switch (data.type) {
|
||||
case "IDENTIFY_SHARD": {
|
||||
log.info(`starting to identify shard #${data.shardId}`);
|
||||
await manager.identify(data.shardId);
|
||||
parentPort.on('message', async (data: WorkerMessage) => {
|
||||
switch (data.type) {
|
||||
case 'IDENTIFY_SHARD': {
|
||||
log.info(`starting to identify shard #${data.shardId}`);
|
||||
await manager.identify(data.shardId);
|
||||
|
||||
break;
|
||||
}
|
||||
case "ALLOW_IDENTIFY": {
|
||||
identifyPromises.get(data.shardId)?.();
|
||||
identifyPromises.delete(data.shardId);
|
||||
break;
|
||||
}
|
||||
case 'ALLOW_IDENTIFY': {
|
||||
identifyPromises.get(data.shardId)?.();
|
||||
identifyPromises.delete(data.shardId);
|
||||
|
||||
break;
|
||||
}
|
||||
case "SHARD_PAYLOAD": {
|
||||
manager.shards.get(data.shardId)?.send(data.data);
|
||||
break;
|
||||
}
|
||||
case 'SHARD_PAYLOAD': {
|
||||
manager.shards.get(data.shardId)?.send(data.data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "GET_SHARD_INFO": {
|
||||
const infos = manager.shards.map(buildShardInfo);
|
||||
break;
|
||||
}
|
||||
case 'GET_SHARD_INFO': {
|
||||
const infos = manager.shards.map(buildShardInfo);
|
||||
|
||||
parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
|
||||
}
|
||||
}
|
||||
parentPort?.postMessage({ type: 'NONCE_REPLY', nonce: data.nonce, data: infos });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;
|
||||
|
||||
export interface WorkerIdentifyShard {
|
||||
type: "IDENTIFY_SHARD";
|
||||
shardId: number;
|
||||
type: 'IDENTIFY_SHARD';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerAllowIdentify {
|
||||
type: "ALLOW_IDENTIFY";
|
||||
shardId: number;
|
||||
type: 'ALLOW_IDENTIFY';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerShardPayload {
|
||||
type: "SHARD_PAYLOAD";
|
||||
shardId: number;
|
||||
data: ShardSocketRequest;
|
||||
type: 'SHARD_PAYLOAD';
|
||||
shardId: number;
|
||||
data: ShardSocketRequest;
|
||||
}
|
||||
|
||||
export interface WorkerGetShardInfo {
|
||||
type: "GET_SHARD_INFO";
|
||||
nonce: string;
|
||||
type: 'GET_SHARD_INFO';
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export interface WorkerCreateData {
|
||||
intents: number;
|
||||
token: string;
|
||||
handlerUrls: string[];
|
||||
handlerAuthorization: string;
|
||||
path: string;
|
||||
totalShards: number;
|
||||
workerId: number;
|
||||
intents: number;
|
||||
token: string;
|
||||
handlerUrls: string[];
|
||||
handlerAuthorization: string;
|
||||
path: string;
|
||||
totalShards: number;
|
||||
workerId: number;
|
||||
}
|
||||
|
||||
export interface WorkerShardInfo {
|
||||
workerId: number;
|
||||
shardId: number;
|
||||
rtt: number;
|
||||
state: ShardState;
|
||||
workerId: number;
|
||||
shardId: number;
|
||||
rtt: number;
|
||||
state: ShardState;
|
||||
}
|
||||
|
||||
const connectRabbitmq = async () => {
|
||||
let connection: amqplib.Connection | undefined;
|
||||
let connection: amqplib.Connection | undefined;
|
||||
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
channel = undefined;
|
||||
log.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
channel = undefined;
|
||||
log.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
|
||||
if (!connection) return;
|
||||
connection.on("error", (err) => {
|
||||
channel = undefined;
|
||||
log.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
if (!connection) return;
|
||||
connection.on('error', (err) => {
|
||||
channel = undefined;
|
||||
log.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
connection.on("close", () => {
|
||||
channel = undefined;
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
connection.on('close', () => {
|
||||
channel = undefined;
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
channel = await connection.createChannel();
|
||||
await channel.assertExchange(
|
||||
"gatewayMessage",
|
||||
"x-message-deduplication",
|
||||
{
|
||||
durable: true,
|
||||
arguments: {
|
||||
"x-cache-size": 1000,
|
||||
"x-cache-ttl": 500,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
channel = undefined;
|
||||
}
|
||||
try {
|
||||
channel = await connection.createChannel();
|
||||
await channel.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-cache-size': 1000,
|
||||
'x-cache-ttl': 500,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
channel = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (useMessageQueue) {
|
||||
connectRabbitmq();
|
||||
connectRabbitmq();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import dotenv from "dotenv";
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { BASE_URL, createRestManager } from "discordeno";
|
||||
import express from "express";
|
||||
import { setupAnalyticsHooks } from "../analytics.js";
|
||||
import { REST_URL } from "../configs.js";
|
||||
import { BASE_URL, createRestManager } from 'discordeno';
|
||||
import express from 'express';
|
||||
import { setupAnalyticsHooks } from '../analytics.js';
|
||||
import { REST_URL } from '../configs.js';
|
||||
dotenv.config();
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
@@ -11,10 +11,10 @@ const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
const REST_PORT = process.env.REST_PORT as string;
|
||||
|
||||
const rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
debug: console.log,
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
debug: console.log,
|
||||
});
|
||||
|
||||
// Add send fetching analytics hook to rest
|
||||
@@ -22,39 +22,39 @@ setupAnalyticsHooks(rest);
|
||||
|
||||
// @ts-expect-error
|
||||
rest.convertRestError = (errorStack, data) => {
|
||||
if (!data) return { message: errorStack.message };
|
||||
return { ...data, message: errorStack.message };
|
||||
if (!data) return { message: errorStack.message };
|
||||
return { ...data, message: errorStack.message };
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all("/*", async (req, res) => {
|
||||
if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: "Invalid authorization key." });
|
||||
}
|
||||
app.all('/*', async (req, res) => {
|
||||
if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: 'Invalid authorization key.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await rest.runMethod(rest, req.method , `${BASE_URL}${req.url}`, req.body);
|
||||
try {
|
||||
const result = await rest.runMethod(rest, req.method, `${BASE_URL}${req.url}`, req.body);
|
||||
|
||||
if (result) {
|
||||
res.status(200).json(result);
|
||||
} else {
|
||||
res.status(204).json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
if (result) {
|
||||
res.status(200).json(result);
|
||||
} else {
|
||||
res.status(204).json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(REST_PORT, () => {
|
||||
console.log(`REST listening at ${REST_URL}`);
|
||||
console.log(`REST listening at ${REST_URL}`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dotEnvConfig } from './deps.ts.js';
|
||||
import { dotEnvConfig } from './deps.ts.js'
|
||||
|
||||
dotEnvConfig({ export: true });
|
||||
export const BOT_TOKEN = process.env.BOT_TOKEN || "";
|
||||
export const BOT_ID = BigInt(atob(BOT_TOKEN.split(".")[0]));
|
||||
dotEnvConfig({ export: true })
|
||||
export const BOT_TOKEN = process.env.BOT_TOKEN || ''
|
||||
export const BOT_ID = BigInt(atob(BOT_TOKEN.split('.')[0]))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/mod.ts";
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/plugins/mod.ts";
|
||||
export { config as dotEnvConfig } from "https://deno.land/x/dotenv@v3.1.0/mod.ts";
|
||||
export * from "https://deno.land/std@0.117.0/fmt/colors.ts";
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/mod.ts'
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/plugins/mod.ts'
|
||||
export { config as dotEnvConfig } from 'https://deno.land/x/dotenv@v3.1.0/mod.ts'
|
||||
export * from 'https://deno.land/std@0.117.0/fmt/colors.ts'
|
||||
|
||||
+20
-28
@@ -1,27 +1,19 @@
|
||||
import {
|
||||
ActivityTypes,
|
||||
createBot,
|
||||
enableCachePlugin,
|
||||
enableCacheSweepers,
|
||||
fastFileLoader,
|
||||
GatewayIntents,
|
||||
startBot,
|
||||
} from './deps.ts.js';
|
||||
import { BOT_ID, BOT_TOKEN } from './configs.ts.js';
|
||||
import { logger } from './src/utils/logger.ts.js';
|
||||
import { events } from './src/events/mod.ts.js';
|
||||
import { updateCommands } from './src/utils/helpers.ts.js';
|
||||
import { ActivityTypes, createBot, enableCachePlugin, enableCacheSweepers, fastFileLoader, GatewayIntents, startBot } from './deps.ts.js'
|
||||
import { BOT_ID, BOT_TOKEN } from './configs.ts.js'
|
||||
import { logger } from './src/utils/logger.ts.js'
|
||||
import { events } from './src/events/mod.ts.js'
|
||||
import { updateCommands } from './src/utils/helpers.ts.js'
|
||||
|
||||
const log = logger({ name: "Main" });
|
||||
const log = logger({ name: 'Main' })
|
||||
|
||||
log.info("Starting Bot, this might take a while...");
|
||||
log.info('Starting Bot, this might take a while...')
|
||||
|
||||
const paths = ["./src/events", "./src/commands"];
|
||||
const paths = ['./src/events', './src/commands']
|
||||
await fastFileLoader(paths).catch((err) => {
|
||||
log.fatal(`Unable to Import ${paths}`);
|
||||
log.fatal(err);
|
||||
Deno.exit(1);
|
||||
});
|
||||
log.fatal(`Unable to Import ${paths}`)
|
||||
log.fatal(err)
|
||||
Deno.exit(1)
|
||||
})
|
||||
|
||||
export const bot = enableCachePlugin(
|
||||
createBot({
|
||||
@@ -30,25 +22,25 @@ export const bot = enableCachePlugin(
|
||||
intents: GatewayIntents.Guilds,
|
||||
events,
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
// @ts-nocheck: no-updated-depencdencies
|
||||
enableCacheSweepers(bot);
|
||||
enableCacheSweepers(bot)
|
||||
|
||||
bot.gateway.manager.createShardOptions.makePresence = (shardId: number) => {
|
||||
return {
|
||||
shardId,
|
||||
status: "online",
|
||||
status: 'online',
|
||||
activities: [
|
||||
{
|
||||
name: "Discordeno is the Best Lib",
|
||||
name: 'Discordeno is the Best Lib',
|
||||
type: ActivityTypes.Game,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await startBot(bot);
|
||||
await startBot(bot)
|
||||
|
||||
await updateCommands(bot);
|
||||
await updateCommands(bot)
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Bot, Interaction } from '../../deps.ts.js';
|
||||
import { Collection } from '../../deps.ts.js';
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Bot, Interaction } from '../../deps.ts.js'
|
||||
import { Collection } from '../../deps.ts.js'
|
||||
|
||||
export type subCommand = Omit<Command, "subcommands">;
|
||||
export type subCommand = Omit<Command, 'subcommands'>
|
||||
export interface subCommandGroup {
|
||||
name: string;
|
||||
subCommands: subCommand[];
|
||||
name: string
|
||||
subCommands: subCommand[]
|
||||
}
|
||||
export interface Command {
|
||||
name: string;
|
||||
description: string;
|
||||
usage?: string[];
|
||||
options?: ApplicationCommandOption[];
|
||||
type: ApplicationCommandTypes;
|
||||
name: string
|
||||
description: string
|
||||
usage?: string[]
|
||||
options?: ApplicationCommandOption[]
|
||||
type: ApplicationCommandTypes
|
||||
/** Defaults to `Guild` */
|
||||
scope?: "Global" | "Guild";
|
||||
execute: (bot: Bot, interaction: Interaction) => unknown;
|
||||
subcommands?: Array<subCommandGroup | subCommand>;
|
||||
scope?: 'Global' | 'Guild'
|
||||
execute: (bot: Bot, interaction: Interaction) => unknown
|
||||
subcommands?: Array<subCommandGroup | subCommand>
|
||||
}
|
||||
|
||||
export const commands = new Collection<string, Command>();
|
||||
export const commands = new Collection<string, Command>()
|
||||
|
||||
export function createCommand(command: Command) {
|
||||
commands.set(command.name, command);
|
||||
commands.set(command.name, command)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js';
|
||||
import { humanizeMilliseconds, snowflakeToTimestamp } from '../utils/helpers.ts.js';
|
||||
import { createCommand } from './mod.ts.js';
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js'
|
||||
import { humanizeMilliseconds, snowflakeToTimestamp } from '../utils/helpers.ts.js'
|
||||
import { createCommand } from './mod.ts.js'
|
||||
|
||||
createCommand({
|
||||
name: "ping",
|
||||
description: "Ping the Bot!",
|
||||
name: 'ping',
|
||||
description: 'Ping the Bot!',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
scope: "Global",
|
||||
scope: 'Global',
|
||||
execute: async (bot, interaction) => {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
await bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`,
|
||||
},
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
await bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`,
|
||||
},
|
||||
);
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { events } from './mod.ts.js';
|
||||
import { updateGuildCommands } from '../utils/helpers.ts.js';
|
||||
import { events } from './mod.ts.js'
|
||||
import { updateGuildCommands } from '../utils/helpers.ts.js'
|
||||
|
||||
events.guildCreate = async (bot, guild) => await updateGuildCommands(bot, guild);
|
||||
events.guildCreate = async (bot, guild) => await updateGuildCommands(bot, guild)
|
||||
|
||||
@@ -1,135 +1,111 @@
|
||||
import type {
|
||||
BotWithCache,
|
||||
Guild} from '../../deps.ts.js';
|
||||
import {
|
||||
ApplicationCommandOptionTypes,
|
||||
bgBlack,
|
||||
bgYellow,
|
||||
black,
|
||||
green,
|
||||
red,
|
||||
white,
|
||||
yellow,
|
||||
} from '../../deps.ts.js';
|
||||
import { events } from './mod.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.ts.js';
|
||||
import type { Command} from '../commands/mod.ts.js';
|
||||
import { commands } from '../commands/mod.ts.js';
|
||||
import type { BotWithCache, Guild } from '../../deps.ts.js'
|
||||
import { ApplicationCommandOptionTypes, bgBlack, bgYellow, black, green, red, white, yellow } from '../../deps.ts.js'
|
||||
import { events } from './mod.ts.js'
|
||||
import { logger } from '../utils/logger.ts.js'
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.ts.js'
|
||||
import type { Command } from '../commands/mod.ts.js'
|
||||
import { commands } from '../commands/mod.ts.js'
|
||||
|
||||
const log = logger({ name: "Event: InteractionCreate" });
|
||||
const log = logger({ name: 'Event: InteractionCreate' })
|
||||
|
||||
events.interactionCreate = async (rawBot, interaction) => {
|
||||
const bot = rawBot as BotWithCache;
|
||||
const bot = rawBot as BotWithCache
|
||||
|
||||
if (interaction.data && interaction.id) {
|
||||
let guildName = "Direct Message";
|
||||
let guild = {} as Guild;
|
||||
let guildName = 'Direct Message'
|
||||
let guild = {} as Guild
|
||||
|
||||
// Set guild, if there was an error getting the guild, then just say it was a DM. (What else are we going to do?)
|
||||
if (interaction.guildId) {
|
||||
const guildOrVoid = await getGuildFromId(bot, interaction.guildId).catch(
|
||||
(err) => {
|
||||
log.error(err);
|
||||
},
|
||||
);
|
||||
const guildOrVoid = await getGuildFromId(bot, interaction.guildId).catch((err) => {
|
||||
log.error(err)
|
||||
})
|
||||
if (guildOrVoid) {
|
||||
guild = guildOrVoid;
|
||||
guildName = guild.name;
|
||||
guild = guildOrVoid
|
||||
guildName = guild.name
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(white(`Trigger`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(white(`Trigger`))}] by ${interaction.user.username}#${
|
||||
interaction.user.discriminator
|
||||
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
|
||||
let command: undefined | Command = interaction.data.name ? commands.get(interaction.data.name) : undefined;
|
||||
let commandName = command?.name;
|
||||
let command: undefined | Command = interaction.data.name ? commands.get(interaction.data.name) : undefined
|
||||
let commandName = command?.name
|
||||
|
||||
if (command !== undefined) {
|
||||
if (interaction.data.name) {
|
||||
if (interaction.data.options?.[0]) {
|
||||
const optionType = interaction.data.options[0].type;
|
||||
const optionType = interaction.data.options[0].type
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command.subcommands) return;
|
||||
if (!command.subcommands) return
|
||||
|
||||
// Try to find the subcommand group
|
||||
const subCommandGroup = command.subcommands?.find(
|
||||
(command) => command.name == interaction.data?.options?.[0].name,
|
||||
);
|
||||
if (!subCommandGroup) return;
|
||||
const subCommandGroup = command.subcommands?.find((command) => command.name == interaction.data?.options?.[0].name)
|
||||
if (!subCommandGroup) return
|
||||
|
||||
if (isSubCommand(subCommandGroup)) return;
|
||||
if (isSubCommand(subCommandGroup)) return
|
||||
|
||||
// Get name of the command which we are looking for
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name ||
|
||||
interaction.data.options?.[0].options?.[0].name;
|
||||
if (!targetCmdName) return;
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name || interaction.data.options?.[0].options?.[0].name
|
||||
if (!targetCmdName) return
|
||||
|
||||
// Try to find the command
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName);
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName)
|
||||
|
||||
commandName += ` ${subCommandGroup.name} ${command?.name}`;
|
||||
commandName += ` ${subCommandGroup.name} ${command?.name}`
|
||||
|
||||
// Normal
|
||||
}
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command?.subcommands) return;
|
||||
if (!command?.subcommands) return
|
||||
|
||||
// Try to find the command
|
||||
const found = command.subcommands.find((command) => command.name == interaction.data?.options?.[0].name);
|
||||
if (!found) return;
|
||||
const found = command.subcommands.find((command) => command.name == interaction.data?.options?.[0].name)
|
||||
if (!found) return
|
||||
|
||||
if (isSubCommandGroup(found)) return;
|
||||
if (isSubCommandGroup(found)) return
|
||||
|
||||
command = found;
|
||||
commandName += ` ${command?.name}`;
|
||||
command = found
|
||||
commandName += ` ${command?.name}`
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (command) {
|
||||
command.execute(rawBot, interaction);
|
||||
command.execute(rawBot, interaction)
|
||||
log.info(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(green(`Success`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(green(`Success`))}] by ${interaction.user.username}#${
|
||||
interaction.user.discriminator
|
||||
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
} else {
|
||||
throw "";
|
||||
throw ''
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(red(`Error`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
err.length ? log.error(err) : undefined;
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(red(`Error`))}] by ${interaction.user.username}#${
|
||||
interaction.user.discriminator
|
||||
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
err.length ? log.error(err) : undefined
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(yellow(`Not Found`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(yellow(`Not Found`))}] by ${interaction.user.username}#${
|
||||
interaction.user.discriminator
|
||||
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
// Handle subcommands
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { EventHandlers } from '../../deps.ts.js';
|
||||
import type { EventHandlers } from '../../deps.ts.js'
|
||||
|
||||
export const events: Partial<EventHandlers> = {};
|
||||
export const events: Partial<EventHandlers> = {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { events } from './mod.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
import { events } from './mod.ts.js'
|
||||
import { logger } from '../utils/logger.ts.js'
|
||||
|
||||
const log = logger({ name: "Event: Ready" });
|
||||
const log = logger({ name: 'Event: Ready' })
|
||||
|
||||
events.ready = () => {
|
||||
log.info("Bot Ready");
|
||||
};
|
||||
log.info('Bot Ready')
|
||||
}
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
import type {
|
||||
Bot,
|
||||
BotWithCache,
|
||||
CreateApplicationCommand,
|
||||
Guild,
|
||||
MakeRequired} from '../../deps.ts.js';
|
||||
import {
|
||||
getGuild,
|
||||
hasProperty,
|
||||
upsertGuildApplicationCommands,
|
||||
} from '../../deps.ts.js';
|
||||
import { logger } from './logger.ts.js';
|
||||
import type { subCommand, subCommandGroup } from '../commands/mod.ts.js';
|
||||
import { commands } from '../commands/mod.ts.js';
|
||||
import type { Bot, BotWithCache, CreateApplicationCommand, Guild, MakeRequired } from '../../deps.ts.js'
|
||||
import { getGuild, hasProperty, upsertGuildApplicationCommands } from '../../deps.ts.js'
|
||||
import { logger } from './logger.ts.js'
|
||||
import type { subCommand, subCommandGroup } from '../commands/mod.ts.js'
|
||||
import { commands } from '../commands/mod.ts.js'
|
||||
|
||||
const log = logger({ name: "Helpers" });
|
||||
const log = logger({ name: 'Helpers' })
|
||||
|
||||
/** This function will update all commands, or the defined scope */
|
||||
export async function updateCommands(bot: BotWithCache, scope?: "Guild" | "Global") {
|
||||
const globalCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
export async function updateCommands(bot: BotWithCache, scope?: 'Guild' | 'Global') {
|
||||
const globalCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope) {
|
||||
if (command.scope === "Guild") {
|
||||
if (command.scope === 'Guild') {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
} else if (command.scope === "Global") {
|
||||
})
|
||||
} else if (command.scope === 'Global') {
|
||||
globalCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
})
|
||||
}
|
||||
} else {
|
||||
perGuildCommands.push({
|
||||
@@ -43,87 +34,87 @@ export async function updateCommands(bot: BotWithCache, scope?: "Guild" | "Globa
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (globalCommands.length && (scope === "Global" || scope === undefined)) {
|
||||
log.info("Updating Global Commands, changes should apply in short...");
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(log.error);
|
||||
if (globalCommands.length && (scope === 'Global' || scope === undefined)) {
|
||||
log.info('Updating Global Commands, changes should apply in short...')
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(log.error)
|
||||
}
|
||||
|
||||
if (perGuildCommands.length && (scope === "Guild" || scope === undefined)) {
|
||||
if (perGuildCommands.length && (scope === 'Guild' || scope === undefined)) {
|
||||
await bot.guilds.forEach(async (guild: Guild) => {
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands);
|
||||
});
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** Update commands for a guild */
|
||||
export async function updateGuildCommands(bot: Bot, guild: Guild) {
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope) {
|
||||
if (command.scope === "Guild") {
|
||||
if (command.scope === 'Guild') {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (perGuildCommands.length) {
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands);
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuildFromId(bot: BotWithCache, guildId: bigint): Promise<Guild> {
|
||||
let returnValue: Guild = {} as Guild;
|
||||
let returnValue: Guild = {} as Guild
|
||||
|
||||
if (guildId !== 0n) {
|
||||
if (bot.guilds.get(guildId)) {
|
||||
returnValue = bot.guilds.get(guildId) as Guild;
|
||||
returnValue = bot.guilds.get(guildId) as Guild
|
||||
}
|
||||
|
||||
await getGuild(bot, guildId).then((guild) => {
|
||||
if (guild) bot.guilds.set(guildId, guild);
|
||||
if (guild) returnValue = guild;
|
||||
});
|
||||
if (guild) bot.guilds.set(guildId, guild)
|
||||
if (guild) returnValue = guild
|
||||
})
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
return returnValue
|
||||
}
|
||||
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
return Number(id / 4194304n + 1420070400000n)
|
||||
}
|
||||
|
||||
export function humanizeMilliseconds(milliseconds: number) {
|
||||
// Gets ms into seconds
|
||||
const time = milliseconds / 1000;
|
||||
if (time < 1) return "1s";
|
||||
const time = milliseconds / 1000
|
||||
if (time < 1) return '1s'
|
||||
|
||||
const days = Math.floor(time / 86400);
|
||||
const hours = Math.floor((time % 86400) / 3600);
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60);
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60);
|
||||
const days = Math.floor(time / 86400)
|
||||
const hours = Math.floor((time % 86400) / 3600)
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60)
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60)
|
||||
|
||||
const dayString = days ? `${days}d ` : "";
|
||||
const hourString = hours ? `${hours}h ` : "";
|
||||
const minuteString = minutes ? `${minutes}m ` : "";
|
||||
const secondString = seconds ? `${seconds}s ` : "";
|
||||
const dayString = days ? `${days}d ` : ''
|
||||
const hourString = hours ? `${hours}h ` : ''
|
||||
const minuteString = minutes ? `${minutes}m ` : ''
|
||||
const secondString = seconds ? `${seconds}s ` : ''
|
||||
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`;
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`
|
||||
}
|
||||
|
||||
export function isSubCommand(data: subCommand | subCommandGroup): data is subCommand {
|
||||
return !hasProperty(data, "subCommands");
|
||||
return !hasProperty(data, 'subCommands')
|
||||
}
|
||||
|
||||
export function isSubCommandGroup(data: subCommand | subCommandGroup): data is subCommandGroup {
|
||||
return hasProperty(data, "subCommands");
|
||||
return hasProperty(data, 'subCommands')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js';
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js'
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
@@ -10,81 +10,81 @@ export enum LogLevels {
|
||||
}
|
||||
|
||||
const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Debug, "DEBUG"],
|
||||
[LogLevels.Info, "INFO"],
|
||||
[LogLevels.Warn, "WARN"],
|
||||
[LogLevels.Error, "ERROR"],
|
||||
[LogLevels.Fatal, "FATAL"],
|
||||
]);
|
||||
[LogLevels.Debug, 'DEBUG'],
|
||||
[LogLevels.Info, 'INFO'],
|
||||
[LogLevels.Warn, 'WARN'],
|
||||
[LogLevels.Error, 'ERROR'],
|
||||
[LogLevels.Fatal, 'FATAL'],
|
||||
])
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
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;
|
||||
logLevel?: LogLevels
|
||||
name?: string
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
if (level < logLevel) return;
|
||||
if (level < logLevel) return
|
||||
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
let color = colorFunctions.get(level)
|
||||
if (!color) color = noColor
|
||||
|
||||
const date = new Date();
|
||||
const date = new Date()
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || "DEBUG"),
|
||||
name ? `${name} >` : ">",
|
||||
color(prefixes.get(level) || 'DEBUG'),
|
||||
name ? `${name} >` : '>',
|
||||
...args,
|
||||
];
|
||||
]
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log);
|
||||
return console.debug(...log)
|
||||
case LogLevels.Info:
|
||||
return console.info(...log);
|
||||
return console.info(...log)
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log);
|
||||
return console.warn(...log)
|
||||
case LogLevels.Error:
|
||||
return console.error(...log);
|
||||
return console.error(...log)
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log);
|
||||
return console.error(...log)
|
||||
default:
|
||||
return console.log(...log);
|
||||
return console.log(...log)
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
logLevel = level;
|
||||
logLevel = level
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
log(LogLevels.Debug, ...args);
|
||||
log(LogLevels.Debug, ...args)
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
log(LogLevels.Info, ...args);
|
||||
log(LogLevels.Info, ...args)
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
log(LogLevels.Warn, ...args);
|
||||
log(LogLevels.Warn, ...args)
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
log(LogLevels.Error, ...args);
|
||||
log(LogLevels.Error, ...args)
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
log(LogLevels.Fatal, ...args);
|
||||
log(LogLevels.Fatal, ...args)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -95,7 +95,7 @@ export function logger({
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const log = logger();
|
||||
export const log = logger()
|
||||
|
||||
Reference in New Issue
Block a user