Merge branch 'Alex' into 'master'

Add Database functionality

See merge request csc/discord-bot!4
This commit is contained in:
Alex Zhang 2021-05-24 01:49:59 +00:00
commit 99d27c8da4
12 changed files with 978 additions and 353 deletions

View File

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

18
.gitattributes vendored Normal file
View File

@ -0,0 +1,18 @@
# Set the default behavior, in case people don't have core.autocrlf set.
<<<<<<< HEAD
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
=======
* text eol=crlf
>>>>>>> 543689e4ff503897e2e276ee4b29f8ee791c123a

9
.gitignore vendored
View File

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

View File

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

14
.vscode/settings.json vendored
View File

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

View File

@ -1,12 +1,12 @@
# 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 ## Running the bot locally
1. Run `yarn` to install dependencies. 1. Run `yarn` to install dependencies.
1. Add the required environment variables in a `.env` file in the root directory. 1. Add the required environment variables in a `.env` file in the root directory.
1. Run `yarn dev` to start the bot locally. 1. Run `yarn dev` to start the bot locally.

68
components/db.ts Normal file
View File

@ -0,0 +1,68 @@
import sqlite3 = require('sqlite3')
import { open, Database } from 'sqlite'
import Discord from 'discord.js'
let db : Database | null = null;
export async function openDb () {
if(db == null){
db = await open({
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);')
}
return db;
}
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');
return;
}
await openDb().then((db) => {
db.run('INSERT INTO saved_data (msg_id,data)' + 'VALUES(?,?)', [message.id, args[0]]);
});
await message.channel.send('Saved ' + args[0] + ' with id ' + message.id);
break;
case 'dump':
await openDb().then(async (db) => {
let flag: boolean = true;
let outEmbed = new Discord.MessageEmbed()
.setColor('#0099ff')
.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){
console.log(rows['msg_id'], rows['data']);
outEmbed = outEmbed.addField(rows['msg_id'], rows['data'], true);
console.log(outEmbed);
}
console.log(outEmbed);
if (flag) {
if (outEmbed.fields.length == 0) {
await message.channel.send('empty');
} else {
await message.channel.send(outEmbed);
}
} else {
await message.channel.send('error');
}
});
break;
case 'clear':
openDb()
.then((db) => {
return db.run('DELETE FROM saved_data');
})
.then(async () => {
await message.channel.send('cleared');
})
.catch();
break;
}
}
console.log('connected to db')

174
index.ts
View File

@ -1,86 +1,88 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
import Discord from 'discord.js'; import Discord from 'discord.js';
import _ from 'lodash'; import _ from 'lodash';
import { openDb, testDb } from './components/db';
import logger from './logger'; import logger from './logger';
const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.'; const NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.';
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.'; const BOT_TOKEN: string = process.env.BOT_TOKEN || '.';
const BOT_PREFIX = '.'; const BOT_PREFIX = '.';
const client = new Discord.Client();
const client = new Discord.Client();
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => {
const parseCommand = (message: Discord.Message): { command: string | null; args: string[] } => { // extract arguments by splitting by spaces and grouping strings in quotes
// extract arguments by splitting by spaces and grouping strings in quotes // e.g. .ping 1 "2 3" => ['ping', '1', '2 3']
// e.g. .ping 1 "2 3" => ['ping', '1', '2 3'] let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
let args = message.content.slice(BOT_PREFIX.length).match(/[^\s"']+|"([^"]*)"|'([^']*)'/g); args = _.map(args, (arg) => {
args = _.map(args, (arg) => { if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) {
if (arg[0].match(/'|"/g) && arg[arg.length - 1].match(/'|"/g)) { return arg.slice(1, arg.length - 1);
return arg.slice(1, arg.length - 1); }
} return arg;
return arg; });
}); // obtain the first argument after the prefix
// obtain the first argument after the prefix const firstArg = args.shift();
const firstArg = args.shift(); if (!firstArg) return { command: null, args: [] };
if (!firstArg) return { command: null, args: [] }; const command = firstArg.toLowerCase();
const command = firstArg.toLowerCase(); return { command, args };
return { command, args }; };
};
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => {
const handleCommand = async (message: Discord.Message, command: string, args: string[]) => { // log command and its author info
// log command and its author info logger.info({
logger.info({ event: 'command',
event: 'command', messageId: message.id,
messageId: message.id, author: message.author.id,
author: message.author.id, authorName: message.author.username,
authorName: message.author.username, channel: message.channel.id,
channel: message.channel.id, command,
command, args
args });
});
switch (command) {
switch (command) { case 'ping':
case 'ping': await message.channel.send('pong');
await message.channel.send('pong'); break;
}
}; //dev testing
if(process.env.NODE_ENV == "dev"){
const handleMessage = async (message: Discord.Message) => { testDb(message, command, args);
// 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; const handleMessage = async (message: Discord.Message) => {
// ignore messages without bot prefix and messages from other bots
try { if (!message.content.startsWith(BOT_PREFIX) || message.author.bot) return;
await handleCommand(message, command, args); // obtain command and args from the command message
} catch (e) { const { command, args } = parseCommand(message);
// log error if (!command) return;
logger.error({
event: 'error', try {
messageId: message.id, await handleCommand(message, command, args);
command: command, } catch (e) {
args: args, // log error
error: e logger.error({
}); event: 'error',
} messageId: message.id,
}; command: command,
args: args,
const startBot = async () => { error: e
client.once('ready', async () => { });
// log bot init event and send system notification }
logger.info({ };
event: 'init'
}); const startBot = async () => {
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel; client.once('ready', async () => {
notif.send('Codey is up!'); // log bot init event and send system notification
}); logger.info({
event: 'init'
client.on('message', handleMessage); });
const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as Discord.TextChannel;
client.login(BOT_TOKEN); notif.send('Codey is up!');
}; });
client.on('message', handleMessage);
startBot(); client.login(BOT_TOKEN);
};
startBot();

View File

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

View File

@ -18,11 +18,14 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
"sqlite": "^4.0.22",
"sqlite3": "^5.0.2",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5" "winston-daily-rotate-file": "^4.5.5"
}, },
"devDependencies": { "devDependencies": {
"@types/sqlite3": "^3.1.7",
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/node": "^15.0.1", "@types/node": "^15.0.1",

View File

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

889
yarn.lock

File diff suppressed because it is too large Load Diff