commit
c3b9d2d2dd
@ -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,3 +1,5 @@ |
||||
/node_modules |
||||
.env |
||||
/logs |
||||
/dist |
||||
/db/*.db |
@ -1,6 +1,6 @@ |
||||
{ |
||||
"semi": true, |
||||
"trailingComma": "none", |
||||
"singleQuote": true, |
||||
"printWidth": 120 |
||||
} |
||||
"semi": true, |
||||
"trailingComma": "none", |
||||
"singleQuote": true, |
||||
"printWidth": 120 |
||||
} |
||||
|
@ -1,10 +1,12 @@ |
||||
# 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. |
||||
|
@ -1,8 +1,11 @@ |
||||
import sqlite3 = require('sqlite3') |
||||
export const db = new sqlite3.Database('./db/bot.db', (err) =>{ |
||||
if(err){ |
||||
console.log(err.message) |
||||
} |
||||
}); |
||||
db.run('CREATE TABLE IF NOT EXISTS saved_data (msg_id INTEGER PRIMARY KEY,data TEXT NOT NULL);') |
||||
import { open } from 'sqlite' |
||||
|
||||
|
||||
export async function openDb () { |
||||
return open({ |
||||
filename: './db/bot.db', |
||||
driver: sqlite3.Database |
||||
}) |
||||
} |
||||
console.log('connected to db') |
||||
|
@ -1,114 +1,136 @@ |
||||
import dotenv = require('dotenv') |
||||
dotenv.config() |
||||
import dotenv from 'dotenv'; |
||||
dotenv.config(); |
||||
import { Database } from 'sqlite' |
||||
|
||||
import Discord = require('discord.js') |
||||
import _ = require('lodash') |
||||
import { db } from './components/db' |
||||
import Discord from 'discord.js'; |
||||
import _ from 'lodash'; |
||||
import { openDb } 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 NOTIF_CHANNEL_ID: string = process.env.NOTIF_CHANNEL_ID || '.'; |
||||
const BOT_TOKEN: string = process.env.BOT_TOKEN || '.'; |
||||
let db : Database; |
||||
const BOT_PREFIX = '.'; |
||||
const client = new Discord.Client(); |
||||
|
||||
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 parseCommand = message => { |
||||
// 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 |
||||
}) |
||||
const firstArg = args.shift() |
||||
if (!firstArg) return |
||||
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 |
||||
}); |
||||
|
||||
const handleCommand = async (message, command, args) => { |
||||
switch(command) { |
||||
case 'ping': |
||||
await message.channel.send('pong') |
||||
break |
||||
switch (command) { |
||||
case 'ping': |
||||
await message.channel.send('pong'); |
||||
|
||||
//dev testing commands
|
||||
case 'save': |
||||
if(args.length<1){ |
||||
await message.channel.send('no args') |
||||
return |
||||
}
|
||||
db.run('INSERT INTO saved_data (msg_id,data)' + |
||||
'VALUES(?,?)', [message.id, args[0]], async (err) =>{ |
||||
if(err){ |
||||
await message.channel.send(err) |
||||
return |
||||
} |
||||
await message.channel.send('saved "'+args[0]+'" with id '+message.id+'.') |
||||
}) |
||||
break |
||||
case 'dump': |
||||
//TODO: make messages be embeds to test
|
||||
let flag : boolean = true |
||||
let outEmbed = new Discord.MessageEmbed().setColor('#0099ff').setTitle('Database Dump').setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ').addField("test", "test", true) |
||||
await db.each('SELECT * FROM saved_data', (err, rows) =>{ |
||||
if(err){ |
||||
flag = false; |
||||
return; |
||||
} |
||||
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.values.length == 0){ |
||||
await message.channel.send("empty") |
||||
} |
||||
else{ |
||||
await message.channel.send(outEmbed) |
||||
} |
||||
} |
||||
else{ |
||||
await message.channel.send("error") |
||||
} |
||||
break; |
||||
case 'clear': |
||||
db.run('DELETE FROM saved_data', async (err) =>{ |
||||
if(err){ |
||||
await message.channel.send(err) |
||||
return |
||||
} |
||||
await message.channel.send("Cleared.") |
||||
|
||||
}) |
||||
} |
||||
} |
||||
//dev testing commands
|
||||
case 'save': |
||||
if (args.length < 1) { |
||||
await message.channel.send('no args') |
||||
return |
||||
} |
||||
await db.run('INSERT INTO saved_data (msg_id,data)' + |
||||
'VALUES(?,?)', [message.id, args[0]]).then((stmt : any, lastID : any, changes : any) => { |
||||
consoel |
||||
}) |
||||
break |
||||
case 'dump': |
||||
//TODO: make messages be embeds to test
|
||||
let flag: boolean = true |
||||
let outEmbed = new Discord.MessageEmbed().setColor('#0099ff').setTitle('Database Dump').setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ').addField("test", "test", true) |
||||
db.each('SELECT * FROM saved_data', (err, rows) => { |
||||
if (err) { |
||||
flag = false; |
||||
return; |
||||
} |
||||
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.values.length == 0) { |
||||
await message.channel.send("empty") |
||||
} |
||||
else { |
||||
await message.channel.send(outEmbed) |
||||
} |
||||
} |
||||
else { |
||||
await message.channel.send("error") |
||||
} |
||||
break; |
||||
case 'clear': |
||||
db.run('DELETE FROM saved_data', async (err : any) => { |
||||
if (err) { |
||||
await message.channel.send(err.message) |
||||
return |
||||
} |
||||
await message.channel.send("Cleared.") |
||||
|
||||
const handleMessage = async 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) |
||||
// TODO: log commands
|
||||
}) |
||||
} |
||||
}; |
||||
|
||||
try { |
||||
await handleCommand(message, command, args) |
||||
} catch(e) { |
||||
// TODO: handle error
|
||||
} |
||||
} |
||||
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; |
||||
|
||||
const startBot = async () => { |
||||
client.once('ready', async () => { |
||||
const notif = await client.channels.fetch(NOTIF_CHANNEL_ID) as Discord.TextChannel |
||||
notif.send('Codey is up!') |
||||
}) |
||||
try { |
||||
await handleCommand(message, command, args); |
||||
} catch (e) { |
||||
// log error
|
||||
logger.error({ |
||||
event: 'error', |
||||
messageId: message.id, |
||||
command: command, |
||||
args: args, |
||||
error: e |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
client.on('message', handleMessage) |
||||
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!'); |
||||
}); |
||||
|
||||
db = await openDb(); |
||||
console.log(db) |
||||
client.on('message', handleMessage); |
||||
|
||||
client.login(BOT_TOKEN) |
||||
} |
||||
client.login(BOT_TOKEN); |
||||
}; |
||||
|
||||
startBot() |
||||
startBot(); |
||||
|
@ -0,0 +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; |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"extends": "@tsconfig/node14/tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "dist", |
||||
"preserveConstEnums": true, |
||||
"esModuleInterop": true |
||||
}, |
||||
"include": ["**/*.ts"] |
||||
} |
Loading…
Reference in new issue