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:
parent
f50e1ba47a
commit
cf38d5b181
|
@ -0,0 +1 @@
|
|||
node_modules
|
22
.eslintrc
22
.eslintrc
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/node_modules
|
||||
.env
|
||||
/logs
|
||||
/dist
|
||||
/db
|
||||
/node_modules
|
||||
.env
|
||||
/logs/*
|
||||
/dist
|
||||
/db/*
|
||||
|
|
12
.prettierrc
12
.prettierrc
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
## 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`
|
||||
|
|
|
@ -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';
|
||||
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();
|
||||
|
|
14
package.json
14
package.json
|
@ -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": "",
|
||||
|
|
|
@ -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 { 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');
|
|
@ -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;
|
|
@ -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"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue