Initialize Sapphire project

This commit is contained in:
SadlyNotSappho 2023-02-10 10:46:20 -08:00
parent a9f52544c7
commit 639bbc1126
22 changed files with 3988 additions and 2 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
# Tokens
DISCORD_TOKEN=
# Configuration
DEFAULT_PREFIX=

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,yarn
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,yarn
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### yarn ###
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.yarn/*
!.yarn/releases
!.yarn/patches
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# if you are NOT using Zero-installs, then:
# comment the following lines
!.yarn/cache
# and uncomment the following lines
# .pnp.*
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,yarn
.env
node_modules

4
.prettierignore Normal file
View File

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

14
.sapphirerc.json Normal file
View File

@ -0,0 +1,14 @@
{
"projectLanguage": "ts",
"locations": {
"base": "src",
"arguments": "arguments",
"commands": "commands",
"listeners": "listeners",
"preconditions": "preconditions"
},
"customFileTemplates": {
"enabled": false,
"location": ""
}
}

File diff suppressed because one or more lines are too long

873
.yarn/releases/yarn-3.4.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

9
.yarnrc.yml Normal file
View File

@ -0,0 +1,9 @@
enableGlobalCache: true
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
yarnPath: .yarn/releases/yarn-3.4.1.cjs

90
Dockerfile Normal file
View File

@ -0,0 +1,90 @@
# ================ #
# Base Stage #
# ================ #
FROM node:16-buster-slim as base
WORKDIR /opt/app
ENV HUSKY=0
ENV CI=true
RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends build-essential python3 libfontconfig1 dumb-init && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# ------------------------------------ #
# Conditional steps for end-users #
# ------------------------------------ #
# Enable one of the following depending on whether you use yarn or npm, then remove the other one
# COPY --chown=node:node yarn.lock .
# COPY --chown=node:node package-lock.json .
# If you use Yarn v3 then enable the following lines:
# COPY --chown=node:node .yarnrc.yml .
# COPY --chown=node:node .yarn/ .yarn/
# If you have an additional "tsconfig.base.json" file then enable the following line:
# COPY --chown=node:node tsconfig.base.json tsconfig.base.json
# If you require additional NodeJS flags then specify them here
ENV NODE_OPTIONS="--enable-source-maps"
# ---------------------------------------- #
# End Conditional steps for end-users #
# ---------------------------------------- #
COPY --chown=node:node package.json .
COPY --chown=node:node tsconfig.json .
RUN sed -i 's/"prepare": "husky install\( .github\/husky\)\?"/"prepare": ""/' ./package.json
ENTRYPOINT ["dumb-init", "--"]
# =================== #
# Development Stage #
# =================== #
# Development, used for development only (defaults to watch command)
FROM base as development
ENV NODE_ENV="development"
USER node
CMD [ "npm", "run", "docker:watch"]
# ================ #
# Builder Stage #
# ================ #
# Build stage for production
FROM base as build
RUN npm install
COPY . /opt/app
RUN npm run build
# ==================== #
# Production Stage #
# ==================== #
# Production image used to run the bot in production, only contains node_modules & dist contents.
FROM base as production
ENV NODE_ENV="production"
COPY --from=build /opt/app/dist /opt/app/dist
COPY --from=build /opt/app/node_modules /opt/app/node_modules
COPY --from=build /opt/app/package.json /opt/app/package.json
RUN chown node:node /opt/app/
USER node
CMD [ "npm", "run", "start"]

View File

@ -1,3 +1,36 @@
# discord-button-roles # Docker Sapphire Bot example
A Discord bot that serves one purpose: button roles, as I haven't found a good free one, that doesn't also have a million other features. This is a basic setup of a Discord bot using the [sapphire framework][sapphire] written in TypeScript containerized with Docker
## How to use it?
### Prerequisite
1. Copy the `.env.example` and rename it to `.env`, make sure to fill the Token.
2. Open the Dockerfile and in the block marked with `Conditional steps for end-users` make sure you enable the lines that apply to your project.
### Development
Run `docker-compose up`. This will start the bot in watch mode and automatically run it after each save.
It will build the `Dockerfile` up until the `development` stage.
### Production
Just like in the development step, you have to fill in the `.env` file and then run the following command to create a production image;
```sh
docker build . -t sapphire-sample-bot
```
To test if your image works, you can run:
```sh
docker run --env-file .env sapphire-sample-bot
```
## 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

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
version: '3.9'
services:
sapphire-sample-bot:
build:
context: .
target: development
env_file: .env
volumes:
- ./:/opt/app

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "discord-button-roles",
"version": "1.0.0",
"main": "dist/index.js",
"author": "@sapphire",
"license": "UNLICENSE",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"start": "node dist/index.js",
"dev": "run-s build start",
"format": "prettier --write \"src/**/*.ts\"",
"predocker:watch": "npm install",
"docker:watch": "tsc-watch --onSuccess \"node ./dist/index.js\""
},
"dependencies": {
"@sapphire/decorators": "^6.0.0",
"@sapphire/discord-utilities": "^3.0.0",
"@sapphire/discord.js-utilities": "6.0.1",
"@sapphire/fetch": "^2.4.1",
"@sapphire/framework": "^4.0.2",
"@sapphire/plugin-api": "^5.0.0",
"@sapphire/plugin-editable-commands": "^3.0.0",
"@sapphire/plugin-logger": "^3.0.1",
"@sapphire/plugin-subcommands": "^4.0.0",
"@sapphire/time-utilities": "^1.7.8",
"@sapphire/type": "^2.3.0",
"@sapphire/utilities": "^3.11.0",
"@skyra/env-utilities": "^1.1.0",
"discord.js": "^14.7.1"
},
"devDependencies": {
"@sapphire/prettier-config": "^1.4.5",
"@sapphire/ts-config": "^3.3.4",
"@types/node": "^18.11.18",
"@types/ws": "^8.5.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.3",
"tsc-watch": "^6.0.0",
"typescript": "^4.9.4"
},
"prettier": "@sapphire/prettier-config",
"packageManager": "yarn@3.4.1"
}

