first commit

This commit is contained in:
Badstagram 2025-04-26 02:29:14 +01:00
commit 8a8e67b47e
52 changed files with 2541 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# compiled output
dist
tmp
out-tsc
# dependencies
node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
.nx/cache
.nx/workspace-data

5
.prettierignore Normal file
View File

@ -0,0 +1,5 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache
/.nx/workspace-data

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode"]
}

109
README.md Normal file
View File

@ -0,0 +1,109 @@
# Botzilla
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
✨ Your new, shiny [Nx workspace](https://nx.dev) is ready ✨.
[Learn more about this workspace setup and its capabilities](https://nx.dev/nx-api/js?utm_source=nx_project&amp;utm_medium=readme&amp;utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
## Generate a library
```sh
npx nx g @nx/js:lib packages/pkg1 --publishable --importPath=@my-org/pkg1
```
## Run tasks
To build the library use:
```sh
npx nx build pkg1
```
To run any task with Nx use:
```sh
npx nx <target> <project-name>
```
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
[More about running tasks in the docs &raquo;](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
## Versioning and releasing
To version and release the library use
```
npx nx release
```
Pass `--dry-run` to see what would happen without actually releasing the library.
[Learn more about Nx release &raquo;](hhttps://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
## Keep TypeScript project references up to date
Nx automatically updates TypeScript [project references](https://www.typescriptlang.org/docs/handbook/project-references.html) in `tsconfig.json` files to ensure they remain accurate based on your project dependencies (`import` or `require` statements). This sync is automatically done when running tasks such as `build` or `typecheck`, which require updated references to function correctly.
To manually trigger the process to sync the project graph dependencies information to the TypeScript project references, run the following command:
```sh
npx nx sync
```
You can enforce that the TypeScript project references are always in the correct state when running in CI by adding a step to your CI job configuration that runs the following command:
```sh
npx nx sync:check
```
[Learn more about nx sync](https://nx.dev/reference/nx-commands#sync)
## Set up CI!
### Step 1
To connect to Nx Cloud, run the following command:
```sh
npx nx connect
```
Connecting to Nx Cloud ensures a [fast and scalable CI](https://nx.dev/ci/intro/why-nx-cloud?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) pipeline. It includes features such as:
- [Remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [Task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [Automated e2e test splitting](https://nx.dev/ci/features/split-e2e-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [Task flakiness detection and rerunning](https://nx.dev/ci/features/flaky-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
### Step 2
Use the following command to configure a CI workflow for your workspace:
```sh
npx nx g ci-workflow
```
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
## Install Nx Console
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
[Install Nx Console &raquo;](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
## Useful links
Learn more:
- [Learn more about this workspace setup](https://nx.dev/nx-api/js?utm_source=nx_project&amp;utm_medium=readme&amp;utm_campaign=nx_projects)
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
And join the Nx community:
- [Discord](https://go.nx.dev/community)
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)

4
apps/bot/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
dist/
node_modules/
.yarn/
examples/*/dist/

13
apps/bot/.sapphirerc.yml Normal file
View File

@ -0,0 +1,13 @@
$schema: "./node_modules/@sapphire/cli/templates/schemas/.sapphirerc.scheme.json"
projectLanguage: "ts"
locations:
base: src
arguments: arguments
commands: commands
listeners: listeners
preconditions: preconditions
interaction-handlers: interaction-handlers
routes: routes
customFileTemplates:
enabled: false
location: ""

30
apps/bot/README.md Normal file
View File

@ -0,0 +1,30 @@
# TypeScript Sapphire Bot example with Tsup
This is a basic setup of a Discord bot using the [sapphire framework][sapphire] written in TypeScript
## How to use it?
### Prerequisite
```sh
npm install
```
### Development
This example can be run with `tsup` to watch the files and automatically restart your bot.
```sh
npm run dev
```
### Production
You can also run the bot with `npm dev`, this will first build your code and then run `node ./dist/index.js`. But this is not the recommended way to run a bot in production.
## License
Dedicated to the public domain via the [Unlicense], courtesy of the Sapphire Community and its contributors.
[sapphire]: https://github.com/sapphiredev/framework
[unlicense]: https://github.com/sapphiredev/examples/blob/main/LICENSE.md

47
apps/bot/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "@aura/bot",
"version": "1.0.0",
"main": "dist/index.js",
"author": "@sapphire",
"license": "UNLICENSE",
"type": "module",
"dependencies": {
"@sapphire/decorators": "^6.1.1",
"@sapphire/discord-utilities": "^3.4.4",
"@sapphire/discord.js-utilities": "7.3.2",
"@sapphire/fetch": "^3.0.5",
"@sapphire/framework": "^5.3.2",
"@sapphire/plugin-api": "^8.0.0",
"@sapphire/plugin-editable-commands": "^4.0.4",
"@sapphire/plugin-logger": "^4.0.2",
"@sapphire/plugin-subcommands": "^7.0.1",
"@sapphire/time-utilities": "^1.7.14",
"@sapphire/type": "^2.6.0",
"@sapphire/utilities": "^3.18.1",
"@skyra/env-utilities": "^1.3.0",
"discord.js": "^14.17.3",
"@aura/tsconfig": "workspace:*",
"@aura/config": "workspace:*"
},
"devDependencies": {
"@sapphire/cli": "^1.9.3",
"@sapphire/prettier-config": "^2.0.0",
"@sapphire/ts-config": "^5.0.1",
"@types/node": "^22.10.7",
"@types/ws": "^8.5.13",
"npm-run-all2": "^7.0.2",
"prettier": "^3.4.2",
"tsup": "^8.3.5",
"typescript": "~5.4.5"
},
"scripts": {
"sapphire": "sapphire",
"generate": "sapphire generate",
"build": "tsup",
"watch": "tsup --watch",
"start": "bun dist/index.js",
"dev": "tsup --watch --onSuccess \"bun ./dist/index.js\"",
"format": "prettier --write \"src/**/*.ts\""
},
"prettier": "@sapphire/prettier-config"
}

3
apps/bot/src/.env Normal file
View File

@ -0,0 +1,3 @@
# Tokens
DISCORD_TOKEN=
OWNERS=

View File

@ -0,0 +1,59 @@
import { ApplyOptions, RequiresClientPermissions, RequiresDMContext, RequiresGuildContext } from '@sapphire/decorators';
import { send } from '@sapphire/plugin-editable-commands';
import { Subcommand } from '@sapphire/plugin-subcommands';
import { EmbedBuilder, PermissionFlagsBits, type Message } from 'discord.js';
@ApplyOptions<Subcommand.Options>({
aliases: ['cwd'],
description: 'A basic command with some subcommands',
subcommands: [
{
name: 'add',
messageRun: 'messageAdd'
},
{
name: 'create',
messageRun: 'messageAdd'
},
{
name: 'remove',
messageRun: 'messageRemove'
},
{
name: 'reset',
messageRun: 'messageReset'
},
{
name: 'show',
messageRun: 'messageShow',
default: true
}
]
})
export class UserCommand extends Subcommand {
// Anyone should be able to view the result, but not modify
public async messageShow(message: Message) {
return send(message, 'Showing!');
}
@RequiresClientPermissions([PermissionFlagsBits.EmbedLinks]) // This sub-command requires the bot to have EMBED_LINKS permission because it sends a EmbedBuilder
public async messageAdd(message: Message) {
const embed = new EmbedBuilder() //
.setColor('#3986E4')
.setDescription('Added!')
.setTitle('Configuration Log')
.setTimestamp();
return send(message, { embeds: [embed] });
}
@RequiresGuildContext((message: Message) => send(message, 'This sub-command can only be used in servers'))
public async messageRemove(message: Message) {
return send(message, 'Removing!');
}
@RequiresDMContext((message: Message) => send(message, 'This sub-command can only be used in DMs'))
public async messageReset(message: Message) {
return send(message, 'Resetting!');
}
}

View File

@ -0,0 +1,50 @@
import { ApplyOptions } from '@sapphire/decorators';
import { send } from '@sapphire/plugin-editable-commands';
import { Subcommand } from '@sapphire/plugin-subcommands';
import type { Message } from 'discord.js';
@ApplyOptions<Subcommand.Options>({
aliases: ['cws'],
description: 'A basic command with some subcommands',
subcommands: [
{
name: 'add',
messageRun: 'messageAdd'
},
{
name: 'create',
messageRun: 'messageAdd'
},
{
name: 'remove',
messageRun: 'messageRemove'
},
{
name: 'reset',
messageRun: 'messageReset'
},
{
name: 'show',
messageRun: 'messageShow',
default: true
}
]
})
export class UserCommand extends Subcommand {
// Anyone should be able to view the result, but not modify
public async messageShow(message: Message) {
return send(message, 'Showing!');
}
public async messageAdd(message: Message) {
return send(message, 'Adding!');
}
public async messageRemove(message: Message) {
return send(message, 'Removing!');
}
public async messageReset(message: Message) {
return send(message, 'Resetting!');
}
}

View File

@ -0,0 +1,70 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Command, type Args } from '@sapphire/framework';
import { send } from '@sapphire/plugin-editable-commands';
import { codeBlock, isThenable } from '@sapphire/utilities';
import type { Message } from 'discord.js';
import { inspect } from 'util';
@ApplyOptions<Command.Options>({
aliases: ['ev'],
description: 'Evals any JavaScript code',
quotes: [],
preconditions: ['OwnerOnly'],
flags: ['async', 'hidden', 'showHidden', 'silent', 's'],
options: ['depth']
})
export class UserCommand extends Command {
public override async messageRun(message: Message, args: Args) {
const code = await args.rest('string');
const { result, success } = await this.eval(message, code, {
async: args.getFlags('async'),
depth: Number(args.getOption('depth')) ?? 0,
showHidden: args.getFlags('hidden', 'showHidden')
});
const output = success ? codeBlock('js', result) : `**ERROR**: ${codeBlock('bash', result)}`;
if (args.getFlags('silent', 's')) return null;
if (output.length > 2000) {
return send(message, {
content: `Output was too long... sent the result as a file.`,
files: [{ attachment: Buffer.from(output), name: 'output.js' }]
});
}
return send(message, `${output}`);
}
private async eval(message: Message, code: string, flags: { async: boolean; depth: number; showHidden: boolean }) {
if (flags.async) code = `(async () => {\n${code}\n})();`;
// @ts-expect-error value is never read, this is so `msg` is possible as an alias when sending the eval.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const msg = message;
let success = true;
let result = null;
try {
// eslint-disable-next-line no-eval
result = eval(code);
} catch (error) {
if (error && error instanceof Error && error.stack) {
this.container.client.logger.error(error);
}
result = error;
success = false;
}
if (isThenable(result)) result = await result;
if (typeof result !== 'string') {
result = inspect(result, {
depth: flags.depth,
showHidden: flags.showHidden
});
}
return { result, success };
}
}

View File

@ -0,0 +1,39 @@
import { ApplyOptions } from '@sapphire/decorators';
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
import { Command } from '@sapphire/framework';
import type { Message } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import { sendLoadingMessage } from '../../lib/utils.js';
@ApplyOptions<Command.Options>({
aliases: ['pm'],
description: 'A command that uses paginated messages.',
generateDashLessAliases: true
})
export class UserCommand extends Command {
public override async messageRun(message: Message) {
const response = await sendLoadingMessage(message);
const paginatedMessage = new PaginatedMessage({
template: new EmbedBuilder()
.setColor('#FF0000')
// Be sure to add a space so this is offset from the page numbers!
.setFooter({ text: ' footer after page numbers' })
});
paginatedMessage
.addPageEmbed((embed) =>
embed //
.setDescription('This is the first page')
.setTitle('Page 1')
)
.addPageBuilder((builder) =>
builder //
.setContent('This is the second page')
.setEmbeds([new EmbedBuilder().setTimestamp()])
);
await paginatedMessage.run(response, message.author);
return response;
}
}

View File

@ -0,0 +1,82 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Command } from '@sapphire/framework';
import { send } from '@sapphire/plugin-editable-commands';
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType, type Message } from 'discord.js';
@ApplyOptions<Command.Options>({
description: 'ping pong'
})
export class UserCommand extends Command {
// Register slash and context menu command
public override registerApplicationCommands(registry: Command.Registry) {
// Create shared integration types and contexts
// These allow the command to be used in guilds and DMs
const integrationTypes: ApplicationIntegrationType[] = [ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall];
const contexts: InteractionContextType[] = [
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
];
// Register slash command
registry.registerChatInputCommand({
name: this.name,
description: this.description,
integrationTypes,
contexts
});
// Register context menu command available from any message
registry.registerContextMenuCommand({
name: this.name,
type: ApplicationCommandType.Message,
integrationTypes,
contexts
});
// Register context menu command available from any user
registry.registerContextMenuCommand({
name: this.name,
type: ApplicationCommandType.User,
integrationTypes,
contexts
});
}
// Message command
public override async messageRun(message: Message) {
const msg = await send(message, 'Ping?');
const content = `Pong! Bot Latency ${Math.round(this.container.client.ws.ping)}ms. API Latency ${
(msg.editedTimestamp || msg.createdTimestamp) - (message.editedTimestamp || message.createdTimestamp)
}ms.`;
return send(message, content);
}
// slash command
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const msg = await interaction.reply({ content: 'Ping?', fetchReply: true });
const content = `Pong! Bot Latency ${Math.round(this.container.client.ws.ping)}ms. API Latency ${
msg.createdTimestamp - interaction.createdTimestamp
}ms.`;
return interaction.editReply({
content
});
}
// context menu command
public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) {
const msg = await interaction.reply({ content: 'Ping?', fetchReply: true });
const content = `Pong! Bot Latency ${Math.round(this.container.client.ws.ping)}ms. API Latency ${
msg.createdTimestamp - interaction.createdTimestamp
}ms.`;
return interaction.editReply({
content
});
}
}

42
apps/bot/src/index.ts Normal file
View File

@ -0,0 +1,42 @@
import './lib/setup.js';
import { container, LogLevel, SapphireClient } from '@sapphire/framework';
import { GatewayIntentBits, Partials } from 'discord.js';
const client = new SapphireClient({
defaultPrefix: '!',
regexPrefix: /^(hey +)?bot[,! ]/i,
caseInsensitiveCommands: true,
logger: {
level: LogLevel.Debug
},
shards: 'auto',
intents: [
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildExpressions,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.MessageContent
],
partials: [Partials.Channel],
loadMessageCommandListeners: true
});
const main = async () => {
try {
client.logger.info('Logging in');
await client.login(container.config.secrets.discord_token);
client.logger.info('logged in');
} catch (error) {
client.logger.fatal(error);
await client.destroy();
process.exit(1);
}
};
void main();

View File

@ -0,0 +1,6 @@
import { join } from 'path';
export const rootDir = join(__dirname, '..', '..');
export const srcDir = join(rootDir, 'src');
export const RandomLoadingMessage = ['Computing...', 'Thinking...', 'Cooking some food', 'Give me a moment', 'Loading...'];

0
apps/bot/src/lib/setup Normal file
View File

41
apps/bot/src/lib/setup.ts Normal file
View File

@ -0,0 +1,41 @@
// Unless explicitly defined, set NODE_ENV as development:
process.env.NODE_ENV ??= 'development';
import { ApplicationCommandRegistries, container, RegisterBehavior } from '@sapphire/framework';
import '@sapphire/plugin-api/register';
import '@sapphire/plugin-editable-commands/register';
import '@sapphire/plugin-logger/register';
import '@sapphire/plugin-subcommands/register';
import { setup, type ArrayString } from '@skyra/env-utilities';
import * as colorette from 'colorette';
import { join } from 'path';
import { inspect } from 'util';
import { srcDir } from './constants.js';
import { BotzillaConfig, loadConfig } from '@aura/config';
// Set default behavior to bulk overwrite
ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical(RegisterBehavior.BulkOverwrite);
// Read env var
setup({ path: join(srcDir, '.env') });
// Set default inspection depth
inspect.defaultOptions.depth = 1;
// Enable colorette
colorette.createColors({ useColor: true });
// init config
container.config = await loadConfig();
declare module '@skyra/env-utilities' {
interface Env {
OWNERS: ArrayString;
}
}
declare module '@sapphire/pieces' {
interface Container {
config: BotzillaConfig;
}
}

63
apps/bot/src/lib/utils.ts Normal file
View File

@ -0,0 +1,63 @@
import type { ChatInputCommandSuccessPayload, Command, ContextMenuCommandSuccessPayload, MessageCommandSuccessPayload } from '@sapphire/framework';
import { container } from '@sapphire/framework';
import { send } from '@sapphire/plugin-editable-commands';
import { cyan } from 'colorette';
import { EmbedBuilder, type APIUser, type Guild, type Message, type User } from 'discord.js';
import { RandomLoadingMessage } from './constants.js';
/**
* Picks a random item from an array
* @param array The array to pick a random item from
* @example
* const randomEntry = pickRandom([1, 2, 3, 4]) // 1
*/
export function pickRandom<T>(array: readonly T[]): T {
const { length } = array;
return array[Math.floor(Math.random() * length)];
}
/**
* Sends a loading message to the current channel
* @param message The message data for which to send the loading message
*/
export function sendLoadingMessage(message: Message): Promise<typeof message> {
return send(message, { embeds: [new EmbedBuilder().setDescription(pickRandom(RandomLoadingMessage)).setColor('#FF0000')] });
}
export function logSuccessCommand(payload: ContextMenuCommandSuccessPayload | ChatInputCommandSuccessPayload | MessageCommandSuccessPayload): void {
let successLoggerData: ReturnType<typeof getSuccessLoggerData>;
if ('interaction' in payload) {
successLoggerData = getSuccessLoggerData(payload.interaction.guild, payload.interaction.user, payload.command);
} else {
successLoggerData = getSuccessLoggerData(payload.message.guild, payload.message.author, payload.command);
}
container.logger.debug(`${successLoggerData.shard} - ${successLoggerData.commandName} ${successLoggerData.author} ${successLoggerData.sentAt}`);
}
export function getSuccessLoggerData(guild: Guild | null, user: User, command: Command) {
const shard = getShardInfo(guild?.shardId ?? 0);
const commandName = getCommandInfo(command);
const author = getAuthorInfo(user);
const sentAt = getGuildInfo(guild);
return { shard, commandName, author, sentAt };
}
function getShardInfo(id: number) {
return `[${cyan(id.toString())}]`;
}
function getCommandInfo(command: Command) {
return cyan(command.name);
}
function getAuthorInfo(author: User | APIUser) {
return `${author.username}[${cyan(author.id)}]`;
}
function getGuildInfo(guild: Guild | null) {
if (guild === null) return 'Direct Messages';
return `${guild.name}[${cyan(guild.id)}]`;
}

View File

@ -0,0 +1,23 @@
import type { ChatInputCommandDeniedPayload, Events } from '@sapphire/framework';
import { Listener, UserError } from '@sapphire/framework';
export class UserEvent extends Listener<typeof Events.ChatInputCommandDenied> {
public override async run({ context, message: content }: UserError, { interaction }: ChatInputCommandDeniedPayload) {
// `context: { silent: true }` should make UserError silent:
// Use cases for this are for example permissions error when running the `eval` command.
if (Reflect.get(Object(context), 'silent')) return;
if (interaction.deferred || interaction.replied) {
return interaction.editReply({
content,
allowedMentions: { users: [interaction.user.id], roles: [] }
});
}
return interaction.reply({
content,
allowedMentions: { users: [interaction.user.id], roles: [] },
ephemeral: true
});
}
}

View File

@ -0,0 +1,14 @@
import { Listener, LogLevel, type ChatInputCommandSuccessPayload } from '@sapphire/framework';
import type { Logger } from '@sapphire/plugin-logger';
import { logSuccessCommand } from '../../../lib/utils.js';
export class UserListener extends Listener {
public override run(payload: ChatInputCommandSuccessPayload) {
logSuccessCommand(payload);
}
public override onLoad() {
this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug;
return super.onLoad();
}
}

View File

@ -0,0 +1,23 @@
import type { ContextMenuCommandDeniedPayload, Events } from '@sapphire/framework';
import { Listener, UserError } from '@sapphire/framework';
export class UserEvent extends Listener<typeof Events.ContextMenuCommandDenied> {
public override async run({ context, message: content }: UserError, { interaction }: ContextMenuCommandDeniedPayload) {
// `context: { silent: true }` should make UserError silent:
// Use cases for this are for example permissions error when running the `eval` command.
if (Reflect.get(Object(context), 'silent')) return;
if (interaction.deferred || interaction.replied) {
return interaction.editReply({
content,
allowedMentions: { users: [interaction.user.id], roles: [] }
});
}
return interaction.reply({
content,
allowedMentions: { users: [interaction.user.id], roles: [] },
ephemeral: true
});
}
}

View File

@ -0,0 +1,14 @@
import { Listener, LogLevel, type ContextMenuCommandSuccessPayload } from '@sapphire/framework';
import type { Logger } from '@sapphire/plugin-logger';
import { logSuccessCommand } from '../../../lib/utils.js';
export class UserListener extends Listener {
public override run(payload: ContextMenuCommandSuccessPayload) {
logSuccessCommand(payload);
}
public override onLoad() {
this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug;
return super.onLoad();
}
}

View File

@ -0,0 +1,12 @@
import type { Events, MessageCommandDeniedPayload } from '@sapphire/framework';
import { Listener, type UserError } from '@sapphire/framework';
export class UserEvent extends Listener<typeof Events.MessageCommandDenied> {
public override async run({ context, message: content }: UserError, { message }: MessageCommandDeniedPayload) {
// `context: { silent: true }` should make UserError silent:
// Use cases for this are for example permissions error when running the `eval` command.
if (Reflect.get(Object(context), 'silent')) return;
return message.reply({ content, allowedMentions: { users: [message.author.id], roles: [] } });
}
}

View File

@ -0,0 +1,15 @@
import type { MessageCommandSuccessPayload } from '@sapphire/framework';
import { Listener, LogLevel } from '@sapphire/framework';
import type { Logger } from '@sapphire/plugin-logger';
import { logSuccessCommand } from '../../../lib/utils.js';
export class UserEvent extends Listener {
public override run(payload: MessageCommandSuccessPayload) {
logSuccessCommand(payload);
}
public override onLoad() {
this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug;
return super.onLoad();
}
}

View File

@ -0,0 +1,13 @@
import type { Events } from '@sapphire/framework';
import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js';
export class UserEvent extends Listener<typeof Events.MentionPrefixOnly> {
public override run(message: Message) {
// Do nothing if we cannot send messages in the channel (eg. group DMs)
if (!message.channel.isSendable()) return;
const prefix = this.container.client.options.defaultPrefix;
return message.channel.send(prefix ? `My prefix in this guild is: \`${prefix}\`` : 'Cannot find any Prefix for Message Commands.');
}
}

View File

@ -0,0 +1,51 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Listener } from '@sapphire/framework';
import type { StoreRegistryValue } from '@sapphire/pieces';
import { blue, gray, green, magenta, magentaBright, white, yellow } from 'colorette';
const dev = process.env.NODE_ENV !== 'production';
@ApplyOptions<Listener.Options>({ once: true })
export class UserEvent extends Listener {
private readonly style = dev ? yellow : blue;
public override run() {
this.printBanner();
this.printStoreDebugInformation();
}
private printBanner() {
const success = green('+');
const llc = dev ? magentaBright : white;
const blc = dev ? magenta : blue;
const line01 = llc('');
const line02 = llc('');
const line03 = llc('');
// Offset Pad
const pad = ' '.repeat(7);
console.log(
String.raw`
${line01} ${pad}${blc('1.0.0')}
${line02} ${pad}[${success}] Gateway
${line03}${dev ? ` ${pad}${blc('<')}${llc('/')}${blc('>')} ${llc('DEVELOPMENT MODE')}` : ''}
`.trim()
);
}
private printStoreDebugInformation() {
const { client, logger } = this.container;
const stores = [...client.stores.values()];
const last = stores.pop()!;
for (const store of stores) logger.info(this.styleStore(store, false));
logger.info(this.styleStore(last, true));
}
private styleStore(store: StoreRegistryValue, last: boolean) {
return gray(`${last ? '└─' : '├─'} Loaded ${this.style(store.size.toString().padEnd(3, ' '))} ${store.name}.`);
}
}

View File

@ -0,0 +1,30 @@
import { AllFlowsPrecondition } from '@sapphire/framework';
import { envParseArray } from '@skyra/env-utilities';
import type { CommandInteraction, ContextMenuCommandInteraction, Message, Snowflake } from 'discord.js';
export class UserPrecondition extends AllFlowsPrecondition {
#message = 'This command can only be used by the owner.';
#owners = this.container.config.global.owners;
public override chatInputRun(interaction: CommandInteraction) {
return this.doOwnerCheck(interaction.user.id);
}
public override contextMenuRun(interaction: ContextMenuCommandInteraction) {
return this.doOwnerCheck(interaction.user.id);
}
public override messageRun(message: Message) {
return this.doOwnerCheck(message.author.id);
}
private doOwnerCheck(userId: Snowflake) {
return this.#owners.includes(userId) ? this.ok() : this.error({ message: this.#message });
}
}
declare module '@sapphire/framework' {
interface Preconditions {
OwnerOnly: never;
}
}

View File

@ -0,0 +1,7 @@
import { Route } from '@sapphire/plugin-api';
export class UserRoute extends Route {
public override run(_request: Route.Request, response: Route.Response) {
response.json({ message: 'Hello World' });
}
}

View File

@ -0,0 +1,7 @@
import { Route } from '@sapphire/plugin-api';
export class UserRoute extends Route {
public override run(_request: Route.Request, response: Route.Response) {
response.json({ message: 'Hello World' });
}
}

View File

@ -0,0 +1,7 @@
import { Route } from '@sapphire/plugin-api';
export class UserRoute extends Route {
public override run(_request: Route.Request, response: Route.Response) {
response.json({ message: 'Landing Page!' });
}
}

View File

@ -0,0 +1,7 @@
import { Route } from '@sapphire/plugin-api';
export class UserRoute extends Route {
public override run(_request: Route.Request, response: Route.Response) {
response.json({ message: 'Landing Page!' });
}
}

10
apps/bot/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": ["@aura/tsconfig", "@aura/tsconfig/extra-strict", "@aura/tsconfig/decorators"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"target": "ES2024"
},
"include": ["src"]
}

17
apps/bot/tsup.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from 'tsup';
export default defineConfig({
clean: true,
bundle: false,
dts: false,
entry: ['src/**/*.ts', '!src/**/*.d.ts'],
format: ['esm'],
minify: false,
tsconfig: 'tsconfig.json',
target: 'es2024',
splitting: false,
skipNodeModulesBundle: true,
sourcemap: true,
shims: false,
keepNames: true
});

1266
bun.lock Normal file

File diff suppressed because it is too large Load Diff

17
config.toml Normal file
View File

@ -0,0 +1,17 @@
[global]
guild_ids = ["1072895701970858026", "739115569311383634"]
owners = ["424239181296959507"]
[global.colors]
success = "#a6da95"
warning = "#eed49f"
error = "#ed8796"
[secrets]
discord_token = "MTM1MzQ3Nzg1MDEzMjM4MTc5OA.GWeoGb.2lQ8w3b0O5bTM9QVO60X2nyf4X1e_YvnK8uK_E"
[logger]
level = "debug"
[logger.seq]
enabled = false

33
nx.json Normal file
View File

@ -0,0 +1,33 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"defaultBase": "master",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals"
],
"production": [
"default"
],
"sharedGlobals": []
},
"plugins": [
{
"plugin": "@nx/js/typescript",
"options": {
"typecheck": {
"targetName": "typecheck"
},
"build": {
"targetName": "build",
"configName": "tsconfig.lib.json",
"buildDepsName": "build-deps",
"watchDepsName": "watch-deps",
"dependsOn": [
"^typecheck"
]
}
}
}
]
}

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "@aura/source",
"version": "0.0.0",
"license": "MIT",
"scripts": {},
"private": true,
"dependencies": {
"@schema-hub/zod-error-formatter": "^0.0.8",
"zod": "^3.24.3"
},
"devDependencies": {
"@nx/js": "20.8.0",
"@swc-node/register": "~1.9.1",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
"nx": "20.8.0",
"prettier": "^2.6.2",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"@types/bun": "latest"
},
"workspaces": [
"packages/*",
"apps/*"
],
"trustedDependencies": [
"@sapphire/type",
"@swc/core",
"esbuild",
"nx"
],
"module": "index.ts",
"type": "module"
}

