first commit
This commit is contained in:
commit
8a8e67b47e
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal 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
5
.prettierignore
Normal 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
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode"]
|
||||
}
|
||||
109
README.md
Normal file
109
README.md
Normal 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&utm_medium=readme&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 »](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 »](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 »](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&utm_medium=readme&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
4
apps/bot/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.yarn/
|
||||
examples/*/dist/
|
||||
13
apps/bot/.sapphirerc.yml
Normal file
13
apps/bot/.sapphirerc.yml
Normal 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
30
apps/bot/README.md
Normal 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
47
apps/bot/package.json
Normal 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
3
apps/bot/src/.env
Normal file
@ -0,0 +1,3 @@
|
||||
# Tokens
|
||||
DISCORD_TOKEN=
|
||||
OWNERS=
|
||||
59
apps/bot/src/commands/General/command-with-decorators.ts
Normal file
59
apps/bot/src/commands/General/command-with-decorators.ts
Normal 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!');
|
||||
}
|
||||
}
|
||||
50
apps/bot/src/commands/General/command-with-subcommands.ts
Normal file
50
apps/bot/src/commands/General/command-with-subcommands.ts
Normal 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!');
|
||||
}
|
||||
}
|
||||
70
apps/bot/src/commands/General/eval.ts
Normal file
70
apps/bot/src/commands/General/eval.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
39
apps/bot/src/commands/General/paginated-message.ts
Normal file
39
apps/bot/src/commands/General/paginated-message.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
82
apps/bot/src/commands/General/ping.ts
Normal file
82
apps/bot/src/commands/General/ping.ts
Normal 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
42
apps/bot/src/index.ts
Normal 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();
|
||||
6
apps/bot/src/lib/constants.ts
Normal file
6
apps/bot/src/lib/constants.ts
Normal 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
0
apps/bot/src/lib/setup
Normal file
41
apps/bot/src/lib/setup.ts
Normal file
41
apps/bot/src/lib/setup.ts
Normal 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
63
apps/bot/src/lib/utils.ts
Normal 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)}]`;
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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: [] } });
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
13
apps/bot/src/listeners/mentionPrefixOnly.ts
Normal file
13
apps/bot/src/listeners/mentionPrefixOnly.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
51
apps/bot/src/listeners/ready.ts
Normal file
51
apps/bot/src/listeners/ready.ts
Normal 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}.`);
|
||||
}
|
||||
}
|
||||
30
apps/bot/src/preconditions/OwnerOnly.ts
Normal file
30
apps/bot/src/preconditions/OwnerOnly.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
7
apps/bot/src/routes/hello-world.get.ts
Normal file
7
apps/bot/src/routes/hello-world.get.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
7
apps/bot/src/routes/hello-world.post.ts
Normal file
7
apps/bot/src/routes/hello-world.post.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
7
apps/bot/src/routes/index.get.ts
Normal file
7
apps/bot/src/routes/index.get.ts
Normal 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!' });
|
||||
}
|
||||
}
|
||||
7
apps/bot/src/routes/index.post.ts
Normal file
7
apps/bot/src/routes/index.post.ts
Normal 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
10
apps/bot/tsconfig.json
Normal 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
17
apps/bot/tsup.config.ts
Normal 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
|
||||
});
|
||||
17
config.toml
Normal file
17
config.toml
Normal 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
33
nx.json
Normal 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
34
package.json
Normal 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
57
packages/config/index.ts
Normal 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';
|
||||
22
packages/config/package.json
Normal file
22
packages/config/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/config/schemas/BotzillaConfig.ts
Normal file
10
packages/config/schemas/BotzillaConfig.ts
Normal 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,
|
||||
});
|
||||
13
packages/config/schemas/GlobalSchema.ts
Normal file
13
packages/config/schemas/GlobalSchema.ts
Normal 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,
|
||||
});
|
||||
21
packages/config/schemas/LoggerSchema.ts
Normal file
21
packages/config/schemas/LoggerSchema.ts
Normal 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,
|
||||
});
|
||||
9
packages/config/schemas/SecretsSchema.ts
Normal file
9
packages/config/schemas/SecretsSchema.ts
Normal 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
|
||||
),
|
||||
});
|
||||
10
packages/config/schemas/index.ts
Normal file
10
packages/config/schemas/index.ts
Normal 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';
|
||||
8
packages/config/tsconfig.json
Normal file
8
packages/config/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["@aura/tsconfig", "@aura/tsconfig/extra-strict"],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
35
packages/tsconfig/package.json
Normal file
35
packages/tsconfig/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
packages/tsconfig/src/decorators.json
Normal file
7
packages/tsconfig/src/decorators.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
9
packages/tsconfig/src/extra-strict.json
Normal file
9
packages/tsconfig/src/extra-strict.json
Normal 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
|
||||
}
|
||||
}
|
||||
29
packages/tsconfig/src/tsconfig.json
Normal file
29
packages/tsconfig/src/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
0
packages/tsconfig/tsconfig.json
Normal file
0
packages/tsconfig/tsconfig.json
Normal file
Loading…
Reference in New Issue
Block a user