View File

@ -0,0 +1,20 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Command } from '@sapphire/framework';
import { send } from '@sapphire/plugin-editable-commands';
import type { Message } from 'discord.js';
@ApplyOptions<Command.Options>({
description: 'ping pong'
})
export class UserCommand extends Command {
public async messageRun(message: Message) {
const msg = await send(message, 'Ping?');
return send(
message,
`Pong from Docker! Bot Latency ${Math.round(this.container.client.ws.ping)}ms. API Latency ${
msg.createdTimestamp - message.createdTimestamp
}ms.`
);
}
}

41
src/index.ts Normal file
View File

@ -0,0 +1,41 @@
import './lib/setup';
import { LogLevel, SapphireClient } from '@sapphire/framework';
import { GatewayIntentBits, Partials } from 'discord.js';
const client = new SapphireClient({
defaultPrefix: process.env.DEFAULT_PREFIX,
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');
} catch (error) {
client.logger.fatal(error);
client.destroy();
process.exit(1);
}
};
main();

4
src/lib/constants.ts Normal file
View File

@ -0,0 +1,4 @@
import { join } from 'path';
export const rootDir = join(__dirname, '..', '..');
export const srcDir = join(rootDir, 'src');

20
src/lib/setup.ts Normal file
View File

@ -0,0 +1,20 @@
// Unless explicitly defined, set NODE_ENV as development:
process.env.NODE_ENV ??= 'development';
import '@sapphire/plugin-api/register';
import '@sapphire/plugin-editable-commands/register';
import '@sapphire/plugin-logger/register';
import * as colorette from 'colorette';
import { setup } from '@skyra/env-utilities';
import { join } from 'path';
import { inspect } from 'util';
import { srcDir } from './constants';
// Read env var
setup({ path: join(srcDir, '.env') });
// Set default inspection depth
inspect.defaultOptions.depth = 1;
// Enable colorette
colorette.createColors({ useColor: true });

View File

@ -0,0 +1,40 @@
import type { MessageCommandSuccessPayload } from '@sapphire/framework';
import { Command, Listener, LogLevel } from '@sapphire/framework';
import type { Logger } from '@sapphire/plugin-logger';
import { cyan } from 'colorette';
import type { Guild, User } from 'discord.js';
export class UserEvent extends Listener {
public run({ message, command }: MessageCommandSuccessPayload) {
const shard = this.shard(message.guild?.shardId ?? 0);
const commandName = this.command(command);
const author = this.author(message.author);
const sentAt = message.guild ? this.guild(message.guild) : this.direct();
this.container.logger.debug(`${shard} - ${commandName} ${author} ${sentAt}`);
}
public onLoad() {
this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug;
return super.onLoad();
}
private shard(id: number) {
return `[${cyan(id.toString())}]`;
}
private command(command: Command) {
return cyan(command.name);
}
private author(author: User) {
return `${author.username}[${cyan(author.id)}]`;
}
private direct() {
return cyan('Direct Messages');
}
private guild(guild: Guild) {
return `${guild.name}[${cyan(guild.id)}]`;
}
}

View File

@ -0,0 +1,9 @@
import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js';
export class UserEvent extends Listener {
public async run(message: Message) {
const prefix = this.container.client.options.defaultPrefix;
return message.channel.send(prefix ? `My prefix in this guild is: \`${prefix}\`` : 'Cannot find any Prefix for Message Commands.');
}
}

View File

@ -0,0 +1,21 @@
import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js';
export class UserEvent extends Listener {
public run(old: Message, message: Message) {
// If the contents of both messages are the same, return:
if (old.content === message.content) return;
// If the message was sent by a webhook, return:
if (message.webhookId !== null) return;
// If the message was sent by the system, return:
if (message.system) return;
// If the message was sent by a bot, return:
if (message.author.bot) return;
// Run the message parser.
this.container.client.emit('preMessageParsed', message);
}
}

50
src/listeners/ready.ts Normal file
View File

@ -0,0 +1,50 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Listener, Store } from '@sapphire/framework';
import { blue, gray, green, magenta, magentaBright, white, yellow } from 'colorette';
const dev = process.env.NODE_ENV !== 'production';
@ApplyOptions<Listener.Options>({ once: true })
export class UserEvent extends Listener {
private readonly style = dev ? yellow : blue;
public 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: Store<any>, last: boolean) {
return gray(`${last ? '└─' : '├─'} Loaded ${this.style(store.size.toString().padEnd(3, ' '))} ${store.name}.`);
}
}

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "@sapphire/ts-config",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": ["src"]
}

2637
yarn.lock Normal file

File diff suppressed because it is too large Load Diff