57
packages/config/index.ts Normal file
View File

@ -0,0 +1,57 @@
import { join } from 'node:path';
import { TOML } from 'bun';
import { safeParse } from '@schema-hub/zod-error-formatter';
import { BotzillaConfig, BotzillaConfigSchema } from './schemas/index.js';
async function loadConfigFile() {
const path = join(__dirname, '..', '..', '..', 'config.toml');
const file = Bun.file(path);
if (!(await file.exists())) {
throw new Error('conifg.toml does not exist');
}
return await file.text();
}
type TomlParseResult =
| {
success: true;
obj: object;
}
| {
success: false;
line: number;
column: number;
message: string;
};
function isToml(contents: string): TomlParseResult {
try {
const obj = TOML.parse(contents);
return { success: true, obj };
} catch ({ line, column, message }: any) {
return { success: false, line, column, message };
}
}
export async function loadConfig(): Promise<BotzillaConfig> {
const contents = await loadConfigFile();
const tomlParseResult = isToml(contents);
if (!tomlParseResult.success) {
throw new Error(
`Parsing error on line ${tomlParseResult.line}, column ${tomlParseResult.column}: ${tomlParseResult.message}`
);
}
const parseResult = safeParse(BotzillaConfigSchema, tomlParseResult.obj);
if (parseResult.success) return parseResult.data;
throw parseResult.error;
}
export * from './schemas/index.js';

