Merge branch 'docker' into 'master'
Dockerized local development environment See merge request csc/discord-bot!8
This commit is contained in:
commit
90c51a9e95
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
22
.eslintrc
22
.eslintrc
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
|
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
|
||||||
"env": {
|
"env": {
|
||||||
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
|
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
|
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
.env
|
.env
|
||||||
/logs
|
/logs/*
|
||||||
/dist
|
!/logs/.gitkeep
|
||||||
/db
|
/dist
|
||||||
|
/db/*
|
||||||
|
!/db/.gitkeep
|
||||||
|
|
12
.prettierrc
12
.prettierrc
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM node:16-alpine
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
COPY package.json .
|
||||||
|
COPY yarn.lock .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy app files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD [ "npm", "run", "local:run" ]
|
36
README.md
36
README.md
|
@ -1,12 +1,24 @@
|
||||||
# Codey Bot
|
# Codey Bot
|
||||||
|
|
||||||
## Required environment variables
|
## Required environment variables
|
||||||
|
|
||||||
- `BOT_TOKEN`: the token found in the bot user account.
|
- `BOT_TOKEN`: the token found in the bot user account.
|
||||||
- `NOTIF_CHANNEL_ID`: the ID of the channel the bot will send system notifications to.
|
- `NOTIF_CHANNEL_ID`: the ID of the channel the bot will send system notifications to.
|
||||||
|
|
||||||
## Running the bot locally
|
## Prerequisites
|
||||||
|
|
||||||
1. Run `yarn` to install dependencies.
|
- [Yarn](https://classic.yarnpkg.com/en/docs/install)
|
||||||
1. Add the required environment variables in a `.env` file in the root directory.
|
- [Docker](https://docs.docker.com/get-docker/) (tested up to v20.10.6)
|
||||||
1. Run `yarn dev` to start the bot locally.
|
|
||||||
|
## Running the bot locally
|
||||||
|
|
||||||
|
1. Build docker image: `yarn image:build`
|
||||||
|
1. Start container in detached mode: `yarn start`
|
||||||
|
1. View and follow console output: `yarn logs`
|
||||||
|
|
||||||
|
## Other usage
|
||||||
|
|
||||||
|
- Stop the container: `yarn stop`
|
||||||
|
- Stop and remove the container: `yarn clean`
|
||||||
|
- Restart the container: `yarn restart`
|
||||||
|
- Fresh build and restart: `yarn image:build && yarn clean && yarn start`
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
services:
|
||||||
|
codey-bot:
|
||||||
|
image: codey:latest
|
||||||
|
container_name: codey-bot
|
||||||
|
environment:
|
||||||
|
- CHOKIDAR_USEPOLLING=true
|
||||||
|
volumes:
|
||||||
|
- ./src:/usr/app/src
|
||||||
|
- ./logs:/usr/app/logs
|
||||||
|
- ./db:/usr/app/db
|
95
index.ts
95
index.ts
|
@ -1,92 +1,3 @@
|
||||||
import dotenv from 'dotenv';
|
import { startBot } from './src/bot';
|
||||||
dotenv.config();
|
|
||||||
|
startBot();
|
||||||
import Discord from 'discord.js';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { openDB, testDb } from './components/db';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.';
|
|
||||||
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.';
|
|
||||||
const BOT_PREFIX = '.';
|
|
||||||
|
|
||||||
const client = new Discord.Client();
|
|
||||||
|
|
||||||
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => {
|
|
||||||
// extract arguments by splitting by spaces and grouping strings in quotes
|
|
||||||
// e.g. .ping 1 "2 3" => ['ping', '1', '2 3']
|
|
||||||
let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
|
|
||||||
args = _.map(args, (arg) => {
|
|
||||||
if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) {
|
|
||||||
return arg.slice(1, arg.length - 1);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
});
|
|
||||||
// obtain the first argument after the prefix
|
|
||||||
const firstArg = args.shift();
|
|
||||||
if (!firstArg) return { command: null, args: [] };
|
|
||||||
const command = firstArg.toLowerCase();
|
|
||||||
return { command, args };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => {
|
|
||||||
// log command and its author info
|
|
||||||
logger.info({
|
|
||||||
event: 'command',
|
|
||||||
messageId: message.id,
|
|
||||||
author: message.author.id,
|
|
||||||
authorName: message.author.username,
|
|
||||||
channel: message.channel.id,
|
|
||||||
command,
|
|
||||||
args
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'ping':
|
|
||||||
await message.channel.send('pong');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//dev testing
|
|
||||||
if (process.env.NODE_ENV == 'dev') {
|
|
||||||
testDb(message, command, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMessage = async (message: Discord.Message) => {
|
|
||||||
// ignore messages without bot prefix and messages from other bots
|
|
||||||
if (!message.content.startsWith(BOT_PREFIX) || message.author.bot) return;
|
|
||||||
// obtain command and args from the command message
|
|
||||||
const { command, args } = parseCommand(message);
|
|
||||||
if (!command) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleCommand(message, command, args);
|
|
||||||
} catch (e) {
|
|
||||||
// log error
|
|
||||||
logger.error({
|
|
||||||
event: 'error',
|
|
||||||
messageId: message.id,
|
|
||||||
command: command,
|
|
||||||
args: args,
|
|
||||||
error: e
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startBot = async () => {
|
|
||||||
client.once('ready', async () => {
|
|
||||||
// log bot init event and send system notification
|
|
||||||
logger.info({
|
|
||||||
event: 'init'
|
|
||||||
});
|
|
||||||
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel;
|
|
||||||
notif.send('Codey is up!');
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('message', handleMessage);
|
|
||||||
|
|
||||||
client.login(BOT_TOKEN);
|
|
||||||
};
|
|
||||||
|
|
||||||
startBot();
|
|
||||||
|
|
14
package.json
14
package.json
|
@ -4,10 +4,16 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"yarn run-dev\" \"yarn watch\"",
|
"local:run": "concurrently \"yarn node:watch\" \"yarn ts:watch\"",
|
||||||
"run-dev": "cross-env NODE_ENV=dev nodemon index.ts",
|
"node:watch": "NODE_ENV=dev nodemon index.ts",
|
||||||
"watch": "tsc --watch",
|
"ts:watch": "tsc --watch",
|
||||||
"build": "tsc",
|
"ts:build": "tsc",
|
||||||
|
"image:build": "docker build . -t codey",
|
||||||
|
"clean": "docker compose down",
|
||||||
|
"stop": "docker compose stop",
|
||||||
|
"start": "docker compose up -d",
|
||||||
|
"restart": "docker compose restart",
|
||||||
|
"logs": "docker logs -f codey-bot",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import Discord from 'discord.js';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { openDB, testDb } from './components/db';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
|
const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.';
|
||||||
|
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.';
|
||||||
|
const BOT_PREFIX = '.';
|
||||||
|
|
||||||
|
const client = new Discord.Client();
|
||||||
|
|
||||||
|
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => {
|
||||||
|
// extract arguments by splitting by spaces and grouping strings in quotes
|
||||||
|
// e.g. .ping 1 "2 3" => ['ping', '1', '2 3']
|
||||||
|
let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
|
||||||
|
args = _.map(args, (arg) => {
|
||||||
|
if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) {
|
||||||
|
return arg.slice(1, arg.length - 1);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
});
|
||||||
|
// obtain the first argument after the prefix
|
||||||
|
const firstArg = args.shift();
|
||||||
|
if (!firstArg) return { command: null, args: [] };
|
||||||
|
const command = firstArg.toLowerCase();
|
||||||
|
return { command, args };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => {
|
||||||
|
// log command and its author info
|
||||||
|
logger.info({
|
||||||
|
event: 'command',
|
||||||
|
messageId: message.id,
|
||||||
|
author: message.author.id,
|
||||||
|
authorName: message.author.username,
|
||||||
|
channel: message.channel.id,
|
||||||
|
command,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'ping':
|
||||||
|
await message.channel.send('pong');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//dev testing
|
||||||
|
if (process.env.NODE_ENV == 'dev') {
|
||||||
|
testDb(message, command, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessage = async (message: Discord.Message) => {
|
||||||
|
// ignore messages without bot prefix and messages from other bots
|
||||||
|
if (!message.content.startsWith(BOT_PREFIX) || message.author.bot) return;
|
||||||
|
// obtain command and args from the command message
|
||||||
|
const { command, args } = parseCommand(message);
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleCommand(message, command, args);
|
||||||
|
} catch (e) {
|
||||||
|
// log error
|
||||||
|
logger.error({
|
||||||
|
event: 'error',
|
||||||
|
messageId: message.id,
|
||||||
|
command: command,
|
||||||
|
args: args,
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startBot = async () => {
|
||||||
|
client.once('ready', async () => {
|
||||||
|
// log bot init event and send system notification
|
||||||
|
logger.info({
|
||||||
|
event: 'init'
|
||||||
|
});
|
||||||
|
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel;
|
||||||
|
notif.send('Codey is up!');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', handleMessage);
|
||||||
|
|
||||||
|
client.login(BOT_TOKEN);
|
||||||
|
};
|
|
@ -1,22 +1,22 @@
|
||||||
import sqlite3 = require('sqlite3')
|
import sqlite3 = require('sqlite3');
|
||||||
import { open, Database } from 'sqlite'
|
import { open, Database } from 'sqlite';
|
||||||
import Discord from 'discord.js'
|
import Discord from 'discord.js';
|
||||||
|
|
||||||
let db : Database | null = null;
|
let db: Database | null = null;
|
||||||
|
|
||||||
export async function openDB () {
|
export async function openDB() {
|
||||||
if(db == null){
|
if (db == null) {
|
||||||
db = await open({
|
db = await open({
|
||||||
filename: './db/bot.db',
|
filename: 'db/bot.db',
|
||||||
driver: sqlite3.Database
|
driver: sqlite3.Database
|
||||||
})
|
});
|
||||||
await db.run('CREATE TABLE IF NOT EXISTS saved_data (msg_id INTEGER PRIMARY KEY,data TEXT NOT NULL);')
|
await db.run('CREATE TABLE IF NOT EXISTS saved_data (msg_id INTEGER PRIMARY KEY,data TEXT NOT NULL);');
|
||||||
}
|
}
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testDb(message: Discord.Message, command: string, args: string[]){
|
export async function testDb(message: Discord.Message, command: string, args: string[]) {
|
||||||
switch(command){
|
switch (command) {
|
||||||
case 'save':
|
case 'save':
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
await message.channel.send('no args');
|
await message.channel.send('no args');
|
||||||
|
@ -35,7 +35,7 @@ export async function testDb(message: Discord.Message, command: string, args: st
|
||||||
.setTitle('Database Dump')
|
.setTitle('Database Dump')
|
||||||
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||||
const res = await db.all('SELECT * FROM saved_data');
|
const res = await db.all('SELECT * FROM saved_data');
|
||||||
for(const rows of res){
|
for (const rows of res) {
|
||||||
console.log(rows['msg_id'], rows['data']);
|
console.log(rows['msg_id'], rows['data']);
|
||||||
outEmbed = outEmbed.addField(rows['msg_id'], rows['data'], true);
|
outEmbed = outEmbed.addField(rows['msg_id'], rows['data'], true);
|
||||||
console.log(outEmbed);
|
console.log(outEmbed);
|
||||||
|
@ -65,4 +65,4 @@ export async function testDb(message: Discord.Message, command: string, args: st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('connected to db')
|
console.log('connected to db');
|
|
@ -1,40 +1,40 @@
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
require('winston-daily-rotate-file');
|
require('winston-daily-rotate-file');
|
||||||
|
|
||||||
const dailyRotateTransport = new winston.transports.DailyRotateFile({
|
const dailyRotateTransport = new winston.transports.DailyRotateFile({
|
||||||
filename: '%DATE%.log',
|
filename: '%DATE%.log',
|
||||||
dirname: 'logs',
|
dirname: 'logs',
|
||||||
zippedArchive: true
|
zippedArchive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const dailyRotateErrorTransport = new winston.transports.DailyRotateFile({
|
const dailyRotateErrorTransport = new winston.transports.DailyRotateFile({
|
||||||
filename: 'error-%DATE%.log',
|
filename: 'error-%DATE%.log',
|
||||||
dirname: 'logs',
|
dirname: 'logs',
|
||||||
zippedArchive: true,
|
zippedArchive: true,
|
||||||
level: 'error'
|
level: 'error'
|
||||||
});
|
});
|
||||||
|
|
||||||
const consoleTransport = new winston.transports.Console({
|
const consoleTransport = new winston.transports.Console({
|
||||||
format: winston.format.prettyPrint()
|
format: winston.format.prettyPrint()
|
||||||
});
|
});
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
winston.format.printf(
|
winston.format.printf(
|
||||||
({ level, message, timestamp }: { level: string; message: string; timestamp: string }) =>
|
({ level, message, timestamp }: { level: string; message: string; timestamp: string }) =>
|
||||||
`[${timestamp}] ${level}: ${JSON.stringify(message)}`
|
`[${timestamp}] ${level}: ${JSON.stringify(message)}`
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
transports: [dailyRotateTransport, dailyRotateErrorTransport]
|
transports: [dailyRotateTransport, dailyRotateErrorTransport]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'dev') {
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
logger.add(
|
logger.add(
|
||||||
new winston.transports.Console({
|
new winston.transports.Console({
|
||||||
format: winston.format.prettyPrint()
|
format: winston.format.prettyPrint()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"extends": "@tsconfig/node14/tsconfig.json",
|
"extends": "@tsconfig/node14/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"]
|
"include": ["**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue