Initial
This commit is contained in:
commit
0f8e912093
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Build Outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Turborepo starter
|
||||||
|
|
||||||
|
This is an official starter Turborepo.
|
||||||
|
|
||||||
|
## Using this example
|
||||||
|
|
||||||
|
Run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx create-turbo@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's inside?
|
||||||
|
|
||||||
|
This Turborepo includes the following packages/apps:
|
||||||
|
|
||||||
|
### Apps and Packages
|
||||||
|
|
||||||
|
- `docs`: a [Next.js](https://nextjs.org/) app
|
||||||
|
- `web`: another [Next.js](https://nextjs.org/) app
|
||||||
|
- `@wyvern/ui`: a stub React component library shared by both `web` and `docs` applications
|
||||||
|
- `@wyvern/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
||||||
|
- `@wyvern/typescript-config`: `tsconfig.json`s used throughout the monorepo
|
||||||
|
|
||||||
|
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
This Turborepo has some additional tools already setup for you:
|
||||||
|
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||||
|
- [ESLint](https://eslint.org/) for code linting
|
||||||
|
- [Prettier](https://prettier.io) for code formatting
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
To build all apps and packages, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd my-turborepo
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Develop
|
||||||
|
|
||||||
|
To develop all apps and packages, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd my-turborepo
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Caching
|
||||||
|
|
||||||
|
Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
|
||||||
|
|
||||||
|
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd my-turborepo
|
||||||
|
npx turbo login
|
||||||
|
```
|
||||||
|
|
||||||
|
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
|
||||||
|
|
||||||
|
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx turbo link
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
Learn more about the power of Turborepo:
|
||||||
|
|
||||||
|
- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
|
||||||
|
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
|
||||||
|
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
|
||||||
|
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
|
||||||
|
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
|
||||||
|
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)
|
||||||
15
apps/bot/.sapphire/templates/arguments/argument.ts
Normal file
15
apps/bot/.sapphire/templates/arguments/argument.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Argument } from '@sapphire/framework';
|
||||||
|
|
||||||
|
@ApplyOptions<Argument.Options>({})
|
||||||
|
export class UserArgument extends Argument<string> {
|
||||||
|
public override run(parameter: string) {
|
||||||
|
return this.ok(parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface ArgType {
|
||||||
|
/*{{name}}*/: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { ApplicationCommandType } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
description: 'A basic contextMenu command'
|
||||||
|
})
|
||||||
|
export class UserCommand extends Command {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerContextMenuCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setType(ApplicationCommandType.Message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) {
|
||||||
|
return interaction.reply({ content: 'Hello world!' });
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/bot/.sapphire/templates/commands/message-command.ts
Normal file
12
apps/bot/.sapphire/templates/commands/message-command.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import type { Message } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
description: 'A basic command'
|
||||||
|
})
|
||||||
|
export class UserCommand extends Command {
|
||||||
|
public override async messageRun(message: Message) {
|
||||||
|
return message.channel.send('Hello world!');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
apps/bot/.sapphire/templates/commands/slash-command.ts
Normal file
19
apps/bot/.sapphire/templates/commands/slash-command.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
description: 'A basic slash command'
|
||||||
|
})
|
||||||
|
export class UserCommand extends Command {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
|
||||||
|
return interaction.reply({ content: 'Hello world!' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
|
||||||
|
import { AutocompleteInteraction, type ApplicationCommandOptionChoiceData } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.Autocomplete
|
||||||
|
})
|
||||||
|
export class AutocompleteHandler extends InteractionHandler {
|
||||||
|
public override async run(interaction: AutocompleteInteraction, result: ApplicationCommandOptionChoiceData[]) {
|
||||||
|
return interaction.respond(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async parse(interaction: AutocompleteInteraction) {
|
||||||
|
// Only run this interaction for the command with ID '1000000000000000000'
|
||||||
|
if (interaction.commandId !== '1000000000000000000') return this.none();
|
||||||
|
// Get the focussed (current) option
|
||||||
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
|
// Ensure that the option name is one that can be autocompleted, or return none if not.
|
||||||
|
switch (focusedOption.name) {
|
||||||
|
case 'search': {
|
||||||
|
// Search your API or similar. This is example code!
|
||||||
|
const searchResult = await myApi.searchForSomething(focusedOption.value);
|
||||||
|
// Map the search results to the structure required for Autocomplete
|
||||||
|
return this.some(searchResult.map((match) => ({ name: match.name, value: match.key })));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return this.none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
|
||||||
|
import type { ButtonInteraction } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.Button
|
||||||
|
})
|
||||||
|
export class ButtonHandler extends InteractionHandler {
|
||||||
|
public async run(interaction: ButtonInteraction) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Hello from a button interaction handler!',
|
||||||
|
// Let's make it so only the person who pressed the button can see this message!
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override parse(interaction: ButtonInteraction) {
|
||||||
|
if (interaction.customId !== 'my-awesome-button') return this.none();
|
||||||
|
|
||||||
|
return this.some();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
|
||||||
|
import type { ModalSubmitInteraction } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.ModalSubmit
|
||||||
|
})
|
||||||
|
export class ModalHandler extends InteractionHandler {
|
||||||
|
public async run(interaction: ModalSubmitInteraction) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Thank you for submitting the form!',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override parse(interaction: ModalSubmitInteraction) {
|
||||||
|
if (interaction.customId !== 'hello-popup') return this.none();
|
||||||
|
|
||||||
|
return this.some();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
|
||||||
|
import type { StringSelectMenuInteraction } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.SelectMenu
|
||||||
|
})
|
||||||
|
export class MenuHandler extends InteractionHandler {
|
||||||
|
public override async run(interaction: StringSelectMenuInteraction) {
|
||||||
|
await interaction.reply({
|
||||||
|
// Remember how we can have multiple values? Let's get the first one!
|
||||||
|
content: `You selected: ${interaction.values[0]}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override parse(interaction: StringSelectMenuInteraction) {
|
||||||
|
if (interaction.customId !== 'my-echo-select') return this.none();
|
||||||
|
|
||||||
|
return this.some();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/bot/.sapphire/templates/listeners/listener.ts
Normal file
7
apps/bot/.sapphire/templates/listeners/listener.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Listener } from '@sapphire/framework';
|
||||||
|
|
||||||
|
@ApplyOptions<Listener.Options>({})
|
||||||
|
export class UserEvent extends Listener {
|
||||||
|
public override run() {}
|
||||||
|
}
|
||||||
22
apps/bot/.sapphire/templates/preconditions/precondition.ts
Normal file
22
apps/bot/.sapphire/templates/preconditions/precondition.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Precondition } from '@sapphire/framework';
|
||||||
|
import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
|
||||||
|
|
||||||
|
export class UserPrecondition extends Precondition {
|
||||||
|
public override messageRun(message: Message) {
|
||||||
|
return this.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
return this.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override contextMenuRun(interaction: ContextMenuCommandInteraction) {
|
||||||
|
return this.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface Preconditions {
|
||||||
|
/*{{name}}*/: never;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/bot/.sapphire/templates/routes/route.ts
Normal file
15
apps/bot/.sapphire/templates/routes/route.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { methods, Route, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
|
||||||
|
|
||||||
|
@ApplyOptions<Route.Options>({
|
||||||
|
route: '/*{{name}}*/'
|
||||||
|
})
|
||||||
|
export class UserRoute extends Route {
|
||||||
|
public [methods.GET](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Hello World' });
|
||||||
|
}
|
||||||
|
|
||||||
|
public [methods.POST](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Hello World' });
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/bot/README.md
Normal file
32
apps/bot/README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# TypeScript Complete Sapphire Bot example
|
||||||
|
|
||||||
|
This is a more complete setup of a Discord bot using the [sapphire framework][sapphire] written in TypeScript.
|
||||||
|
|
||||||
|
It is similar to the [starter setup](../with-typescript-starter/), but adds more data structures and a more complete setup.
|
||||||
|
|
||||||
|
## How to use it?
|
||||||
|
|
||||||
|
### Prerequisite
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
This example can be run with `tsc-watch` to watch the files and automatically restart your bot.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run watch:start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
71
apps/bot/package.json
Normal file
71
apps/bot/package.json
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "@wyvern/bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"author": "@sapphire",
|
||||||
|
"license": "UNLICENSE",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@wyvern/config": "*",
|
||||||
|
"@wyvern/database": "*",
|
||||||
|
"@wyvern/plugin-custom-logger": "*",
|
||||||
|
"@wyvern/urban-dictionary": "*",
|
||||||
|
"@wyvern/error-handler": "*",
|
||||||
|
"@sapphire/decorators": "^6.0.4",
|
||||||
|
"@sapphire/discord-utilities": "^3.2.2",
|
||||||
|
"@sapphire/discord.js-utilities": "7.1.6",
|
||||||
|
"@sapphire/fetch": "^3.0.2",
|
||||||
|
"@sapphire/framework": "^5.0.7",
|
||||||
|
"@sapphire/plugin-api": "^6.1.1",
|
||||||
|
"@sapphire/plugin-editable-commands": "^4.0.2",
|
||||||
|
"@sapphire/plugin-logger": "^4.0.2",
|
||||||
|
"@sapphire/plugin-subcommands": "^6.0.3",
|
||||||
|
"@sapphire/string-store": "^1.0.1",
|
||||||
|
"@sapphire/time-utilities": "^1.7.12",
|
||||||
|
"@sapphire/type": "^2.4.4",
|
||||||
|
"@sapphire/utilities": "^3.15.3",
|
||||||
|
"@skyra/env-utilities": "^1.3.0",
|
||||||
|
"colorette": "^2.0.20",
|
||||||
|
"discord.js": "^14.14.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sapphire/cli": "^1.9.3",
|
||||||
|
"@sapphire/prettier-config": "^2.0.0",
|
||||||
|
"@sapphire/ts-config": "^5.0.0",
|
||||||
|
"@types/node": "^20.11.5",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"npm-run-all2": "^6.1.1",
|
||||||
|
"prettier": "^3.2.4",
|
||||||
|
"tsc-watch": "^6.0.4",
|
||||||
|
"typescript": "~5.4.5",
|
||||||
|
"tsup": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"sapphire": "sapphire",
|
||||||
|
"generate": "sapphire generate",
|
||||||
|
"build": "tsup",
|
||||||
|
"watch": "tsup --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsup --watch --onSuccess \"node ./dist/index.js\"",
|
||||||
|
"watch:start": "tsc-watch --onSuccess \"node ./dist/index.js\"",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
|
"typecheck": "tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#lib/structures": "./dist/lib/structures/index.js",
|
||||||
|
"#lib/interfaces": "./dist/lib/interfaces/index.js",
|
||||||
|
"#lib/parsers": "./dist/lib/parsers/index.js",
|
||||||
|
"#lib/errors": "./dist/lib/errors/index.js",
|
||||||
|
"#lib/managers": "./dist/lib/managers/index.js",
|
||||||
|
"#lib/utils": "./dist/lib/utils.js",
|
||||||
|
"#lib/config": "./dist/lib/config.js",
|
||||||
|
"#lib/constants": "./dist/lib/constants.js",
|
||||||
|
"#lib/types": "./dist/lib/types.js",
|
||||||
|
"#lib/customIds": "./dist/lib/customIdTypes/index.js",
|
||||||
|
"#lib/markdown": "./dist/lib/markdown/index.js",
|
||||||
|
"#drizzle": "./dist/drizzle/schema.js",
|
||||||
|
"#prisma": "./dist/lib/prisma.js",
|
||||||
|
"#stringFormatters": "./dist/lib/stringFormatters/index.js"
|
||||||
|
},
|
||||||
|
"prettier": "@sapphire/prettier-config"
|
||||||
|
}
|
||||||
38
apps/bot/sapphire.toml
Normal file
38
apps/bot/sapphire.toml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[project]
|
||||||
|
language = "ts"
|
||||||
|
module_system = "cjs"
|
||||||
|
base = "src"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
|
||||||
|
[categories]
|
||||||
|
arguments = { source_path = "arguments", target_path = "arguments" }
|
||||||
|
commands = { source_path = "commands", target_path = "commands" }
|
||||||
|
interaction-handlers = { source_path = "interaction-handlers", target_path = "interaction-handlers" }
|
||||||
|
listeners = { source_path = "listeners", target_path = "listeners" }
|
||||||
|
preconditions = { source_path = "preconditions", target_path = "preconditions" }
|
||||||
|
routes = { source_path = "routes", target_path = "routes" }
|
||||||
|
|
||||||
|
[templates.arguments]
|
||||||
|
argument = { aliases = ["a", "arg"] }
|
||||||
|
|
||||||
|
[templates.commands]
|
||||||
|
context-menu-command = { aliases = ["cmc", "context", "contextmenu", "contextmenucommand"] }
|
||||||
|
message-command = { aliases = ["mc", "message", "messagecommand"] }
|
||||||
|
slash-command = { aliases = ["sc", "command", "slash", "slashcommand"] }
|
||||||
|
|
||||||
|
[templates.interaction-handlers]
|
||||||
|
autocomplete-interaction-handler = { aliases = ["aih", "auto", "autocomplete"] }
|
||||||
|
button-interaction-handler = { aliases = ["bih", "button"] }
|
||||||
|
modal-interaction-handler = { aliases = ["mih", "modal"] }
|
||||||
|
select-menu-interaction-handler = { aliases = ["smih", "select"] }
|
||||||
|
|
||||||
|
[templates.listeners]
|
||||||
|
listener = { aliases = ["l"] }
|
||||||
|
|
||||||
|
[templates.preconditions]
|
||||||
|
precondition = { aliases = ["p"] }
|
||||||
|
|
||||||
|
[templates.routes]
|
||||||
|
route = { aliases = ["r"] }
|
||||||
|
|
||||||
24
apps/bot/src/commands/Admin/Admin.ts
Normal file
24
apps/bot/src/commands/Admin/Admin.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { adminErrorTest, adminSyncDatabase } from './_index.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Subcommand.Options>({
|
||||||
|
description: 'Admin commands',
|
||||||
|
preconditions: ['OwnerOnly'],
|
||||||
|
subcommands: [
|
||||||
|
{ name: 'error_test', chatInputRun: adminErrorTest },
|
||||||
|
{ name: 'sync_database', chatInputRun: adminSyncDatabase }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserCommand extends Subcommand {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addSubcommand((b) => b.setName('error_test').setDescription('Throw an error'))
|
||||||
|
.addSubcommand((b) => b.setName('sync_database').setDescription('Sync the database'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/bot/src/commands/Admin/_adminErrorTest.ts
Normal file
7
apps/bot/src/commands/Admin/_adminErrorTest.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
|
||||||
|
export function adminErrorTest(_i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const a: string = null!;
|
||||||
|
|
||||||
|
a!.charAt(1);
|
||||||
|
}
|
||||||
44
apps/bot/src/commands/Admin/_adminSyncDatabase.ts
Normal file
44
apps/bot/src/commands/Admin/_adminSyncDatabase.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { InternalError } from '#lib/errors';
|
||||||
|
import { container, Result } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { prismaClient } from '@wyvern/database';
|
||||||
|
|
||||||
|
export async function adminSyncDatabase(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const res = await Result.fromAsync(container.client.guilds.fetch());
|
||||||
|
const guildIds = res.match({
|
||||||
|
ok: (x) => x.map((g) => g.id),
|
||||||
|
err: (e) => {
|
||||||
|
throw new InternalError(`${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const guildCreateManyArgs = guildIds.map((id) => {
|
||||||
|
return { guildId: id };
|
||||||
|
});
|
||||||
|
|
||||||
|
const welcomeMessageCreateManyArgs = guildIds.map((id) => {
|
||||||
|
return { guildId: id };
|
||||||
|
});
|
||||||
|
|
||||||
|
const leaveMessageCreateManyArgs = guildIds.map((id) => {
|
||||||
|
return { guildId: id };
|
||||||
|
});
|
||||||
|
|
||||||
|
await prismaClient.guild.createMany({
|
||||||
|
data: guildCreateManyArgs,
|
||||||
|
skipDuplicates: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await prismaClient.guildWelcomeMessage.createMany({
|
||||||
|
data: welcomeMessageCreateManyArgs
|
||||||
|
});
|
||||||
|
|
||||||
|
await prismaClient.guildLeaveMessage.createMany({
|
||||||
|
data: leaveMessageCreateManyArgs
|
||||||
|
});
|
||||||
|
|
||||||
|
await i.reply({
|
||||||
|
content: 'Synced database',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
2
apps/bot/src/commands/Admin/_index.ts
Normal file
2
apps/bot/src/commands/Admin/_index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './_adminErrorTest.js';
|
||||||
|
export * from './_adminSyncDatabase.js';
|
||||||
64
apps/bot/src/commands/Info/ping.ts
Normal file
64
apps/bot/src/commands/Info/ping.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createEmbed } from '#lib/utils';
|
||||||
|
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { prismaClient } from '@wyvern/database';
|
||||||
|
import { codeBlock, Status } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
description: 'ping pong'
|
||||||
|
})
|
||||||
|
export class UserCommand extends Command {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand({
|
||||||
|
name: this.name,
|
||||||
|
description: this.description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async chatInputRun(i: Command.ChatInputCommandInteraction) {
|
||||||
|
const { client } = this.container;
|
||||||
|
const dbTime = performance.now();
|
||||||
|
await prismaClient.$queryRaw`SELECT 1`;
|
||||||
|
const dbTiming = performance.now() - dbTime;
|
||||||
|
|
||||||
|
const waitEmbed = createEmbed(i.user).setDescription('🏓 Pong!...');
|
||||||
|
const message = await i.reply({ embeds: [waitEmbed] });
|
||||||
|
const thisServerShard = client.ws.shards.get(i.guild!.shardId);
|
||||||
|
|
||||||
|
if (!thisServerShard) throw 'Unable to get server shard';
|
||||||
|
|
||||||
|
const pingMessage = createEmbed(i.user).addFields([
|
||||||
|
{
|
||||||
|
name: 'Host Latency',
|
||||||
|
value: codeBlock('yaml', client.ws.ping > 0 ? `${Math.floor(client.ws.ping)}ms` : 'Calculating...'),
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Client Latency',
|
||||||
|
value: codeBlock('yaml', `${Math.floor(message.createdTimestamp - i.createdTimestamp)}ms`),
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Database Latency',
|
||||||
|
value: codeBlock('yaml', `${Math.floor(dbTiming)}ms`),
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Websocket',
|
||||||
|
value: codeBlock('yaml', `${Status[thisServerShard.status]}`),
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Shard',
|
||||||
|
value: codeBlock(
|
||||||
|
'yaml',
|
||||||
|
`${thisServerShard.id}/${client.ws.shards.size} (${thisServerShard.ping > 0 ? `${Math.floor(thisServerShard.ping)}ms` : 'Calculating...'})`
|
||||||
|
),
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await message.edit({ embeds: [pingMessage] });
|
||||||
|
}
|
||||||
|
}
|
||||||
81
apps/bot/src/commands/Mod/Report.ts
Normal file
81
apps/bot/src/commands/Mod/Report.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { InternalError } from '#lib/errors';
|
||||||
|
import { compressCustomId } from '#lib/utils';
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
InteractionContextType,
|
||||||
|
Message,
|
||||||
|
MessageContextMenuCommandInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
User,
|
||||||
|
UserContextMenuCommandInteraction
|
||||||
|
} from 'discord.js';
|
||||||
|
import { ReportMessageCustomIdParams, ReportUserCustomIdParams } from 'src/lib/customIdParams/report.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
description: 'Report',
|
||||||
|
preconditions: [{ name: 'RequiredSetting', context: { setting: 'reportChannel' } }]
|
||||||
|
})
|
||||||
|
export class UserCommand extends Command {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
// register user context menu
|
||||||
|
registry.registerContextMenuCommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('Report User')
|
||||||
|
.setType(2) // User context menu - idk why i cant use ApplicationCommandType.User here
|
||||||
|
.setContexts(InteractionContextType.Guild)
|
||||||
|
);
|
||||||
|
|
||||||
|
// register message context menu
|
||||||
|
registry.registerContextMenuCommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('Report Message')
|
||||||
|
.setType(3) // Message context menu - idk why i cant use ApplicationCommandType.Message here
|
||||||
|
.setContexts(InteractionContextType.Guild)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async contextMenuRun(i: Command.ContextMenuCommandInteraction) {
|
||||||
|
if (i.isMessageContextMenuCommand()) this._messageReport(i.targetMessage, i);
|
||||||
|
else if (i.isUserContextMenuCommand()) this._userReport(i.targetUser, i);
|
||||||
|
else throw new InternalError('Interaction was an unexpected type');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _messageReport(msg: Message, i: MessageContextMenuCommandInteraction) {
|
||||||
|
const row = new ActionRowBuilder<TextInputBuilder>().setComponents([
|
||||||
|
new TextInputBuilder().setLabel('Reason').setPlaceholder('Why are you reporting this message').setRequired(true).setCustomId('reason')
|
||||||
|
]);
|
||||||
|
|
||||||
|
const idRes = compressCustomId<ReportMessageCustomIdParams>({
|
||||||
|
name: 'report-msg',
|
||||||
|
channelId: msg.channelId,
|
||||||
|
messageId: msg.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idRes.isErr()) throw new InternalError(idRes.unwrapErr());
|
||||||
|
|
||||||
|
const modal = new ModalBuilder().setTitle('Report Message').setComponents(row).setCustomId(idRes.unwrap());
|
||||||
|
|
||||||
|
await i.showModal(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _userReport(user: User, i: UserContextMenuCommandInteraction) {
|
||||||
|
const row = new ActionRowBuilder<TextInputBuilder>().setComponents([
|
||||||
|
new TextInputBuilder().setLabel('Reason').setPlaceholder('Why are you reporting this user').setRequired(true).setCustomId('reason')
|
||||||
|
]);
|
||||||
|
|
||||||
|
const idRes = compressCustomId<ReportUserCustomIdParams>({
|
||||||
|
name: 'report-user',
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idRes.isErr()) throw new InternalError(idRes.unwrapErr());
|
||||||
|
|
||||||
|
const modal = new ModalBuilder().setTitle('Report User').setComponents(row).setCustomId(idRes.unwrap());
|
||||||
|
|
||||||
|
await i.showModal(modal);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
apps/bot/src/commands/Settings/Settings.ts
Normal file
137
apps/bot/src/commands/Settings/Settings.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import {
|
||||||
|
autoRole_create,
|
||||||
|
autoRole_remove,
|
||||||
|
reportChannel_set,
|
||||||
|
reportChannel_unset,
|
||||||
|
welcomeMessage_disable,
|
||||||
|
welcomeMessage_enable,
|
||||||
|
welcomeMessage_preview,
|
||||||
|
welcomeMessage_setColor,
|
||||||
|
welcomeMessage_setMessage,
|
||||||
|
welcomeMessage_setTitle
|
||||||
|
} from './_index.js';
|
||||||
|
import { ChannelType } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Subcommand.Options>({
|
||||||
|
description: 'Tag commands',
|
||||||
|
requiredUserPermissions: ['ManageGuild'],
|
||||||
|
subcommands: [
|
||||||
|
{
|
||||||
|
name: 'auto_role',
|
||||||
|
type: 'group',
|
||||||
|
entries: [
|
||||||
|
{ name: 'create', chatInputRun: autoRole_create },
|
||||||
|
{ name: 'remove', chatInputRun: autoRole_remove }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'report_channel',
|
||||||
|
type: 'group',
|
||||||
|
entries: [
|
||||||
|
{ name: 'set', chatInputRun: reportChannel_set },
|
||||||
|
{ name: 'unset', chatInputRun: reportChannel_unset }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'welcome_message',
|
||||||
|
type: 'group',
|
||||||
|
entries: [
|
||||||
|
{ name: 'enable', chatInputRun: welcomeMessage_enable },
|
||||||
|
{ name: 'disable', chatInputRun: welcomeMessage_disable },
|
||||||
|
{ name: 'preview', chatInputRun: welcomeMessage_preview },
|
||||||
|
|
||||||
|
{ name: 'set_title', chatInputRun: welcomeMessage_setTitle },
|
||||||
|
{ name: 'set_message', chatInputRun: welcomeMessage_setMessage },
|
||||||
|
{ name: 'set_color', chatInputRun: welcomeMessage_setColor }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserCommand extends Subcommand {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addSubcommandGroup((b) =>
|
||||||
|
b //
|
||||||
|
.setName('auto_role')
|
||||||
|
.setDescription('Auto Role commands')
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('create')
|
||||||
|
.setDescription('Create an auto role')
|
||||||
|
.addRoleOption((b) => b.setName('role').setDescription('The role to create').setRequired(true))
|
||||||
|
)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('remove')
|
||||||
|
.setDescription('Remove an auto role')
|
||||||
|
.addRoleOption((b) => b.setName('role').setDescription('The role to remove').setRequired(true))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommandGroup((b) =>
|
||||||
|
b //
|
||||||
|
.setName('report_channel')
|
||||||
|
.setDescription('report channel commands')
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('set')
|
||||||
|
.setDescription('Set the report channel')
|
||||||
|
.addChannelOption((b) =>
|
||||||
|
b.setName('channel').setDescription('The channel to set').setRequired(true).addChannelTypes(ChannelType.GuildText)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('unset')
|
||||||
|
.setDescription('Unset the report channel')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommandGroup((b) =>
|
||||||
|
b //
|
||||||
|
.setName('welcome_message')
|
||||||
|
.setDescription('Welcome message commands')
|
||||||
|
.addSubcommand((b) => b.setName('enable').setDescription('Enable the welcome message'))
|
||||||
|
.addSubcommand((b) => b.setName('disable').setDescription('Disable the welcome message'))
|
||||||
|
.addSubcommand((b) => b.setName('preview').setDescription('Preview the welcome message'))
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('set_title')
|
||||||
|
.setDescription('Set the title of the welcome message')
|
||||||
|
.addStringOption((b) =>
|
||||||
|
b //
|
||||||
|
.setName('title')
|
||||||
|
.setDescription('The title')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('set_message')
|
||||||
|
.setDescription('Set the message of the welcome message')
|
||||||
|
.addStringOption((b) =>
|
||||||
|
b //
|
||||||
|
.setName('message')
|
||||||
|
.setDescription('The message')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('set_color')
|
||||||
|
.setDescription('Set the color of the welcome message')
|
||||||
|
.addStringOption((b) =>
|
||||||
|
b //
|
||||||
|
.setName('color')
|
||||||
|
.setDescription('The color')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/bot/src/commands/Settings/_index.ts
Normal file
12
apps/bot/src/commands/Settings/_index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export * from './autoRole/_create.js';
|
||||||
|
export * from './autoRole/_remove.js';
|
||||||
|
|
||||||
|
export * from './reportChannel/_set.js';
|
||||||
|
export * from './reportChannel/_unset.js';
|
||||||
|
|
||||||
|
export * from './welcomeMessage/_enable.js';
|
||||||
|
export * from './welcomeMessage/_disable.js';
|
||||||
|
export * from './welcomeMessage/_setTitle.js';
|
||||||
|
export * from './welcomeMessage/_setMessage.js';
|
||||||
|
export * from './welcomeMessage/_setColor.js';
|
||||||
|
export * from './welcomeMessage/_preview.js';
|
||||||
27
apps/bot/src/commands/Settings/autoRole/_create.ts
Normal file
27
apps/bot/src/commands/Settings/autoRole/_create.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { CommandFailedError, InternalError } from '#lib/errors';
|
||||||
|
import { createEmbed } from '#lib/utils';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { createAutoRole, getGuild, prismaClient } from '@wyvern/database';
|
||||||
|
import { mention } from '@wyvern/markdown';
|
||||||
|
|
||||||
|
export async function autoRole_create(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const role = i.options.getRole('role', true);
|
||||||
|
|
||||||
|
const guildRes = await getGuild(i.guild!.id);
|
||||||
|
|
||||||
|
if (guildRes.isErr()) {
|
||||||
|
throw new InternalError(guildRes.unwrapErr().message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { autoRoles: existingRoles } = guildRes.unwrap();
|
||||||
|
|
||||||
|
if (existingRoles.includes(role.id)) throw new CommandFailedError('That role already exists');
|
||||||
|
|
||||||
|
await prismaClient.$queryRawTyped(createAutoRole(role.id, i.guild!.id));
|
||||||
|
|
||||||
|
const embed = createEmbed(i.user)
|
||||||
|
.setTitle('Auto Role Created')
|
||||||
|
.setDescription(`${mention('role', role.id)} will now be given to new members!`);
|
||||||
|
|
||||||
|
await i.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
27
apps/bot/src/commands/Settings/autoRole/_remove.ts
Normal file
27
apps/bot/src/commands/Settings/autoRole/_remove.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { CommandFailedError, InternalError } from '#lib/errors';
|
||||||
|
import { createEmbed } from '#lib/utils';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { removeAutoRole, getGuild, prismaClient } from '@wyvern/database';
|
||||||
|
import { mention } from '@wyvern/markdown';
|
||||||
|
|
||||||
|
export async function autoRole_remove(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const role = i.options.getRole('role', true);
|
||||||
|
|
||||||
|
const guildRes = await getGuild(i.guild!.id);
|
||||||
|
|
||||||
|
if (guildRes.isErr()) {
|
||||||
|
throw new InternalError(guildRes.unwrapErr().message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { autoRoles: existingRoles } = guildRes.unwrap();
|
||||||
|
|
||||||
|
if (!existingRoles.includes(role.id)) throw new CommandFailedError("That role isn't configured as an auto role");
|
||||||
|
|
||||||
|
await prismaClient.$queryRawTyped(removeAutoRole(role.id, i.guild!.id));
|
||||||
|
|
||||||
|
const embed = createEmbed(i.user, 'error')
|
||||||
|
.setTitle('Auto Role Removed')
|
||||||
|
.setDescription(`${mention('role', role.id)} will no longer be given to new members!`);
|
||||||
|
|
||||||
|
await i.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
28
apps/bot/src/commands/Settings/reportChannel/_set.ts
Normal file
28
apps/bot/src/commands/Settings/reportChannel/_set.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { CommandFailedError } from '#lib/errors';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { prismaClient } from '@wyvern/database';
|
||||||
|
import { TextChannel } from 'discord.js';
|
||||||
|
import { mention } from '@wyvern/markdown';
|
||||||
|
|
||||||
|
export async function reportChannel_set(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const channel = i.options.getChannel('channel', true);
|
||||||
|
const guild = i.guild!;
|
||||||
|
|
||||||
|
if (!(channel instanceof TextChannel)) throw new CommandFailedError('Channel is not a text channel');
|
||||||
|
|
||||||
|
const perms = channel.permissionsFor(guild.members.me!);
|
||||||
|
|
||||||
|
if (!perms.any(['SendMessages', 'EmbedLinks'])) throw new CommandFailedError("I don't have the required permissions for that channel");
|
||||||
|
|
||||||
|
await prismaClient.guild.update({
|
||||||
|
data: {
|
||||||
|
reportChannel: channel.id
|
||||||
|
},
|
||||||
|
where: { id: guild.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
await i.reply({
|
||||||
|
content: `User reports will now be sent to ${mention('channel', channel.id)}`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
16
apps/bot/src/commands/Settings/reportChannel/_unset.ts
Normal file
16
apps/bot/src/commands/Settings/reportChannel/_unset.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { prismaClient } from '@wyvern/database';
|
||||||
|
|
||||||
|
export async function reportChannel_unset(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
await prismaClient.guild.update({
|
||||||
|
data: {
|
||||||
|
reportChannel: null
|
||||||
|
},
|
||||||
|
where: { id: i.guild!.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
await i.reply({
|
||||||
|
content: `User reports will no longer be sent`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
export async function welcomeMessage_disable(i: Subcommand.ChatInputCommandInteraction) {}
|
||||||
2
apps/bot/src/commands/Settings/welcomeMessage/_enable.ts
Normal file
2
apps/bot/src/commands/Settings/welcomeMessage/_enable.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
export async function welcomeMessage_enable(i: Subcommand.ChatInputCommandInteraction) {}
|
||||||
26
apps/bot/src/commands/Settings/welcomeMessage/_preview.ts
Normal file
26
apps/bot/src/commands/Settings/welcomeMessage/_preview.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { InternalError } from '#lib/errors';
|
||||||
|
import { createEmbed, formatWelcomeLeaveMessage } from '#lib/utils';
|
||||||
|
import { Result } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { isNullish } from '@sapphire/utilities';
|
||||||
|
import { getGuild, Prisma, prismaClient } from '@wyvern/database';
|
||||||
|
import { ColorResolvable, GuildMember } from 'discord.js';
|
||||||
|
export async function welcomeMessage_preview(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
await i.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
|
const res = await prismaClient.guild.findFirstOrThrow({
|
||||||
|
where: { guildId: i.guild!.id },
|
||||||
|
include: { guildWelcomeMessage: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { title, description, embedColor } = res!.guildWelcomeMessage!;
|
||||||
|
|
||||||
|
const embed = createEmbed(i.user)
|
||||||
|
.setTitle(formatWelcomeLeaveMessage(title, i.member as GuildMember, i.guild!))
|
||||||
|
.setDescription(formatWelcomeLeaveMessage(description, i.member as GuildMember, i.guild!))
|
||||||
|
.setColor(embedColor as ColorResolvable);
|
||||||
|
|
||||||
|
await i.editReply({
|
||||||
|
embeds: [embed]
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
export async function welcomeMessage_setColor(i: Subcommand.ChatInputCommandInteraction) {}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
export async function welcomeMessage_setMessage(i: Subcommand.ChatInputCommandInteraction) {}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
export async function welcomeMessage_setTitle(i: Subcommand.ChatInputCommandInteraction) {}
|
||||||
23
apps/bot/src/commands/Tag/Tag.ts
Normal file
23
apps/bot/src/commands/Tag/Tag.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { createTag } from './_index.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Subcommand.Options>({
|
||||||
|
description: 'Tag commands',
|
||||||
|
subcommands: [{ name: 'create', chatInputRun: createTag }]
|
||||||
|
})
|
||||||
|
export class UserCommand extends Subcommand {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('create')
|
||||||
|
.setDescription('Create a tag')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
apps/bot/src/commands/Tag/_createTag.ts
Normal file
44
apps/bot/src/commands/Tag/_createTag.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
|
||||||
|
|
||||||
|
export async function createTag(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const row1 = new ActionRowBuilder<TextInputBuilder>().setComponents([
|
||||||
|
new TextInputBuilder() //
|
||||||
|
.setCustomId('name')
|
||||||
|
.setPlaceholder('The tag name')
|
||||||
|
.setLabel('Name')
|
||||||
|
.setMinLength(1)
|
||||||
|
.setMaxLength(255)
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setRequired(true)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const row2 = new ActionRowBuilder<TextInputBuilder>().setComponents([
|
||||||
|
new TextInputBuilder() //
|
||||||
|
.setCustomId('content')
|
||||||
|
.setLabel('Content')
|
||||||
|
.setPlaceholder('The tag content')
|
||||||
|
.setMinLength(1)
|
||||||
|
.setMaxLength(255)
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setRequired(true)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const row3 = new ActionRowBuilder<TextInputBuilder>().setComponents([
|
||||||
|
new TextInputBuilder() //
|
||||||
|
.setCustomId('image')
|
||||||
|
.setLabel('Image')
|
||||||
|
.setPlaceholder('An image Url')
|
||||||
|
.setMinLength(1)
|
||||||
|
.setMaxLength(255)
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setRequired(false)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const modal = new ModalBuilder() //
|
||||||
|
.setCustomId('create-tag')
|
||||||
|
.setTitle('Create Tag')
|
||||||
|
.setComponents([row1, row2, row3]);
|
||||||
|
|
||||||
|
await i.showModal(modal);
|
||||||
|
}
|
||||||
1
apps/bot/src/commands/Tag/_index.ts
Normal file
1
apps/bot/src/commands/Tag/_index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './_createTag.js';
|
||||||
29
apps/bot/src/commands/Urban/Urban.ts
Normal file
29
apps/bot/src/commands/Urban/Urban.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Command } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { urbanDefine, urbanRandom } from './_index.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Subcommand.Options>({
|
||||||
|
description: 'Tag commands',
|
||||||
|
nsfw: true,
|
||||||
|
subcommands: [
|
||||||
|
{ name: 'define', chatInputRun: urbanDefine },
|
||||||
|
{ name: 'random', chatInputRun: urbanRandom }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserCommand extends Subcommand {
|
||||||
|
public override registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand((builder) =>
|
||||||
|
builder //
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addSubcommand((b) =>
|
||||||
|
b //
|
||||||
|
.setName('define')
|
||||||
|
.setDescription('Define a term on Urban Dictionary')
|
||||||
|
.addStringOption((b) => b.setName('term').setDescription('The term').setRequired(true))
|
||||||
|
)
|
||||||
|
.addSubcommand((b) => b.setName('random').setDescription('Define a random term'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/bot/src/commands/Urban/_define.ts
Normal file
26
apps/bot/src/commands/Urban/_define.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { container } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||||
|
import { createEmbed } from '#lib/utils';
|
||||||
|
|
||||||
|
export async function urbanDefine(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const { urban } = container;
|
||||||
|
|
||||||
|
const term = i.options.getString('term', true);
|
||||||
|
|
||||||
|
const { list } = await urban.defineWord(term);
|
||||||
|
|
||||||
|
const pMsg = new PaginatedMessage();
|
||||||
|
|
||||||
|
list.forEach(({ definition, word, written_on, thumbs_down, thumbs_up, author }) => {
|
||||||
|
pMsg.addPageEmbed(
|
||||||
|
createEmbed(i.user)
|
||||||
|
.setTimestamp(new Date(written_on))
|
||||||
|
.setTitle(urban.removeBrackets(word))
|
||||||
|
.setDescription(urban.removeBrackets(definition))
|
||||||
|
.setFooter({ text: `👍 ${thumbs_up} | 👎 ${thumbs_down} | Written by ${author}` })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await pMsg.run(i);
|
||||||
|
}
|
||||||
2
apps/bot/src/commands/Urban/_index.ts
Normal file
2
apps/bot/src/commands/Urban/_index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './_define.js';
|
||||||
|
export * from './_random.js';
|
||||||
24
apps/bot/src/commands/Urban/_random.ts
Normal file
24
apps/bot/src/commands/Urban/_random.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createEmbed } from '#lib/utils';
|
||||||
|
import { container } from '@sapphire/framework';
|
||||||
|
import { Subcommand } from '@sapphire/plugin-subcommands';
|
||||||
|
import { pickRandom } from '@sapphire/utilities';
|
||||||
|
|
||||||
|
export async function urbanRandom(i: Subcommand.ChatInputCommandInteraction) {
|
||||||
|
const { urban } = container;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { list } = await urban.randomDefinition();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { definition, word, written_on, thumbs_down, thumbs_up, author } = pickRandom(list)
|
||||||
|
|
||||||
|
const embed = createEmbed(i.user)
|
||||||
|
.setTimestamp(new Date(written_on))
|
||||||
|
.setTitle(urban.removeBrackets(word))
|
||||||
|
.setDescription(urban.removeBrackets(definition))
|
||||||
|
.setFooter({ text: `👍 ${thumbs_up} | 👎 ${thumbs_down} | Written by ${author}` })
|
||||||
|
|
||||||
|
await i.reply({ embeds: [embed] })
|
||||||
|
}
|
||||||
45
apps/bot/src/index.ts
Normal file
45
apps/bot/src/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { UrbanDictionary } from '@wyvern/urban-dictionary';
|
||||||
|
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.GuildEmojisAndStickers,
|
||||||
|
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();
|
||||||
|
client.logger.info('logged in');
|
||||||
|
|
||||||
|
container.urban = new UrbanDictionary();
|
||||||
|
} catch (error) {
|
||||||
|
client.logger.fatal(error);
|
||||||
|
await client.destroy();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void main();
|
||||||
40
apps/bot/src/interaction-handlers/modal/createTag.ts
Normal file
40
apps/bot/src/interaction-handlers/modal/createTag.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { createEmbed, validateUrl } from '#lib/utils';
|
||||||
|
import { createTag, prismaClient } from '@wyvern/database';
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
|
||||||
|
import { inlineCode, ModalSubmitInteraction } from 'discord.js';
|
||||||
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.ModalSubmit
|
||||||
|
})
|
||||||
|
export class ModalHandler extends InteractionHandler {
|
||||||
|
override async run(i: ModalSubmitInteraction, { content, image, name }: InteractionHandler.ParseResult<this>) {
|
||||||
|
const imageUrl = validateUrl(image).match({
|
||||||
|
ok: (url) => url.href,
|
||||||
|
err: (_) => ''
|
||||||
|
});
|
||||||
|
|
||||||
|
await prismaClient.$queryRawTyped(createTag(content, imageUrl, name, i.guildId!, i.user.id, createId()));
|
||||||
|
|
||||||
|
const embed = createEmbed(i.user) //
|
||||||
|
.setTitle('Tag Created')
|
||||||
|
.setDescription(`Created a tag with title ${inlineCode(name)}`);
|
||||||
|
|
||||||
|
await i.reply({
|
||||||
|
embeds: [embed]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override parse(i: ModalSubmitInteraction) {
|
||||||
|
if (i.customId !== 'create-tag') return this.none();
|
||||||
|
|
||||||
|
const fields = i.fields;
|
||||||
|
|
||||||
|
const name = fields.getTextInputValue('name');
|
||||||
|
const content = fields.getTextInputValue('content');
|
||||||
|
const image = fields.getTextInputValue('image');
|
||||||
|
|
||||||
|
return this.some({ name, content, image });
|
||||||
|
}
|
||||||
|
}
|
||||||
16
apps/bot/src/lib/constants.ts
Normal file
16
apps/bot/src/lib/constants.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
console.log(import.meta.dirname);
|
||||||
|
|
||||||
|
const up = (count: number) => {
|
||||||
|
const foo: Array<string> = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
foo.push('..');
|
||||||
|
}
|
||||||
|
return foo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rootDir = join(import.meta.dirname, ...up(4));
|
||||||
|
export const srcDir = join(rootDir, 'src');
|
||||||
|
|
||||||
|
export const RandomLoadingMessage = ['Computing...', 'Thinking...', 'Cooking some food', 'Give me a moment', 'Loading...'];
|
||||||
3
apps/bot/src/lib/customIdParams/baseCustomIdParams.ts
Normal file
3
apps/bot/src/lib/customIdParams/baseCustomIdParams.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface BaseCustomIdParams {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
2
apps/bot/src/lib/customIdParams/index.ts
Normal file
2
apps/bot/src/lib/customIdParams/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './baseCustomIdParams.js';
|
||||||
|
export * from './report.js';
|
||||||
10
apps/bot/src/lib/customIdParams/report.ts
Normal file
10
apps/bot/src/lib/customIdParams/report.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { BaseCustomIdParams } from './index.js';
|
||||||
|
|
||||||
|
export interface ReportMessageCustomIdParams extends BaseCustomIdParams {
|
||||||
|
channelId: string;
|
||||||
|
messageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportUserCustomIdParams extends BaseCustomIdParams {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
8
apps/bot/src/lib/errors/CommandFailedError.ts
Normal file
8
apps/bot/src/lib/errors/CommandFailedError.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class CommandFailedError extends Error {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/bot/src/lib/errors/InternalError.ts
Normal file
8
apps/bot/src/lib/errors/InternalError.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class InternalError extends Error {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/bot/src/lib/errors/index.ts
Normal file
2
apps/bot/src/lib/errors/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './CommandFailedError.js';
|
||||||
|
export * from './InternalError.js';
|
||||||
46
apps/bot/src/lib/setup.ts
Normal file
46
apps/bot/src/lib/setup.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Unless explicitly defined, set NODE_ENV as development:
|
||||||
|
process.env.NODE_ENV ??= 'development';
|
||||||
|
|
||||||
|
import { ApplicationCommandRegistries, RegisterBehavior } from '@sapphire/framework';
|
||||||
|
import '@sapphire/plugin-api/register';
|
||||||
|
import '@sapphire/plugin-editable-commands/register';
|
||||||
|
// import '@sapphire/plugin-logger/register';
|
||||||
|
|
||||||
|
import '@wyvern/plugin-custom-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 { rootDir } from './constants.js';
|
||||||
|
|
||||||
|
import { UrbanDictionary } from '@wyvern/urban-dictionary';
|
||||||
|
|
||||||
|
// Set default behavior to bulk overwrite
|
||||||
|
ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical(RegisterBehavior.BulkOverwrite);
|
||||||
|
|
||||||
|
ApplicationCommandRegistries.setDefaultGuildIds(['1072895701970858026', '739115569311383634']);
|
||||||
|
|
||||||
|
// Read env var
|
||||||
|
console.log(join(rootDir, '.env'));
|
||||||
|
|
||||||
|
setup({ path: join(rootDir, '.env') });
|
||||||
|
|
||||||
|
// Set default inspection depth
|
||||||
|
inspect.defaultOptions.depth = 1;
|
||||||
|
|
||||||
|
// Enable colorette
|
||||||
|
colorette.createColors({ useColor: true });
|
||||||
|
|
||||||
|
declare module '@skyra/env-utilities' {
|
||||||
|
interface Env {
|
||||||
|
OWNERS: ArrayString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/pieces' {
|
||||||
|
interface Container {
|
||||||
|
urban: UrbanDictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
apps/bot/src/lib/utils.ts
Normal file
185
apps/bot/src/lib/utils.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import {
|
||||||
|
ChatInputCommandSuccessPayload,
|
||||||
|
Command,
|
||||||
|
ContextMenuCommandSuccessPayload,
|
||||||
|
err,
|
||||||
|
MessageCommandSuccessPayload,
|
||||||
|
ok,
|
||||||
|
Result
|
||||||
|
} from '@sapphire/framework';
|
||||||
|
import { container } from '@sapphire/framework';
|
||||||
|
import { send } from '@sapphire/plugin-editable-commands';
|
||||||
|
import { cyan } from 'colorette';
|
||||||
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
EmbedBuilder,
|
||||||
|
GuildMember,
|
||||||
|
Role,
|
||||||
|
RoleResolvable,
|
||||||
|
type APIUser,
|
||||||
|
type Guild,
|
||||||
|
type Message,
|
||||||
|
type User
|
||||||
|
} from 'discord.js';
|
||||||
|
import { RandomLoadingMessage } from '#lib/constants';
|
||||||
|
import { globalConfig } from '@wyvern/config';
|
||||||
|
|
||||||
|
import { isNullish } from '@sapphire/utilities';
|
||||||
|
import { brotliCompressSync, brotliDecompressSync } from 'node:zlib';
|
||||||
|
import { serialize, deserialize } from 'binarytf';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEmbed(userOrMember: User | GuildMember, type: keyof typeof globalConfig.colors = 'success'): EmbedBuilder {
|
||||||
|
const color = globalConfig.colors[type];
|
||||||
|
|
||||||
|
const user = userOrMember instanceof GuildMember ? userOrMember.user : userOrMember;
|
||||||
|
|
||||||
|
return new EmbedBuilder() //
|
||||||
|
.setAuthor({
|
||||||
|
name: user.username,
|
||||||
|
iconURL: user.avatarURL({ forceStatic: false }) ?? ''
|
||||||
|
})
|
||||||
|
.setColor(color)
|
||||||
|
.setTimestamp(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUrl(url: string): Result<URL, TypeError> {
|
||||||
|
return Result.from<URL, TypeError>(() => new URL(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFullCommandName(i: ChatInputCommandInteraction) {
|
||||||
|
const opts = i.options;
|
||||||
|
|
||||||
|
const baseName = i.commandName;
|
||||||
|
const subcommandGroup = opts.getSubcommandGroup(false);
|
||||||
|
const subcommand = opts.getSubcommand(false);
|
||||||
|
|
||||||
|
const parts: string[] = [baseName];
|
||||||
|
|
||||||
|
if (!isNullish(subcommandGroup)) parts.push(subcommandGroup);
|
||||||
|
if (!isNullish(subcommand)) parts.push(subcommand);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmpty<T>(arr: T[]) {
|
||||||
|
return arr.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryAddRole(role: RoleResolvable, member: GuildMember): Promise<Result<GuildMember, string>> {
|
||||||
|
const a = await member.roles
|
||||||
|
.add(role)
|
||||||
|
.then((r) => r)
|
||||||
|
.catch((e) => e as string);
|
||||||
|
|
||||||
|
return a instanceof GuildMember ? ok(a) : err(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkAddRoles(roles: RoleResolvable[], member: GuildMember) {
|
||||||
|
const failed: string[] = [];
|
||||||
|
const successful: string[] = [];
|
||||||
|
|
||||||
|
roles.forEach(async (role) => {
|
||||||
|
const res = await tryAddRole(role, member);
|
||||||
|
|
||||||
|
res.match({
|
||||||
|
ok: () => successful.push(role instanceof Role ? role.id : role),
|
||||||
|
err: () => failed.push(role instanceof Role ? role.id : role)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { failed, successful };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compressCustomId<T>(params: T, customMessagePart?: string): Result<string, string> {
|
||||||
|
const serializedId = brotliCompressSync(serialize<T>(params)).toString('binary');
|
||||||
|
|
||||||
|
if (serializedId.length > 80) {
|
||||||
|
const resolvedCustomMessagePart = customMessagePart ?? '';
|
||||||
|
return err(
|
||||||
|
`Due to Discord API limitations I was unable to resolve that request. ${resolvedCustomMessagePart}This issue will be fixed in the future.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(serializedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decompressCustomId<T>(content: string): Result<T, Error> {
|
||||||
|
const result = Result.from<T, Error>(() =>
|
||||||
|
//
|
||||||
|
deserialize<T>(brotliDecompressSync(Buffer.from(content, 'binary')))
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWelcomeLeaveMessage(msg: string, member: GuildMember, guild: Guild) {
|
||||||
|
return msg //
|
||||||
|
.replaceAll(/{user}/g, member.user.username)
|
||||||
|
.replaceAll(/{guild}/g, guild.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJson(x: any): boolean {
|
||||||
|
try {
|
||||||
|
JSON.parse(x);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { ChatInputCommand, Listener } from '@sapphire/framework';
|
||||||
|
import { createEmbed, getFullCommandName } from '#lib/utils';
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { ChatInputSubcommandErrorPayload, SubcommandPluginEvents } from '@sapphire/plugin-subcommands';
|
||||||
|
import { WyvernErrorHandler } from '@wyvern/error-handler';
|
||||||
|
import { CommandFailedError } from '#lib/errors';
|
||||||
|
import { codeblock } from '@wyvern/markdown';
|
||||||
|
|
||||||
|
@ApplyOptions<Listener.Options>({
|
||||||
|
event: SubcommandPluginEvents.ChatInputSubcommandError
|
||||||
|
})
|
||||||
|
export class UserListener extends Listener<typeof SubcommandPluginEvents.ChatInputSubcommandError> {
|
||||||
|
public override async run(error: Error, payload: ChatInputSubcommandErrorPayload) {
|
||||||
|
const { interaction, context } = payload;
|
||||||
|
|
||||||
|
if (error instanceof CommandFailedError) {
|
||||||
|
await this.handleCommandFailedError(error, interaction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof CommandFailedError) {
|
||||||
|
await this.handleCommandFailedError(error, interaction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new WyvernErrorHandler();
|
||||||
|
|
||||||
|
handler.handleCommandError({
|
||||||
|
error,
|
||||||
|
|
||||||
|
commandId: context.commandId,
|
||||||
|
commandName: getFullCommandName(interaction),
|
||||||
|
|
||||||
|
guildId: interaction.guildId!,
|
||||||
|
guildName: interaction.guild!.name!,
|
||||||
|
|
||||||
|
userId: interaction.user.id,
|
||||||
|
username: interaction.user.username
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = createEmbed(interaction.user, 'error') //
|
||||||
|
.setTitle('This is embarrassing')
|
||||||
|
.setDescription(codeblock(error.message));
|
||||||
|
|
||||||
|
const method = interaction.deferred || interaction.replied ? 'editReply' : 'reply';
|
||||||
|
|
||||||
|
await interaction[method]({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCommandFailedError(error: CommandFailedError, interaction: ChatInputCommand.Interaction) {
|
||||||
|
const embed = createEmbed(interaction.user, 'error') //
|
||||||
|
.setTitle('Something went wrong')
|
||||||
|
.setDescription(error.message);
|
||||||
|
|
||||||
|
const method = interaction.deferred || interaction.replied ? 'editReply' : 'reply';
|
||||||
|
|
||||||
|
await interaction[method]({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/bot/src/listeners/guildMemberJoin.ts
Normal file
48
apps/bot/src/listeners/guildMemberJoin.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { bulkAddRoles, createEmbed, formatWelcomeLeaveMessage } from '#lib/utils';
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Events } from '@sapphire/framework';
|
||||||
|
import { Listener } from '@sapphire/framework';
|
||||||
|
import { isNullish } from '@sapphire/utilities';
|
||||||
|
import { getGuild, prismaClient } from '@wyvern/database';
|
||||||
|
import type { ColorResolvable, GuildMember } from 'discord.js';
|
||||||
|
|
||||||
|
@ApplyOptions<Listener.Options>({
|
||||||
|
event: Events.GuildMemberAdd
|
||||||
|
})
|
||||||
|
export class UserEvent extends Listener<typeof Events.GuildMemberAdd> {
|
||||||
|
public override async run(member: GuildMember) {
|
||||||
|
this.autoRole(member);
|
||||||
|
this.welcome(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async autoRole(member: GuildMember) {
|
||||||
|
const { autoRoles: roles } = await prismaClient.guild.findFirstOrThrow({
|
||||||
|
where: { guildId: member.guild.id },
|
||||||
|
select: { autoRoles: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
await bulkAddRoles(roles, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async welcome(member: GuildMember) {
|
||||||
|
const res = await prismaClient.guild.findFirstOrThrow({
|
||||||
|
where: { guildId: member.guild!.id },
|
||||||
|
include: { guildWelcomeMessage: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { title, description, embedColor, channelId } = res!.guildWelcomeMessage!;
|
||||||
|
|
||||||
|
if (isNullish(channelId)) return;
|
||||||
|
|
||||||
|
const channel = await member.guild.channels.fetch(channelId);
|
||||||
|
|
||||||
|
if (isNullish(channel) || channel?.isTextBased() || !channel.isSendable()) return;
|
||||||
|
|
||||||
|
const embed = createEmbed(member)
|
||||||
|
.setTitle(formatWelcomeLeaveMessage(title, member, member.guild))
|
||||||
|
.setDescription(formatWelcomeLeaveMessage(description, member, member.guild))
|
||||||
|
.setColor(embedColor as ColorResolvable);
|
||||||
|
|
||||||
|
await channel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
59
apps/bot/src/listeners/message.ts
Normal file
59
apps/bot/src/listeners/message.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { InternalError } from '#lib/errors';
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Events, Listener } from '@sapphire/framework';
|
||||||
|
import { getGuild } from '@wyvern/database';
|
||||||
|
import { Message } from 'discord.js';
|
||||||
|
import { fetch, FetchResultTypes } from '@sapphire/fetch';
|
||||||
|
|
||||||
|
@ApplyOptions<Listener.Options>({
|
||||||
|
event: Events.MessageCreate
|
||||||
|
})
|
||||||
|
export class UserEvent extends Listener<typeof Events.MessageCreate> {
|
||||||
|
override async run(msg: Message) {
|
||||||
|
await this._autoCarbon(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _autoCarbon(msg: Message<boolean>) {
|
||||||
|
const guild = await getGuild(msg.guild!.id);
|
||||||
|
|
||||||
|
const autoCarbonEnabled = guild.match({
|
||||||
|
ok: (g) => g.autoCarbon,
|
||||||
|
err: (e) => {
|
||||||
|
throw new InternalError(e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!autoCarbonEnabled) return;
|
||||||
|
|
||||||
|
const re = /^```(?<lang>\w+)?\n(?<code>.+?)(?:\n)?```$/s;
|
||||||
|
|
||||||
|
if (!re.test(msg.content)) return;
|
||||||
|
|
||||||
|
const groups = re.exec(msg.content)!.groups!;
|
||||||
|
|
||||||
|
const result = await fetch(
|
||||||
|
'https://carbonara.aero.bot/api/cook',
|
||||||
|
{
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: groups.code,
|
||||||
|
language: 'auto',
|
||||||
|
theme: 'one-dark',
|
||||||
|
backgroundColor: 'rgb(54, 57, 63)',
|
||||||
|
fontFamily: 'JetBrains Mono',
|
||||||
|
paddingHorizontal: '20px',
|
||||||
|
paddingVertical: '20px',
|
||||||
|
windowControls: 'false',
|
||||||
|
dropShadowBlurRadius: '10px',
|
||||||
|
dropShadowOffsetY: '0px'
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: [['Content-Type', 'application/json']]
|
||||||
|
},
|
||||||
|
FetchResultTypes.Buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = null;
|
||||||
|
|
||||||
|
this.container.logger.debug(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
apps/bot/src/listeners/ready.ts
Normal file
53
apps/bot/src/listeners/ready.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Listener } from '@sapphire/framework';
|
||||||
|
import type { StoreRegistryValue } from '@sapphire/pieces';
|
||||||
|
|
||||||
|
import { blue, green, magenta, magentaBright, white } 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 async 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 `${last ? '└─' : '├─'} Loaded ${store.size.toString().padEnd(3, ' ')} ${store.name}.`;
|
||||||
|
// 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 type { CommandInteraction, ContextMenuCommandInteraction, Message, Snowflake } from 'discord.js';
|
||||||
|
|
||||||
|
const OWNERS = ['424239181296959507'];
|
||||||
|
|
||||||
|
export class UserPrecondition extends AllFlowsPrecondition {
|
||||||
|
#message = 'This command can only be used by the owner.';
|
||||||
|
|
||||||
|
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 OWNERS.includes(userId) ? this.ok() : this.error({ message: this.#message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface Preconditions {
|
||||||
|
OwnerOnly: never;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
apps/bot/src/preconditions/RequiredSetting.ts
Normal file
40
apps/bot/src/preconditions/RequiredSetting.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { AllFlowsPrecondition, Command } from '@sapphire/framework';
|
||||||
|
import { isNullish } from '@sapphire/utilities';
|
||||||
|
import { getGuild, Guild } from '@wyvern/database';
|
||||||
|
import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message, Snowflake } from 'discord.js';
|
||||||
|
|
||||||
|
export interface RequiredSettingPreconditionContext extends AllFlowsPrecondition.Context {
|
||||||
|
setting: keyof Omit<Guild, 'id'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserPrecondition extends AllFlowsPrecondition {
|
||||||
|
public override chatInputRun(interaction: ChatInputCommandInteraction, _: Command, context: RequiredSettingPreconditionContext) {
|
||||||
|
return this.sharedRun(interaction.user.id, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override contextMenuRun(interaction: ContextMenuCommandInteraction, _: Command, context: RequiredSettingPreconditionContext) {
|
||||||
|
return this.sharedRun(interaction.user.id, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override messageRun(message: Message, _: Command, context: RequiredSettingPreconditionContext) {
|
||||||
|
return this.sharedRun(message.author.id, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sharedRun(guildId: Snowflake, { setting }: RequiredSettingPreconditionContext) {
|
||||||
|
const guildRes = await getGuild(guildId);
|
||||||
|
|
||||||
|
if (guildRes.isErr()) return this.error({ identifier: 'NO_SERVER_SETTING' });
|
||||||
|
|
||||||
|
const guild = guildRes.unwrap();
|
||||||
|
|
||||||
|
const value = guild[setting];
|
||||||
|
|
||||||
|
return isNullish(value) ? this.error({ message: 'A required server setting is unset', context: { setting } }) : this.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface Preconditions {
|
||||||
|
RequiredSetting: RequiredSettingPreconditionContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/bot/src/routes/hello-world.ts
Normal file
13
apps/bot/src/routes/hello-world.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
|
||||||
|
|
||||||
|
@ApplyOptions<Route.Options>({ route: 'hello-world' })
|
||||||
|
export class UserRoute extends Route {
|
||||||
|
public override [methods.GET](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Hello World' });
|
||||||
|
}
|
||||||
|
|
||||||
|
public override [methods.POST](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Hello World' });
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/bot/src/routes/main.ts
Normal file
13
apps/bot/src/routes/main.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ApplyOptions } from '@sapphire/decorators';
|
||||||
|
import { Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
|
||||||
|
|
||||||
|
@ApplyOptions<Route.Options>({ route: `` })
|
||||||
|
export class UserRoute extends Route {
|
||||||
|
public override [methods.GET](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Landing Page!' });
|
||||||
|
}
|
||||||
|
|
||||||
|
public override [methods.POST](_request: ApiRequest, response: ApiResponse) {
|
||||||
|
response.json({ message: 'Landing Page!' });
|
||||||
|
}
|
||||||
|
}
|
||||||
63
apps/bot/tsconfig.json
Normal file
63
apps/bot/tsconfig.json
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"@sapphire/ts-config",
|
||||||
|
"@sapphire/ts-config/extra-strict",
|
||||||
|
"@sapphire/ts-config/decorators"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||||
|
"allowJs": true,
|
||||||
|
"moduleDetection": "auto",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"paths": {
|
||||||
|
"#lib/structures": [
|
||||||
|
"src/lib/structures/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/interfaces": [
|
||||||
|
"src/lib/interfaces/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/parsers": [
|
||||||
|
"src/lib/parsers/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/utils": [
|
||||||
|
"src/lib/utils.ts"
|
||||||
|
],
|
||||||
|
"#lib/config": [
|
||||||
|
"src/lib/config.ts"
|
||||||
|
],
|
||||||
|
"#lib/types": [
|
||||||
|
"src/lib/types.ts"
|
||||||
|
],
|
||||||
|
"#lib/constants": [
|
||||||
|
"src/lib/constants.ts"
|
||||||
|
],
|
||||||
|
"#lib/errors": [
|
||||||
|
"src/lib/errors/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/managers": [
|
||||||
|
"src/lib/managers/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/customIds": [
|
||||||
|
"src/lib/customIdTypes/index.ts"
|
||||||
|
],
|
||||||
|
"#lib/markdown": [
|
||||||
|
"src/lib/markdown/index.ts"
|
||||||
|
],
|
||||||
|
"#drizzle": [
|
||||||
|
"src/drizzle/schema.ts"
|
||||||
|
],
|
||||||
|
"#prisma": [
|
||||||
|
"src/lib/prisma.ts"
|
||||||
|
],
|
||||||
|
"#stringFormatters": [
|
||||||
|
"src/lib/stringFormatters/index.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
apps/bot/tsup.config.ts
Normal file
18
apps/bot/tsup.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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: 'es2020',
|
||||||
|
target: 'esnext',
|
||||||
|
splitting: false,
|
||||||
|
skipNodeModulesBundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
shims: false,
|
||||||
|
keepNames: true
|
||||||
|
});
|
||||||
46
docker-compose.yaml
Normal file
46
docker-compose.yaml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
container_name: majoexe-db
|
||||||
|
restart: always
|
||||||
|
user: postgres
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=majoexe
|
||||||
|
- POSTGRES_PASSWORD=0ce267bcae32edcb51e257d0f4439e7fb1e3df3c6577782468550d7cd5a531ea
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- app_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
seq:
|
||||||
|
image: datalust/seq:latest
|
||||||
|
container_name: seq
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- app_network
|
||||||
|
environment:
|
||||||
|
- ACCEPT_EULA=Y
|
||||||
|
- SEQ_FIRSTRUN_ADMINPASSWORDHASH=QJklU79+04hywc4hPkuRIDzsDzqhuVQ4aoVfA0IfP/LLEy8HwWWMfhA8YVnCo4gv18+igc70YaUWRnMdKZAzBc/Cms0hL/+hdXNBDYb4daWu
|
||||||
|
volumes:
|
||||||
|
- seq_data:/data
|
||||||
|
ports:
|
||||||
|
- 5341:80
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
driver: local
|
||||||
|
cache:
|
||||||
|
driver: local
|
||||||
|
seq_data:
|
||||||
|
driver: local
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@wyvern/monorepo",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo build",
|
||||||
|
"dev": "turbo dev",
|
||||||
|
"lint": "turbo lint",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||||
|
"prisma:generate": "turbo run prisma:generate ",
|
||||||
|
"prisma:generateSql": "turbo run prisma:generateSql ",
|
||||||
|
"prisma:push": "turbo run prisma:push",
|
||||||
|
"prisma:migrate": "turbo run prisma:migrate",
|
||||||
|
"typecheck": "turbo run typecheck"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sapphire/framework": "^5.3.1",
|
||||||
|
"colorette": "^2.0.20",
|
||||||
|
"discord.js": "^14.16.3",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"turbo": "^2.2.3",
|
||||||
|
"typescript": "5.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.1.34",
|
||||||
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@prisma/client",
|
||||||
|
"@prisma/engines",
|
||||||
|
"@sapphire/type",
|
||||||
|
"core-js-pure",
|
||||||
|
"esbuild",
|
||||||
|
"prisma"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
|
"binarytf": "^2.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
packages/config/package.json
Normal file
29
packages/config/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@wyvern/config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"build": "tsc",
|
||||||
|
"typecheck": "tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./dist/src/index.js",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./debug": {
|
||||||
|
"default": "./dist/src/configs/debug.js",
|
||||||
|
"types": "./src/configs/debug.ts"
|
||||||
|
},
|
||||||
|
"./global": {
|
||||||
|
"default": "./dist/src/configs/global.js",
|
||||||
|
"types": "./src/configs/global.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/config/src/configs/debug.ts
Normal file
11
packages/config/src/configs/debug.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const debugConfig: DebugConfig = {
|
||||||
|
displayDatabaseLogs: false,
|
||||||
|
logLevel: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogLevel = "";
|
||||||
|
interface DebugConfig {
|
||||||
|
displayDatabaseLogs: boolean;
|
||||||
|
|
||||||
|
logLevel: LogLevel;
|
||||||
|
}
|
||||||
20
packages/config/src/configs/global.ts
Normal file
20
packages/config/src/configs/global.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const globalConfig: GlobalConfig = {
|
||||||
|
guildIds: ["1072895701970858026", "739115569311383634"],
|
||||||
|
colors: {
|
||||||
|
success: "#f5c931",
|
||||||
|
warning: "#d7d1b5",
|
||||||
|
error: "#ffadad",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GlobalConfig {
|
||||||
|
colors: Colors;
|
||||||
|
guildIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type HexCode = `#${string}`;
|
||||||
|
interface Colors {
|
||||||
|
success: HexCode;
|
||||||
|
warning: HexCode;
|
||||||
|
error: HexCode;
|
||||||
|
}
|
||||||
2
packages/config/src/index.ts
Normal file
2
packages/config/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./configs/debug.js";
|
||||||
|
export * from "./configs/global.js";
|
||||||
9
packages/config/tsconfig.json
Normal file
9
packages/config/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@wyvern/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
1
packages/config/tsconfig.tsbuildinfo
Normal file
1
packages/config/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
3
packages/database/.gitignore
vendored
Normal file
3
packages/database/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
37
packages/database/package.json
Normal file
37
packages/database/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@wyvern/database",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"build": "tsc",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:generateSql": "prisma generate --sql",
|
||||||
|
"prisma:push": "prisma db push",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"typecheck": "tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./dist/src/index.js",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@wyvern/config": "*",
|
||||||
|
"@wyvern/plugin-custom-logger": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"client": "^0.0.1",
|
||||||
|
"prisma": "^5.22.0"
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"ws"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Guild" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"auto_roles" TEXT[],
|
||||||
|
"report_channel" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Guild_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "guild_welcome_message" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"channel_id" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL DEFAULT '🎉 Welcome to the server {user}!',
|
||||||
|
"description" TEXT NOT NULL DEFAULT '> Welcome to **{guild}** We hope you enjoy your stay here!',
|
||||||
|
"embed_color" TEXT NOT NULL DEFAULT '#5865F2',
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "guild_welcome_message_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "guild_leave_message" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"channel_id" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL DEFAULT '👋 Goodbye {user}!',
|
||||||
|
"description" TEXT NOT NULL DEFAULT '> We''re sorry to see you go!',
|
||||||
|
"embed_color" TEXT NOT NULL DEFAULT '#5865F2',
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "guild_leave_message_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Tag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"usages" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"image" TEXT,
|
||||||
|
"ownerId" TEXT NOT NULL,
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Guild_guild_id_key" ON "Guild"("guild_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "guild_welcome_message_guild_id_key" ON "guild_welcome_message"("guild_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "guild_leave_message_guild_id_key" ON "guild_leave_message"("guild_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "guild_welcome_message" ADD CONSTRAINT "guild_welcome_message_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "guild_leave_message" ADD CONSTRAINT "guild_leave_message_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "guild_leave_message" ALTER COLUMN "channel_id" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "guild_welcome_message" ALTER COLUMN "channel_id" DROP NOT NULL;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Guild" ADD COLUMN "auto_carbon" BOOLEAN NOT NULL DEFAULT false;
|
||||||
3
packages/database/prisma/migrations/migration_lock.toml
Normal file
3
packages/database/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
83
packages/database/prisma/schema.prisma
Normal file
83
packages/database/prisma/schema.prisma
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["typedSql"]
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id
|
||||||
|
ownedTags Tag[]
|
||||||
|
|
||||||
|
@@map("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Guild {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
guildId String @unique @map("guild_id")
|
||||||
|
|
||||||
|
tags Tag[]
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
autoRoles String[] @map("auto_roles")
|
||||||
|
reportChannel String? @map("report_channel")
|
||||||
|
guildWelcomeMessage GuildWelcomeMessage?
|
||||||
|
guildLeaveMessage GuildLeaveMessage?
|
||||||
|
autoCarbon Boolean @default(false) @map("auto_carbon")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guild welcome message
|
||||||
|
model GuildWelcomeMessage {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
guildId String @unique @map(name: "guild_id")
|
||||||
|
channelId String? @map(name: "channel_id")
|
||||||
|
|
||||||
|
title String @default("🎉 Welcome to the server {user}!")
|
||||||
|
description String @default("> Welcome to **{guild}** We hope you enjoy your stay here!")
|
||||||
|
embedColor String @default("#5865F2") @map(name: "embed_color")
|
||||||
|
enabled Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
|
||||||
|
guild Guild @relation(fields: [guildId], references: [guildId], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map(name: "guild_welcome_message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guild leave message
|
||||||
|
model GuildLeaveMessage {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
guildId String @unique @map(name: "guild_id")
|
||||||
|
channelId String? @map(name: "channel_id")
|
||||||
|
title String @default("👋 Goodbye {user}!")
|
||||||
|
description String @default("> We're sorry to see you go!")
|
||||||
|
embedColor String @default("#5865F2") @map(name: "embed_color")
|
||||||
|
enabled Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
guild Guild @relation(fields: [guildId], references: [guildId], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map(name: "guild_leave_message")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
owner User @relation(fields: [ownerId], references: [id])
|
||||||
|
guild Guild @relation(fields: [guildId], references: [id])
|
||||||
|
|
||||||
|
name String
|
||||||
|
content String
|
||||||
|
usages Int @default(0)
|
||||||
|
image String?
|
||||||
|
|
||||||
|
ownerId String
|
||||||
|
guildId String
|
||||||
|
}
|
||||||
8
packages/database/prisma/sql/createAutoRole.sql
Normal file
8
packages/database/prisma/sql/createAutoRole.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- @param {String} $1:role_id
|
||||||
|
-- @param {String} $2:guild_id
|
||||||
|
UPDATE
|
||||||
|
"public"."Guild"
|
||||||
|
SET
|
||||||
|
"AutoRoles" = array_append("AutoRoles", $1)
|
||||||
|
WHERE
|
||||||
|
"id" = $2;
|
||||||
27
packages/database/prisma/sql/createTag.sql
Normal file
27
packages/database/prisma/sql/createTag.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
-- @param {String} $1:content
|
||||||
|
-- @param {String} $2:image
|
||||||
|
-- @param {String} $3:name
|
||||||
|
-- @param {String} $4:guildId
|
||||||
|
-- @param {String} $5:ownerId
|
||||||
|
-- @param {String} $6:id
|
||||||
|
INSERT INTO
|
||||||
|
"Tag" (
|
||||||
|
"Content",
|
||||||
|
"Image",
|
||||||
|
"Name",
|
||||||
|
|
||||||
|
"guildId",
|
||||||
|
"ownerId",
|
||||||
|
|
||||||
|
"Id"
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
)
|
||||||
11
packages/database/prisma/sql/findTagByTitle.sql
Normal file
11
packages/database/prisma/sql/findTagByTitle.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- @param {String} $1:guildId
|
||||||
|
-- @param {String} $2:name
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
"Tag"
|
||||||
|
WHERE
|
||||||
|
"guildId" = $1
|
||||||
|
AND "Name" = $2
|
||||||
|
LIMIT
|
||||||
|
1
|
||||||
8
packages/database/prisma/sql/removeAutoRole.sql
Normal file
8
packages/database/prisma/sql/removeAutoRole.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- @param {String} $1:role_id
|
||||||
|
-- @param {String} $2:guild_id
|
||||||
|
UPDATE
|
||||||
|
"public"."Guild"
|
||||||
|
SET
|
||||||
|
"AutoRoles" = array_remove("AutoRoles", $1)
|
||||||
|
WHERE
|
||||||
|
"id" = $2;
|
||||||
10
packages/database/src/helpers/getGuild.ts
Normal file
10
packages/database/src/helpers/getGuild.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Guild, Prisma, prismaClient } from "../index.js";
|
||||||
|
import { Result } from "@sapphire/framework";
|
||||||
|
|
||||||
|
export async function getGuild(id: string) {
|
||||||
|
return Result.fromAsync<Guild, Prisma.PrismaClientKnownRequestError>(
|
||||||
|
prismaClient.guild.findFirstOrThrow({
|
||||||
|
where: { guildId: id },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
1
packages/database/src/helpers/index.ts
Normal file
1
packages/database/src/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./getGuild.js";
|
||||||
53
packages/database/src/index.ts
Normal file
53
packages/database/src/index.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { wyvernLogger } from "@wyvern/plugin-custom-logger";
|
||||||
|
import { debugConfig } from "@wyvern/config/debug";
|
||||||
|
|
||||||
|
const log = wyvernLogger.child({ activity: "database" });
|
||||||
|
|
||||||
|
const pris = new PrismaClient({
|
||||||
|
log: [
|
||||||
|
{ emit: "event", level: "query" },
|
||||||
|
{ emit: "event", level: "warn" },
|
||||||
|
{ emit: "event", level: "error" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prismaClientWrapper = (): PrismaClient => {
|
||||||
|
if (debugConfig.displayDatabaseLogs) {
|
||||||
|
pris.$on("query", ({ duration, query }) => {
|
||||||
|
log.info("Query: %s", query);
|
||||||
|
log.info("Duration: %dms", duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
pris.$on("warn", ({ message }) => {
|
||||||
|
wyvernLogger.warn(`Prisma Warning: %s`, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
pris.$on("error", ({ message }) => {
|
||||||
|
wyvernLogger.warn(`Prisma Error: %s`, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pris;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prismaClientSingleton = () => {
|
||||||
|
return prismaClientWrapper();
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const globalThis: {
|
||||||
|
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||||
|
} & typeof global;
|
||||||
|
|
||||||
|
// const prisma: PrismaClient = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
|
|
||||||
|
export const prismaClient = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = pris;
|
||||||
|
|
||||||
|
// also export types
|
||||||
|
export * from "@prisma/client";
|
||||||
|
export * from "@prisma/client/sql";
|
||||||
|
|
||||||
|
// export helper funcs
|
||||||
|
export * from "./helpers/index.js";
|
||||||
9
packages/database/tsconfig.json
Normal file
9
packages/database/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@wyvern/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
21
packages/error-handler/package.json
Normal file
21
packages/error-handler/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@wyvern/error-handler",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"build": "tsc",
|
||||||
|
"typecheck": "tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./dist/src/index.js",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
packages/error-handler/src/WyvernErrorHandler.ts
Normal file
53
packages/error-handler/src/WyvernErrorHandler.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
|
export class WyvernErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
const dsn = process.env.SENTRY_DSN;
|
||||||
|
|
||||||
|
if (dsn == undefined) {
|
||||||
|
throw new Error("process.env.SENTRY_DSN is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Sentry.isInitialized()) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleCommandError(options: CommandErrorOptions) {
|
||||||
|
if (process.env.NODE_ENV != "production") return;
|
||||||
|
|
||||||
|
Sentry.captureException(options.error, (scope) => {
|
||||||
|
scope.setContext("Command", {
|
||||||
|
commandName: options.commandName,
|
||||||
|
commandId: options.commandId,
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.setContext("Guild", {
|
||||||
|
commandName: options.guildName,
|
||||||
|
commandId: options.guildId,
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.setUser({
|
||||||
|
username: options.username,
|
||||||
|
id: options.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return scope;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandErrorOptions {
|
||||||
|
error: Error;
|
||||||
|
|
||||||
|
commandName: string;
|
||||||
|
commandId: string;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
guildName: string;
|
||||||
|
guildId: string;
|
||||||
|
}
|
||||||
1
packages/error-handler/src/index.ts
Normal file
1
packages/error-handler/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./WyvernErrorHandler.js";
|
||||||
9
packages/error-handler/tsconfig.json
Normal file
9
packages/error-handler/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@wyvern/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
1
packages/error-handler/tsconfig.tsbuildinfo
Normal file
1
packages/error-handler/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
3
packages/eslint-config/README.md
Normal file
3
packages/eslint-config/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# `@turbo/eslint-config`
|
||||||
|
|
||||||
|
Collection of internal eslint configurations.
|
||||||
34
packages/eslint-config/library.js
Normal file
34
packages/eslint-config/library.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const { resolve } = require("node:path");
|
||||||
|
|
||||||
|
const project = resolve(process.cwd(), "tsconfig.json");
|
||||||
|
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
extends: ["eslint:recommended", "prettier", "turbo"],
|
||||||
|
plugins: ["only-warn"],
|
||||||
|
globals: {
|
||||||
|
React: true,
|
||||||
|
JSX: true,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: {
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
// Ignore dotfiles
|
||||||
|
".*.js",
|
||||||
|
"node_modules/",
|
||||||
|
"dist/",
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.js?(x)", "*.ts?(x)"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user