mirror of
https://github.com/xHyroM/bun-discord-bot.git
synced 2024-12-22 12:11:06 +01:00
chore: base
This commit is contained in:
parent
9812eadced
commit
28584ed76a
21 changed files with 806 additions and 3 deletions
146
.gitignore
vendored
Normal file
146
.gitignore
vendored
Normal file
|
@ -0,0 +1,146 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
files/config.toml
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
0
README.md
Normal file
0
README.md
Normal file
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
9
files/config.example.toml
Normal file
9
files/config.example.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# For start application rename it to config.toml !!
|
||||
|
||||
[server]
|
||||
port = 3000
|
||||
|
||||
[client]
|
||||
public_key = "CLIENT PUBLIC KEY"
|
||||
token = "CLIENT TOKEN"
|
||||
id = "CLIENT USER ID"
|
11
files/tags.toml
Normal file
11
files/tags.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[test]
|
||||
keywords = ["test", "t"]
|
||||
content = """
|
||||
Hello, new bot :)
|
||||
"""
|
||||
|
||||
[lol]
|
||||
keywords = ["l"]
|
||||
content = """
|
||||
Lol
|
||||
"""
|
15
package.json
15
package.json
|
@ -1,5 +1,16 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.0",
|
||||
"name": "bun-discord-bot",
|
||||
"main": "src/index.js"
|
||||
"scripts": {
|
||||
"start": "bun src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^0.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^0.7.0",
|
||||
"discord-api-types": "^0.36.1",
|
||||
"hono": "^1.6.4",
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
}
|
16
src/commands/help.ts
Normal file
16
src/commands/help.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { InteractionResponseType } from 'discord-api-types/v10';
|
||||
import { Command } from '../structures/Command';
|
||||
|
||||
new Command({
|
||||
name: 'help',
|
||||
description: 'Help command',
|
||||
guildId: '924395690451423332',
|
||||
run: (c) => {
|
||||
return c.respond({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: 'hello'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
45
src/commands/tags.ts
Normal file
45
src/commands/tags.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { APIApplicationCommandInteractionDataStringOption, ApplicationCommandOptionType, InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
||||
import { Command } from '../structures/Command';
|
||||
import { findTags, getTag } from '../utils/tagsUtils';
|
||||
|
||||
new Command({
|
||||
name: 'tags',
|
||||
description: 'Send a tag by name or alias',
|
||||
guildId: '924395690451423332',
|
||||
options: [
|
||||
{
|
||||
name: 'query',
|
||||
description: 'Tag name or alias',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
run: (ctx) => {
|
||||
return ctx.respond(findTags(ctx.value));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'target',
|
||||
description: 'User to mention',
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: false
|
||||
}
|
||||
],
|
||||
run: (c) => {
|
||||
const query: APIApplicationCommandInteractionDataStringOption = c.options[0] as APIApplicationCommandInteractionDataStringOption;
|
||||
const target = c?.resolved?.users?.[0];
|
||||
|
||||
const tag = getTag(query.value);
|
||||
if (!tag)
|
||||
return c.respond({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`❌\` Could not find a tag \`${query.value}\``,
|
||||
flags: MessageFlags.Ephemeral
|
||||
}
|
||||
});
|
||||
|
||||
return c.respond([
|
||||
target ? `*Tag suggestion for <@${target.id}>:*` : '',
|
||||
tag.content
|
||||
].join('\n'));
|
||||
}
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
console.log('Hello World');
|
84
src/index.ts
Normal file
84
src/index.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Hono } from 'hono';
|
||||
import { bodyParse } from 'hono/body-parse';
|
||||
import { Logger } from './utils/Logger';
|
||||
|
||||
// @ts-expect-error Types :(
|
||||
import config from '../files/config.toml';
|
||||
import loadCommands from './utils/loadCommands';
|
||||
import { verifyKey } from './utils/verify';
|
||||
import { APIPingInteraction, APIApplicationCommandInteraction, APIMessageComponentInteraction, InteractionType, InteractionResponseType, ApplicationCommandType, APIApplicationCommandAutocompleteInteraction, ApplicationCommandOptionType, APIApplicationCommandOption } from 'discord-api-types/v10';
|
||||
import { CommandContext } from './structures/contexts/CommandContext';
|
||||
import { Commands } from './managers/CommandManager';
|
||||
import registerCommands from './utils/registerCommands';
|
||||
import { Option, OptionOptions } from './structures/Option';
|
||||
import { AutocompleteContext } from './structures/contexts/AutocompleteContext';
|
||||
|
||||
await loadCommands();
|
||||
try {
|
||||
await registerCommands(config.client.token, config.client.id);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.post('/interaction', bodyParse(), async(c) => {
|
||||
const signature = c.req.headers.get('X-Signature-Ed25519');
|
||||
const timestamp = c.req.headers.get('X-Signature-Timestamp');
|
||||
if (!signature || !timestamp) return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
||||
if (!await verifyKey(JSON.stringify(c.req.parsedBody), signature, timestamp, config.client.public_key)) return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
||||
|
||||
const interaction = c.req.parsedBody as unknown as APIPingInteraction | APIApplicationCommandInteraction | APIMessageComponentInteraction | APIApplicationCommandAutocompleteInteraction;
|
||||
|
||||
if (interaction.type === InteractionType.Ping) {
|
||||
return new CommandContext(c).respond({
|
||||
type: InteractionResponseType.Pong
|
||||
});
|
||||
}
|
||||
|
||||
if (interaction.type === InteractionType.ApplicationCommandAutocomplete && interaction.data.type === ApplicationCommandType.ChatInput) {
|
||||
const command = Commands.get(interaction.data.name);
|
||||
let options = command.options;
|
||||
const subCommandGroup = interaction.data.options.find(option => option.type === ApplicationCommandOptionType.SubcommandGroup)
|
||||
const subCommand = interaction.data.options.find(option => option.type === ApplicationCommandOptionType.Subcommand);
|
||||
|
||||
// @ts-expect-error ?? find
|
||||
if (subCommandGroup) options = options.find(option => option.name === subCommandGroup.name)?.options;
|
||||
// @ts-expect-error ?? find
|
||||
if (subCommand) options = options.find(option => option.name === subCommand.name)?.options;
|
||||
|
||||
// @ts-expect-error i dont want waste time
|
||||
const focused: APIApplicationCommandBasicOption = interaction.data.options.find(option => option.focused === true);
|
||||
// @ts-expect-error ?? find
|
||||
const option: Option | OptionOptions = options.find(option => option.name === focused.name);
|
||||
|
||||
return option.run(new AutocompleteContext(
|
||||
c,
|
||||
option,
|
||||
focused.value
|
||||
));
|
||||
}
|
||||
|
||||
if (interaction.type === InteractionType.ApplicationCommand && interaction.data.type === ApplicationCommandType.ChatInput) {
|
||||
return Commands.get(interaction.data.name).run(new CommandContext(
|
||||
c,
|
||||
interaction.data.options,
|
||||
interaction.data.resolved
|
||||
));
|
||||
}
|
||||
|
||||
return new CommandContext(c).respond({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: 'Beep boop. Boop beep?'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await Bun.serve({
|
||||
port: config.server.port,
|
||||
fetch: app.fetch,
|
||||
});
|
||||
|
||||
Logger.info('🚀 Server started at', config.server.port.toString());
|
||||
Logger.debug(`🌍 http://localhost:${config.server.port}`);
|
20
src/managers/CommandManager.ts
Normal file
20
src/managers/CommandManager.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Collection from '@discordjs/collection';
|
||||
import { Command } from '../structures/Command';
|
||||
|
||||
class CommandManager extends Collection<String, Command> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public register(command: Command): CommandManager {
|
||||
this.set(command.name, command);
|
||||
return this;
|
||||
}
|
||||
|
||||
public unregister(command: Command): CommandManager {
|
||||
this.delete(command.name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const Commands = new CommandManager();
|
59
src/structures/Command.ts
Normal file
59
src/structures/Command.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Command.ts
|
||||
|
||||
import { LocaleString } from 'discord-api-types/v10';
|
||||
import { Commands } from '../managers/CommandManager';
|
||||
import { CommandContext } from './contexts/CommandContext';
|
||||
import { Option, OptionOptions } from './Option';
|
||||
|
||||
export interface CommandOptions {
|
||||
name: string;
|
||||
nameLocalizations?: Record<LocaleString, string>;
|
||||
description?: string;
|
||||
descriptionLocalizations?: Record<LocaleString, string>;
|
||||
guildId?: string;
|
||||
defaultMemberPermissions?: string;
|
||||
options?: Option[] | OptionOptions[];
|
||||
run: (ctx: CommandContext) => Response;
|
||||
}
|
||||
|
||||
export class Command {
|
||||
public name: string;
|
||||
public nameLocalizations?: Record<LocaleString, string>;
|
||||
public description?: string;
|
||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
||||
public guildId?: string;
|
||||
public defaultMemberPermissions?: string;
|
||||
public options: Option[] | OptionOptions[];
|
||||
public run: (ctx: CommandContext) => Response;
|
||||
|
||||
public constructor(options: CommandOptions) {
|
||||
this.name = options.name;
|
||||
this.nameLocalizations = options.nameLocalizations;
|
||||
|
||||
this.description = options.description;
|
||||
this.descriptionLocalizations = options.descriptionLocalizations;
|
||||
|
||||
this.guildId = options.guildId;
|
||||
this.defaultMemberPermissions = options.defaultMemberPermissions;
|
||||
|
||||
this.options = options.options?.map(option => {
|
||||
if (option instanceof Option) return option;
|
||||
else return new Option(option);
|
||||
});
|
||||
this.run = options.run;
|
||||
|
||||
Commands.register(this);
|
||||
}
|
||||
|
||||
public toJSON(): Record<string, any> {
|
||||
return {
|
||||
name: this.name,
|
||||
name_localizations: this.nameLocalizations,
|
||||
description: this.description,
|
||||
description_localizations: this.descriptionLocalizations,
|
||||
guild_id: this.guildId,
|
||||
default_member_permissions: this.defaultMemberPermissions,
|
||||
options: this.options?.map(option => option.toJSON()),
|
||||
}
|
||||
}
|
||||
}
|
96
src/structures/Option.ts
Normal file
96
src/structures/Option.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Argument.ts
|
||||
|
||||
import { ApplicationCommandOptionType, ChannelType, LocaleString } from 'discord-api-types/v10';
|
||||
import { AutocompleteContext } from './contexts/AutocompleteContext';
|
||||
|
||||
export interface OptionChoice {
|
||||
name: string;
|
||||
nameLocalizations?: Record<LocaleString, string>;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface OptionOptions {
|
||||
name: string;
|
||||
nameLocalizations?: Record<LocaleString, string>;
|
||||
description: string;
|
||||
descriptionLocalizations?: Record<LocaleString, string>;
|
||||
type: ApplicationCommandOptionType
|
||||
required?: boolean;
|
||||
choices?: OptionChoice[];
|
||||
options?: Array<Option | OptionOptions>;
|
||||
channelTypes?: ChannelType[];
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
run?: (ctx: AutocompleteContext) => Response;
|
||||
}
|
||||
|
||||
export class Option {
|
||||
public name: string;
|
||||
public nameLocalizations?: Record<LocaleString, string>;
|
||||
public description: string;
|
||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
||||
public type: ApplicationCommandOptionType;
|
||||
public required?: boolean;
|
||||
public choices?: Array<OptionChoice>;
|
||||
public options?: Array<Option>;
|
||||
public channelTypes?: Array<ChannelType | keyof typeof ChannelType>;
|
||||
public minValue?: number;
|
||||
public maxValue?: number;
|
||||
public minLength?: number;
|
||||
public maxLength?: number;
|
||||
public run?: (ctx: AutocompleteContext) => Response;
|
||||
|
||||
public constructor(options: OptionOptions) {
|
||||
this.name = options.name;
|
||||
this.nameLocalizations = options.nameLocalizations;
|
||||
this.description = options.description;
|
||||
this.descriptionLocalizations = options.descriptionLocalizations;
|
||||
this.type = options.type;
|
||||
this.required = options.required;
|
||||
this.choices = options.choices;
|
||||
this.options = options.options?.map(argument => {
|
||||
if (argument instanceof Option) return argument;
|
||||
else return new Option(argument);
|
||||
});
|
||||
this.channelTypes = options.channelTypes;
|
||||
this.minValue = options.minValue;
|
||||
this.maxValue = options.maxValue;
|
||||
this.minLength = options.minLength;
|
||||
this.maxLength = options.maxLength;
|
||||
this.run = options.run;
|
||||
}
|
||||
|
||||
public toJSON(): Record<string, any> {
|
||||
if (
|
||||
this.type === ApplicationCommandOptionType.Subcommand ||
|
||||
this.type === ApplicationCommandOptionType.SubcommandGroup
|
||||
) {
|
||||
return {
|
||||
name: this.name,
|
||||
name_localizations: this.nameLocalizations,
|
||||
description: this.description,
|
||||
description_localizations: this.descriptionLocalizations,
|
||||
type: this.type,
|
||||
options: this.options?.map(option => option.toJSON()),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
name_localizations: this.nameLocalizations,
|
||||
description: this.description,
|
||||
description_localizations: this.descriptionLocalizations,
|
||||
type: this.type,
|
||||
required: this.required,
|
||||
choices: this.choices,
|
||||
channel_types: this.channelTypes,
|
||||
min_value: this.minValue,
|
||||
max_value: this.maxValue,
|
||||
min_length: this.minLength,
|
||||
max_length: this.maxLength,
|
||||
autocomplete: typeof this.run === 'function',
|
||||
};
|
||||
}
|
||||
}
|
24
src/structures/contexts/AutocompleteContext.ts
Normal file
24
src/structures/contexts/AutocompleteContext.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { APIApplicationCommandOptionChoice, InteractionResponseType } from 'discord-api-types/v10';
|
||||
import { Context } from 'hono';
|
||||
import { Option, OptionOptions } from '../Option';
|
||||
|
||||
export class AutocompleteContext {
|
||||
public context: Context;
|
||||
public option?: Option | OptionOptions;
|
||||
public value?: string;
|
||||
|
||||
public constructor(c: Context, option: Option | OptionOptions, value: string) {
|
||||
this.context = c;
|
||||
this.option = option;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public respond(response: APIApplicationCommandOptionChoice[]) {
|
||||
return this.context.json({
|
||||
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
|
||||
data: {
|
||||
choices: response
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
27
src/structures/contexts/CommandContext.ts
Normal file
27
src/structures/contexts/CommandContext.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { APIApplicationCommandInteractionDataOption, APIChatInputApplicationCommandInteractionDataResolved, APIInteractionResponse, InteractionResponseType } from 'discord-api-types/v10';
|
||||
import { Context } from 'hono';
|
||||
|
||||
export class CommandContext {
|
||||
public context: Context;
|
||||
public options?: APIApplicationCommandInteractionDataOption[];
|
||||
public resolved?: APIChatInputApplicationCommandInteractionDataResolved;
|
||||
|
||||
public constructor(c: Context, options?: APIApplicationCommandInteractionDataOption[], resolved?: APIChatInputApplicationCommandInteractionDataResolved) {
|
||||
this.context = c;
|
||||
this.options = options;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
public respond(response: APIInteractionResponse | string) {
|
||||
if (typeof response === 'string') {
|
||||
return this.context.json({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: response
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.context.json(response);
|
||||
}
|
||||
}
|
47
src/utils/Logger.ts
Normal file
47
src/utils/Logger.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
type Level = 'INFO' | 'SUCCESS' | 'WARN' | 'ERROR' | 'DEBUG'
|
||||
|
||||
export class Logger extends null {
|
||||
public static info(...messages: any[]) {
|
||||
this.log('INFO', messages);
|
||||
}
|
||||
|
||||
public static success(...messages: any[]) {
|
||||
this.log('SUCCESS', messages);
|
||||
}
|
||||
|
||||
public static warn(...messages: any[]) {
|
||||
this.log('WARN', messages);
|
||||
}
|
||||
|
||||
public static error(...messages: any[]) {
|
||||
this.log('ERROR', messages);
|
||||
}
|
||||
|
||||
public static debug(...messages: any[]) {
|
||||
this.log('DEBUG', messages);
|
||||
}
|
||||
|
||||
private static log(level: Level, messages: any[]) {
|
||||
const date = new Date();
|
||||
|
||||
let color = '';
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
color = '\x1b[36m';
|
||||
break;
|
||||
case 'SUCCESS':
|
||||
color = '\u001b[32m';
|
||||
break;
|
||||
case 'WARN':
|
||||
color = '\x1b[93m';
|
||||
break;
|
||||
case 'ERROR':
|
||||
color = '\x1b[91m';
|
||||
break;
|
||||
default:
|
||||
color = '\x1b[2m';
|
||||
}
|
||||
|
||||
console.log(`${color}[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}/${level}]\x1b[0m`, ...messages);
|
||||
}
|
||||
}
|
17
src/utils/loadCommands.ts
Normal file
17
src/utils/loadCommands.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { readdirSync } from 'fs';
|
||||
import { basename, dirname, join } from 'path';
|
||||
import { Logger } from './Logger';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
export default async() => {
|
||||
const commandsDir = join(__dirname, '..', 'commands');
|
||||
for (
|
||||
const command of readdirSync(commandsDir)
|
||||
) {
|
||||
const name = basename(command, '.ts');
|
||||
Logger.info(`Loading ${name} command`);
|
||||
await import(join(commandsDir, command));
|
||||
Logger.success(`Command ${name} has been loaded`);
|
||||
}
|
||||
}
|
56
src/utils/registerCommands.ts
Normal file
56
src/utils/registerCommands.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { RouteBases, Routes } from 'discord-api-types/v10';
|
||||
import { Commands } from '../managers/CommandManager';
|
||||
import { Command } from '../structures/Command';
|
||||
import { Logger } from './Logger';
|
||||
|
||||
const sync = async(
|
||||
clientToken: string,
|
||||
clientUserId: string,
|
||||
commands: Command[],
|
||||
guildId?: string,
|
||||
) => {
|
||||
const res = await fetch(
|
||||
`${RouteBases.api}${guildId ? Routes.applicationGuildCommands(clientUserId, guildId) : Routes.applicationCommands(clientUserId)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(commands.flatMap(command => command.toJSON())),
|
||||
headers: {
|
||||
'Authorization': `Bot ${clientToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (res.ok) return Logger.success('🌍 All commands has been synchronized with discord api.');
|
||||
const data = await res.json() as any;
|
||||
|
||||
if (res.status === 429) {
|
||||
setTimeout(
|
||||
() => sync(clientToken, clientUserId, commands, guildId),
|
||||
data.retry_after * 1000,
|
||||
);
|
||||
} else {
|
||||
Logger.error(
|
||||
typeof data.code !== 'undefined' ? data.code.toString() : '',
|
||||
data.message
|
||||
)
|
||||
|
||||
console.log(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default async(clientToken: string, clientUserId: string) => {
|
||||
if (Commands.size === 0) return;
|
||||
|
||||
const [guild, global] = Commands.partition(
|
||||
command => typeof command.guildId === 'string',
|
||||
);
|
||||
|
||||
const guildIds = new Set(guild.map(c => c.guildId));
|
||||
for await (const guildId of guildIds) {
|
||||
const commands = guild.filter(item => item.guildId === guildId);
|
||||
await sync(clientToken, clientUserId, [...commands.values()], guildId);
|
||||
}
|
||||
|
||||
if (global.size > 0) await sync(clientToken, clientUserId, [...global.values()]);
|
||||
}
|
40
src/utils/tagsUtils.ts
Normal file
40
src/utils/tagsUtils.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Collection from '@discordjs/collection';
|
||||
// @ts-expect-error Types :(
|
||||
import tags from '../../files/tags.toml';
|
||||
|
||||
export interface Tag {
|
||||
keywords: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
const tagCache: Collection<string, Tag> = new Collection();
|
||||
|
||||
for (const [key, value] of Object.entries(tags)) {
|
||||
tagCache.set(key, value as unknown as Tag);
|
||||
}
|
||||
|
||||
export const getTag = (name: string) => {
|
||||
const tag = tagCache.get(name) || tagCache.find(tag => tag.keywords.includes(name));
|
||||
return tag;
|
||||
}
|
||||
|
||||
export const findTags = (name: string) => {
|
||||
if (!name)
|
||||
return [
|
||||
...tagCache.map((tag, name) => new Object({
|
||||
name,
|
||||
value: name
|
||||
})).slice(0, 25)
|
||||
];
|
||||
else {
|
||||
const tag = getTag(name);
|
||||
if (tag)
|
||||
return [
|
||||
{
|
||||
name,
|
||||
value: name
|
||||
}
|
||||
]
|
||||
else return findTags(null);
|
||||
}
|
||||
}
|
86
src/utils/verify.ts
Normal file
86
src/utils/verify.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
// from https://github.com/discord/discord-interactions-js/blob/main/src/index.ts
|
||||
|
||||
import { sign } from 'tweetnacl';
|
||||
|
||||
/**
|
||||
* Converts different types to Uint8Array.
|
||||
*
|
||||
* @param value - Value to convert. Strings are parsed as hex.
|
||||
* @param format - Format of value. Valid options: 'hex'. Defaults to utf-8.
|
||||
* @returns Value in Uint8Array form.
|
||||
*/
|
||||
function valueToUint8Array(value: Uint8Array | ArrayBuffer | Buffer | string, format?: string): Uint8Array {
|
||||
if (value == null) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
if (format === 'hex') {
|
||||
const matches = value.match(/.{1,2}/g);
|
||||
if (matches == null) {
|
||||
throw new Error('Value is not a valid hex string');
|
||||
}
|
||||
const hexVal = matches.map((byte: string) => parseInt(byte, 16));
|
||||
return new Uint8Array(hexVal);
|
||||
} else {
|
||||
return new TextEncoder().encode(value);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (Buffer.isBuffer(value)) {
|
||||
const arrayBuffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.length);
|
||||
return new Uint8Array(value);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Runtime doesn't have Buffer
|
||||
}
|
||||
if (value instanceof ArrayBuffer) {
|
||||
return new Uint8Array(value);
|
||||
}
|
||||
if (value instanceof Uint8Array) {
|
||||
return value;
|
||||
}
|
||||
throw new Error('Unrecognized value type, must be one of: string, Buffer, ArrayBuffer, Uint8Array');
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two arrays.
|
||||
*
|
||||
* @param arr1 - First array
|
||||
* @param arr2 - Second array
|
||||
* @returns Concatenated arrays
|
||||
*/
|
||||
function concatUint8Arrays(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {
|
||||
const merged = new Uint8Array(arr1.length + arr2.length);
|
||||
merged.set(arr1);
|
||||
merged.set(arr2, arr1.length);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a payload from Discord against its signature and key.
|
||||
*
|
||||
* @param rawBody - The raw payload data
|
||||
* @param signature - The signature from the `X-Signature-Ed25519` header
|
||||
* @param timestamp - The timestamp from the `X-Signature-Timestamp` header
|
||||
* @param clientPublicKey - The public key from the Discord developer dashboard
|
||||
* @returns Whether or not validation was successful
|
||||
*/
|
||||
export const verifyKey = (
|
||||
body: Uint8Array | ArrayBuffer | Buffer | string,
|
||||
signature: Uint8Array | ArrayBuffer | Buffer | string,
|
||||
timestamp: Uint8Array | ArrayBuffer | Buffer | string,
|
||||
clientPublicKey: Uint8Array | ArrayBuffer | Buffer | string,
|
||||
): boolean => {
|
||||
try {
|
||||
const timestampData = valueToUint8Array(timestamp);
|
||||
const bodyData = valueToUint8Array(body);
|
||||
const message = concatUint8Arrays(timestampData, bodyData);
|
||||
|
||||
const signatureData = valueToUint8Array(signature, 'hex');
|
||||
const publicKeyData = valueToUint8Array(clientPublicKey, 'hex');
|
||||
return sign.detached.verify(message, signatureData, publicKeyData);
|
||||
} catch (ex) {
|
||||
console.error('[discord-interactions]: Invalid verifyKey parameters', ex);
|
||||
return false;
|
||||
}
|
||||
}
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "Node",
|
||||
// "bun-types" is the important part
|
||||
"types": ["bun-types"]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue