Initialize Sapphire project
This commit is contained in:
parent
a9f52544c7
commit
639bbc1126
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Tokens
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DEFAULT_PREFIX=
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.yarn/
|
||||||
|
examples/*/dist/
|
|
@ -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
File diff suppressed because one or more lines are too long
|
@ -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
|
|
@ -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"]
|
37
README.md
37
README.md
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
sapphire-sample-bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: development
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./:/opt/app
|
|
@ -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"
|
||||||
|
}
|
|
@ -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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const rootDir = join(__dirname, '..', '..');
|
||||||
|
export const srcDir = join(rootDir, 'src');
|
|
@ -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 });
|
|
@ -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)}]`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}.`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@sapphire/ts-config",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
Loading…
Reference in New Issue