dockerized local dev env

dockerize init

using docker compose to manage container

removed debug

using npm instead of yarn when building image

change container workdir

persist logs and db using volumes
This commit is contained in:
Charles Zhang 2021-05-29 02:40:40 -04:00
parent f50e1ba47a
commit cf38d5b181
14 changed files with 243 additions and 199 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -1,11 +1,11 @@
{
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
"env": {
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
},
"extends": [
"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
"plugin:prettier/recommended"
]
}
{
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
"env": {
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
},
"extends": [
"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
"plugin:prettier/recommended"
]
}

10
.gitignore vendored
View File

@ -1,5 +1,5 @@
/node_modules
.env
/logs
/dist
/db
/node_modules
.env
/logs/*
/dist
/db/*

View File

@ -1,6 +1,6 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 120
}
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 120
}

14
.vscode/settings.json vendored
View File

@ -1,7 +1,7 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

14
Dockerfile Normal file
View File

@ -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" ]

View File

@ -1,12 +1,24 @@
# Codey Bot
## Required environment variables
- `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.
## Running the bot locally
1. Run `yarn` to install dependencies.
1. Add the required environment variables in a `.env` file in the root directory.
1. Run `yarn dev` to start the bot locally.
# Codey Bot
## Required environment variables
- `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.
## Prerequisites
- [Yarn](https://classic.yarnpkg.com/en/docs/install)
- [Docker](https://docs.docker.com/get-docker/) (tested up to v20.10.6)
## 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`

10
docker-compose.yml Normal file
View File

@ -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

View File

@ -1,92 +1,3 @@
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
});
}
};
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();
import { startBot } from './src/bot';
startBot();

View File

@ -4,10 +4,16 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "concurrently \"yarn run-dev\" \"yarn watch\"",
"run-dev": "cross-env NODE_ENV=dev nodemon index.ts",
"watch": "tsc --watch",
"build": "tsc",
"local:run": "concurrently \"yarn node:watch\" \"yarn ts:watch\"",
"node:watch": "NODE_ENV=dev nodemon index.ts",
"ts:watch": "tsc --watch",
"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"
},
"author": "",

90
src/bot.ts Normal file
View File

@ -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);
};

View File

@ -1,22 +1,22 @@
import sqlite3 = require('sqlite3')
import { open, Database } from 'sqlite'
import Discord from 'discord.js'
import sqlite3 = require('sqlite3');
import { open, Database } from 'sqlite';
import Discord from 'discord.js';
let db : Database | null = null;
let db: Database | null = null;
export async function openDB () {
if(db == null){
export async function openDB() {
if (db == null) {
db = await open({
filename: './db/bot.db',
filename: 'db/bot.db',
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;
}
export async function testDb(message: Discord.Message, command: string, args: string[]){
switch(command){
export async function testDb(message: Discord.Message, command: string, args: string[]) {
switch (command) {
case 'save':
if (args.length < 1) {
await message.channel.send('no args');
@ -35,7 +35,7 @@ export async function testDb(message: Discord.Message, command: string, args: st
.setTitle('Database Dump')
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
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']);
outEmbed = outEmbed.addField(rows['msg_id'], rows['data'], true);
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');

View File

@ -1,40 +1,40 @@
const winston = require('winston');
require('winston-daily-rotate-file');
const dailyRotateTransport = new winston.transports.DailyRotateFile({
filename: '%DATE%.log',
dirname: 'logs',
zippedArchive: true
});
const dailyRotateErrorTransport = new winston.transports.DailyRotateFile({
filename: 'error-%DATE%.log',
dirname: 'logs',
zippedArchive: true,
level: 'error'
});
const consoleTransport = new winston.transports.Console({
format: winston.format.prettyPrint()
});
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(
({ level, message, timestamp }: { level: string; message: string; timestamp: string }) =>
`[${timestamp}] ${level}: ${JSON.stringify(message)}`
)
),
transports: [dailyRotateTransport, dailyRotateErrorTransport]
});
if (process.env.NODE_ENV === 'dev') {
logger.add(
new winston.transports.Console({
format: winston.format.prettyPrint()
})
);
}
export default logger;
const winston = require('winston');
require('winston-daily-rotate-file');
const dailyRotateTransport = new winston.transports.DailyRotateFile({
filename: '%DATE%.log',
dirname: 'logs',
zippedArchive: true
});
const dailyRotateErrorTransport = new winston.transports.DailyRotateFile({
filename: 'error-%DATE%.log',
dirname: 'logs',
zippedArchive: true,
level: 'error'
});
const consoleTransport = new winston.transports.Console({
format: winston.format.prettyPrint()
});
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(
({ level, message, timestamp }: { level: string; message: string; timestamp: string }) =>
`[${timestamp}] ${level}: ${JSON.stringify(message)}`
)
),
transports: [dailyRotateTransport, dailyRotateErrorTransport]
});
if (process.env.NODE_ENV === 'dev') {
logger.add(
new winston.transports.Console({
format: winston.format.prettyPrint()
})
);
}
export default logger;

View File

@ -1,9 +1,9 @@
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"preserveConstEnums": true,
"esModuleInterop": true
},
"include": ["**/*.ts"]
}
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"preserveConstEnums": true,
"esModuleInterop": true
},
"include": ["**/*.ts"]
}