Skip to content

Commit 09cda80

Browse files
Dockerized local development environment
1 parent f50e1ba commit 09cda80

16 files changed

+245
-199
lines changed

.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

.eslintrc

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
{
2-
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
3-
"env": {
4-
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
5-
},
6-
"extends": [
7-
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
8-
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
9-
"plugin:prettier/recommended"
10-
]
11-
}
1+
{
2+
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
3+
"env": {
4+
"ecmaVersion": 2020 // Allows for the parsing of modern ECMAScript features
5+
},
6+
"extends": [
7+
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
8+
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
9+
"plugin:prettier/recommended"
10+
]
11+
}

.gitignore

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
/node_modules
2-
.env
3-
/logs
4-
/dist
5-
/db
1+
/node_modules
2+
.env
3+
/logs/*
4+
!/logs/.gitkeep
5+
/dist
6+
/db/*
7+
!/db/.gitkeep

.prettierrc

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{
2-
"semi": true,
3-
"trailingComma": "none",
4-
"singleQuote": true,
5-
"printWidth": 120
6-
}
1+
{
2+
"semi": true,
3+
"trailingComma": "none",
4+
"singleQuote": true,
5+
"printWidth": 120
6+
}

.vscode/settings.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
{
2-
"editor.defaultFormatter": "esbenp.prettier-vscode",
3-
"editor.formatOnSave": true,
4-
"[javascript]": {
5-
"editor.defaultFormatter": "esbenp.prettier-vscode"
6-
}
7-
}
1+
{
2+
"editor.defaultFormatter": "esbenp.prettier-vscode",
3+
"editor.formatOnSave": true,
4+
"[javascript]": {
5+
"editor.defaultFormatter": "esbenp.prettier-vscode"
6+
}
7+
}

Dockerfile

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM node:16-alpine
2+
3+
# Create app directory
4+
WORKDIR /usr/app
5+
6+
# Install app dependencies
7+
COPY package.json .
8+
COPY yarn.lock .
9+
RUN npm install
10+
11+
# Copy app files
12+
COPY . .
13+
14+
CMD [ "npm", "run", "local:run" ]

README.md

+24-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1-
# Codey Bot
2-
3-
## Required environment variables
4-
5-
- `BOT_TOKEN`: the token found in the bot user account.
6-
- `NOTIF_CHANNEL_ID`: the ID of the channel the bot will send system notifications to.
7-
8-
## Running the bot locally
9-
10-
1. Run `yarn` to install dependencies.
11-
1. Add the required environment variables in a `.env` file in the root directory.
12-
1. Run `yarn dev` to start the bot locally.
1+
# Codey Bot
2+
3+
## Required environment variables
4+
5+
- `BOT_TOKEN`: the token found in the bot user account.
6+
- `NOTIF_CHANNEL_ID`: the ID of the channel the bot will send system notifications to.
7+
8+
## Prerequisites
9+
10+
- [Yarn](https://classic.yarnpkg.com/en/docs/install)
11+
- [Docker](https://docs.docker.com/get-docker/) (tested up to v20.10.6)
12+
13+
## Running the bot locally
14+
15+
1. Build docker image: `yarn image:build`
16+
1. Start container in detached mode: `yarn start`
17+
1. View and follow console output: `yarn logs`
18+
19+
## Other usage
20+
21+
- Stop the container: `yarn stop`
22+
- Stop and remove the container: `yarn clean`
23+
- Restart the container: `yarn restart`
24+
- Fresh build and restart: `yarn image:build && yarn clean && yarn start`

db/.gitkeep

Whitespace-only changes.

docker-compose.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
codey-bot:
3+
image: codey:latest
4+
container_name: codey-bot
5+
environment:
6+
- CHOKIDAR_USEPOLLING=true
7+
volumes:
8+
- ./src:/usr/app/src
9+
- ./logs:/usr/app/logs
10+
- ./db:/usr/app/db

index.ts

+3-92
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,3 @@
1-
import dotenv from 'dotenv';
2-
dotenv.config();
3-
4-
import Discord from 'discord.js';
5-
import _ from 'lodash';
6-
import { openDB, testDb } from './components/db';
7-
import logger from './logger';
8-
9-
const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.';
10-
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.';
11-
const BOT_PREFIX = '.';
12-
13-
const client = new Discord.Client();
14-
15-
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => {
16-
// extract arguments by splitting by spaces and grouping strings in quotes
17-
// e.g. .ping 1 "2 3" => ['ping', '1', '2 3']
18-
let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
19-
args = _.map(args, (arg) => {
20-
if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) {
21-
return arg.slice(1, arg.length - 1);
22-
}
23-
return arg;
24-
});
25-
// obtain the first argument after the prefix
26-
const firstArg = args.shift();
27-
if (!firstArg) return { command: null, args: [] };
28-
const command = firstArg.toLowerCase();
29-
return { command, args };
30-
};
31-
32-
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => {
33-
// log command and its author info
34-
logger.info({
35-
event: 'command',
36-
messageId: message.id,
37-
author: message.author.id,
38-
authorName: message.author.username,
39-
channel: message.channel.id,
40-
command,
41-
args
42-
});
43-
44-
switch (command) {
45-
case 'ping':
46-
await message.channel.send('pong');
47-
break;
48-
}
49-
50-
//dev testing
51-
if (process.env.NODE_ENV == 'dev') {
52-
testDb(message, command, args);
53-
}
54-
};
55-
56-
const handleMessage = async (message: Discord.Message) => {
57-
// ignore messages without bot prefix and messages from other bots
58-
if (!message.content.startsWith(BOT_PREFIX) || message.author.bot) return;
59-
// obtain command and args from the command message
60-
const { command, args } = parseCommand(message);
61-
if (!command) return;
62-
63-
try {
64-
await handleCommand(message, command, args);
65-
} catch (e) {
66-
// log error
67-
logger.error({
68-
event: 'error',
69-
messageId: message.id,
70-
command: command,
71-
args: args,
72-
error: e
73-
});
74-
}
75-
};
76-
77-
const startBot = async () => {
78-
client.once('ready', async () => {
79-
// log bot init event and send system notification
80-
logger.info({
81-
event: 'init'
82-
});
83-
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel;
84-
notif.send('Codey is up!');
85-
});
86-
87-
client.on('message', handleMessage);
88-
89-
client.login(BOT_TOKEN);
90-
};
91-
92-
startBot();
1+
import { startBot } from './src/bot';
2+
3+
startBot();

logs/.gitkeep

Whitespace-only changes.

package.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44
"description": "",
55
"main": "index.js",
66
"scripts": {
7-
"dev": "concurrently \"yarn run-dev\" \"yarn watch\"",
8-
"run-dev": "cross-env NODE_ENV=dev nodemon index.ts",
9-
"watch": "tsc --watch",
10-
"build": "tsc",
7+
"local:run": "concurrently \"yarn node:watch\" \"yarn ts:watch\"",
8+
"node:watch": "NODE_ENV=dev nodemon index.ts",
9+
"ts:watch": "tsc --watch",
10+
"ts:build": "tsc",
11+
"image:build": "docker build . -t codey",
12+
"clean": "docker compose down",
13+
"stop": "docker compose stop",
14+
"start": "docker compose up -d",
15+
"restart": "docker compose restart",
16+
"logs": "docker logs -f codey-bot",
1117
"test": "echo \"Error: no test specified\" && exit 1"
1218
},
1319
"author": "",

src/bot.ts

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import dotenv from 'dotenv';
2+
dotenv.config();
3+
4+
import Discord from 'discord.js';
5+
import _ from 'lodash';
6+
import { openDB, testDb } from './components/db';
7+
import logger from './logger';
8+
9+
const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.';
10+
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.';
11+
const BOT_PREFIX = '.';
12+
13+
const client = new Discord.Client();
14+
15+
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => {
16+
// extract arguments by splitting by spaces and grouping strings in quotes
17+
// e.g. .ping 1 "2 3" => ['ping', '1', '2 3']
18+
let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
19+
args = _.map(args, (arg) => {
20+
if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) {
21+
return arg.slice(1, arg.length - 1);
22+
}
23+
return arg;
24+
});
25+
// obtain the first argument after the prefix
26+
const firstArg = args.shift();
27+
if (!firstArg) return { command: null, args: [] };
28+
const command = firstArg.toLowerCase();
29+
return { command, args };
30+
};
31+
32+
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => {
33+
// log command and its author info
34+
logger.info({
35+
event: 'command',
36+
messageId: message.id,
37+
author: message.author.id,
38+
authorName: message.author.username,
39+
channel: message.channel.id,
40+
command,
41+
args
42+
});
43+
44+
switch (command) {
45+
case 'ping':
46+
await message.channel.send('pong');
47+
break;
48+
}
49+
50+
//dev testing
51+
if (process.env.NODE_ENV == 'dev') {
52+
testDb(message, command, args);
53+
}
54+
};
55+
56+
const handleMessage = async (message: Discord.Message) => {
57+
// ignore messages without bot prefix and messages from other bots
58+
if (!message.content.startsWith(BOT_PREFIX) || message.author.bot) return;
59+
// obtain command and args from the command message
60+
const { command, args } = parseCommand(message);
61+
if (!command) return;
62+
63+
try {
64+
await handleCommand(message, command, args);
65+
} catch (e) {
66+
// log error
67+
logger.error({
68+
event: 'error',
69+
messageId: message.id,
70+
command: command,
71+
args: args,
72+
error: e
73+
});
74+
}
75+
};
76+
77+
export const startBot = async () => {
78+
client.once('ready', async () => {
79+
// log bot init event and send system notification
80+
logger.info({
81+
event: 'init'
82+
});
83+
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel;
84+
notif.send('Codey is up!');
85+
});
86+
87+
client.on('message', handleMessage);
88+
89+
client.login(BOT_TOKEN);
90+
};

components/db.ts renamed to src/components/db.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import sqlite3 = require('sqlite3')
2-
import { open, Database } from 'sqlite'
3-
import Discord from 'discord.js'
1+
import sqlite3 = require('sqlite3');
2+
import { open, Database } from 'sqlite';
3+
import Discord from 'discord.js';
44

5-
let db : Database | null = null;
5+
let db: Database | null = null;
66

7-
export async function openDB () {
8-
if(db == null){
7+
export async function openDB() {
8+
if (db == null) {
99
db = await open({
10-
filename: './db/bot.db',
10+
filename: 'db/bot.db',
1111
driver: sqlite3.Database
12-
})
13-
await db.run('CREATE TABLE IF NOT EXISTS saved_data (msg_id INTEGER PRIMARY KEY,data TEXT NOT NULL);')
12+
});
13+
await db.run('CREATE TABLE IF NOT EXISTS saved_data (msg_id INTEGER PRIMARY KEY,data TEXT NOT NULL);');
1414
}
1515
return db;
1616
}
1717

18-
export async function testDb(message: Discord.Message, command: string, args: string[]){
19-
switch(command){
18+
export async function testDb(message: Discord.Message, command: string, args: string[]) {
19+
switch (command) {
2020
case 'save':
2121
if (args.length < 1) {
2222
await message.channel.send('no args');
@@ -35,7 +35,7 @@ export async function testDb(message: Discord.Message, command: string, args: st
3535
.setTitle('Database Dump')
3636
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
3737
const res = await db.all('SELECT * FROM saved_data');
38-
for(const rows of res){
38+
for (const rows of res) {
3939
console.log(rows['msg_id'], rows['data']);
4040
outEmbed = outEmbed.addField(rows['msg_id'], rows['data'], true);
4141
console.log(outEmbed);
@@ -65,4 +65,4 @@ export async function testDb(message: Discord.Message, command: string, args: st
6565
}
6666
}
6767

68-
console.log('connected to db')
68+
console.log('connected to db');

0 commit comments

Comments
 (0)