View File

@ -0,0 +1,22 @@
{
"name": "@aura/config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"type": "module",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc --skipLibCheck"
},
"dependencies": {
"@aura/tsconfig": "workspace:*"
},
"exports": {
".": {
"default": "./dist/index.js",
"types": "./src/index.ts"
}
}
}

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
import { GlobalSchema } from './GlobalSchema.js';
import { LogggerSchema } from './LoggerSchema.js';
import { SecretsSchema } from './SecretsSchema.js';
export const BotzillaConfigSchema = z.object({
global: GlobalSchema,
logger: LogggerSchema,
secrets: SecretsSchema,
});

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
export const GlobalSchema = z.object({
guild_ids: z.string().array().optional().default([]),
owners: z.string().array().optional().default([]),
});
const hexString = z.string().startsWith('#');
export const ColorsSchema = z.object({
success: hexString,
warning: hexString,
error: hexString,
});

View File

@ -0,0 +1,21 @@
import { z } from 'zod';
export const SeqSchema = z
.object({
enabled: z.literal(false),
})
.or(
z.object({
enabled: z.literal(true),
host: z.string().url(),
api_key: z.string(),
})
);
export const LogggerSchema = z.object({
level: z
.enum(['verbose', 'debug', 'info', 'warn', 'error', 'fatal'])
.optional()
.default('debug'),
seq: SeqSchema,
});

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
export const SecretsSchema = z.object({
discord_token: z
.string()
.regex(
/(?<mfaToken>mfa\.[a-z0-9_-]{20,})|(?<basicToken>[a-z0-9_-]{23,28}\.[a-z0-9_-]{6,7}\.[a-z0-9_-]{27})/i
),
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
import { BotzillaConfigSchema } from './BotzillaConfig.js';
export type BotzillaConfig = z.infer<typeof BotzillaConfigSchema>;
// export schemas
export * from './GlobalSchema.js';
export * from './LoggerSchema.js';
export * from './SecretsSchema.js';
export * from './BotzillaConfig.js';

View File

@ -0,0 +1,8 @@
{
"extends": ["@aura/tsconfig", "@aura/tsconfig/extra-strict"],
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
}
}

View File

@ -0,0 +1,35 @@
{
"name": "@aura/tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"main": "src/tsconfig.json",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "echo nothing to build :3"
},
"exports": {
".": {
"types": "./src/tsconfig.json",
"import": "./src/tsconfig.json",
"require": "./src/tsconfig.json"
},
"./base": {
"types": "./src/tsconfig.json",
"import": "./src/tsconfig.json",
"require": "./src/tsconfig.json"
},
"./decorators": {
"types": "./src/decorators.json",
"import": "./src/decorators.json",
"require": "./src/decorators.json"
},
"./extra-strict": {
"types": "./src/extra-strict.json",
"import": "./src/extra-strict.json",
"require": "./src/extra-strict.json"
}
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}

View File

@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json",
"compilerOptions": {
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true
}
}

View File

@ -0,0 +1,29 @@
{
"compileOnSave": true,
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"importHelpers": false,
"incremental": true,
"lib": ["esnext"],
"module": "Node16",
"moduleResolution": "Node16",
"newLine": "lf",
"noEmitHelpers": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"pretty": true,
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "ES2020",
"useDefineForClassFields": true
}
}

View File