mirror of
https://github.com/Xevion/leebot.git
synced 2025-12-05 23:15:20 -06:00
Initial commit
Created using ``@sapphire/cli`
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Tokens
|
||||
DISCORD_TOKEN=
|
||||
|
||||
# Configuration
|
||||
DEFAULT_PREFIX=
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Ignore a blackhole and the folder for development
|
||||
node_modules/
|
||||
.vs/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Yarn files
|
||||
.yarn/install-state.gz
|
||||
.yarn/build-state.yml
|
||||
|
||||
# Environment variables
|
||||
.DS_Store
|
||||
|
||||
dist/
|
||||
|
||||
# Ignore the config file (contains sensitive information such as tokens)
|
||||
config.ts
|
||||
|
||||
# Ignore heapsnapshot and log files
|
||||
*.heapsnapshot
|
||||
*.log
|
||||
|
||||
# Ignore npm lockfiles file
|
||||
package-lock.json
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
14
.sapphirerc.json
Normal file
14
.sapphirerc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"projectLanguage": "ts",
|
||||
"locations": {
|
||||
"base": "src",
|
||||
"arguments": "arguments",
|
||||
"commands": "commands",
|
||||
"listeners": "listeners",
|
||||
"preconditions": "preconditions"
|
||||
},
|
||||
"customFileTemplates": {
|
||||
"enabled": false,
|
||||
"location": ""
|
||||
}
|
||||
}
|
||||
90
Dockerfile
Normal file
90
Dockerfile
Normal 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"]
|
||||
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Docker Sapphire Bot example
|
||||
|
||||
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-leebot
|
||||
```
|
||||
|
||||
To test if your image works, you can run:
|
||||
|
||||
```sh
|
||||
docker run --env-file .env sapphire-leebot
|
||||
```
|
||||
|
||||
## 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
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
sapphire-leebot:
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./:/opt/app
|
||||
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "leebot",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"author": "@sapphire",
|
||||
"license": "UNLICENSE",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^0.4.0",
|
||||
"@sapphire/decorators": "^4.0.2",
|
||||
"@sapphire/discord-utilities": "^2.6.0",
|
||||
"@sapphire/discord.js-utilities": "^4.4.0",
|
||||
"@sapphire/fetch": "^2.0.4",
|
||||
"@sapphire/framework": "^2.3.0",
|
||||
"@sapphire/plugin-api": "^3.1.3",
|
||||
"@sapphire/plugin-editable-commands": "*",
|
||||
"@sapphire/plugin-logger": "^2.1.2",
|
||||
"@sapphire/plugin-subcommands": "^2.1.3",
|
||||
"@sapphire/time-utilities": "^1.5.2",
|
||||
"@sapphire/type": "^2.1.2",
|
||||
"@sapphire/utilities": "^3.2.1",
|
||||
"discord.js": "^13.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sapphire/prettier-config": "^1.2.9",
|
||||
"@sapphire/ts-config": "^3.1.8",
|
||||
"@types/node": "^17.0.10",
|
||||
"@types/ws": "^8.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.5.1",
|
||||
"tsc-watch": "^4.6.0",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"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\""
|
||||
},
|
||||
"prettier": "@sapphire/prettier-config"
|
||||
}
|
||||
21
src/commands/General/ping.ts
Normal file
21
src/commands/General/ping.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Command, CommandOptions, PieceContext } from '@sapphire/framework';
|
||||
import type { Message } from 'discord.js';
|
||||
|
||||
export class UserCommand extends Command {
|
||||
public constructor(context: PieceContext, options: CommandOptions) {
|
||||
super(context, {
|
||||
...options,
|
||||
description: 'ping pong'
|
||||
});
|
||||
}
|
||||
|
||||
public async messageRun(message: Message) {
|
||||
const msg = await message.channel.send('Ping?');
|
||||
|
||||
return msg.edit(
|
||||
`Pong from Docker! Bot Latency ${Math.round(this.container.client.ws.ping)}ms. API Latency ${
|
||||
msg.createdTimestamp - message.createdTimestamp
|
||||
}ms.`
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/index.ts
Normal file
37
src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import './lib/setup';
|
||||
import { LogLevel, SapphireClient } from '@sapphire/framework';
|
||||
|
||||
const client = new SapphireClient({
|
||||
defaultPrefix: process.env.DEFAULT_PREFIX,
|
||||
regexPrefix: /^(hey +)?bot[,! ]/i,
|
||||
caseInsensitiveCommands: true,
|
||||
logger: {
|
||||
level: LogLevel.Debug
|
||||
},
|
||||
shards: 'auto',
|
||||
intents: [
|
||||
'GUILDS',
|
||||
'GUILD_MEMBERS',
|
||||
'GUILD_BANS',
|
||||
'GUILD_EMOJIS_AND_STICKERS',
|
||||
'GUILD_VOICE_STATES',
|
||||
'GUILD_MESSAGES',
|
||||
'GUILD_MESSAGE_REACTIONS',
|
||||
'DIRECT_MESSAGES',
|
||||
'DIRECT_MESSAGE_REACTIONS'
|
||||
]
|
||||
});
|
||||
|
||||
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();
|
||||
11
src/lib/setup.ts
Normal file
11
src/lib/setup.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import '@sapphire/plugin-logger/register';
|
||||
import '@sapphire/plugin-api/register';
|
||||
import '@sapphire/plugin-editable-commands/register';
|
||||
import * as colorette from 'colorette';
|
||||
import { inspect } from 'util';
|
||||
|
||||
// Set default inspection depth
|
||||
inspect.defaultOptions.depth = 1;
|
||||
|
||||
// Enable colorette
|
||||
colorette.createColors({ useColor: true });
|
||||
47
src/listeners/commands/commandSuccessLogger.ts
Normal file
47
src/listeners/commands/commandSuccessLogger.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { CommandSuccessPayload, ListenerOptions, PieceContext } from '@sapphire/framework';
|
||||
import { Command, Events, 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<typeof Events.CommandSuccess> {
|
||||
public constructor(context: PieceContext, options?: ListenerOptions) {
|
||||
super(context, {
|
||||
...options,
|
||||
event: Events.CommandSuccess
|
||||
});
|
||||
}
|
||||
|
||||
public run({ message, command }: CommandSuccessPayload) {
|
||||
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)}]`;
|
||||
}
|
||||
}
|
||||
9
src/listeners/mentionPrefixOnly.ts
Normal file
9
src/listeners/mentionPrefixOnly.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Listener } from '@sapphire/framework';
|
||||
import type { Message } from 'discord.js';
|
||||
|
||||
export class UserEvent extends Listener<'mentionPrefixOnly'> {
|
||||
public async run(message: Message) {
|
||||
const prefix = this.container.client.options.defaultPrefix;
|
||||
return message.channel.send(prefix ? `My prefix in this guild is: \`${prefix}\`` : 'You do not need a prefix in DMs.');
|
||||
}
|
||||
}
|
||||
21
src/listeners/messages/messageUpdate.ts
Normal file
21
src/listeners/messages/messageUpdate.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
56
src/listeners/ready.ts
Normal file
56
src/listeners/ready.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ListenerOptions, PieceContext } from '@sapphire/framework';
|
||||
import { Listener, Store } from '@sapphire/framework';
|
||||
import { blue, gray, green, magenta, magentaBright, white, yellow } from 'colorette';
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export class UserEvent extends Listener {
|
||||
private readonly style = dev ? yellow : blue;
|
||||
|
||||
public constructor(context: PieceContext, options?: ListenerOptions) {
|
||||
super(context, {
|
||||
...options,
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
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
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@sapphire/ts-config",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user