
16 changed files with 1302 additions and 300 deletions
-
35.eslintrc
-
102discord-bot-core/Client.js
-
15discord-bot-core/Command.js
-
53discord-bot-core/HandleGuildMessage.js
-
74discord-bot-core/Util.js
-
6discord-bot-core/base-embedded-data.js
-
8discord-bot-core/base-guild-data.js
-
102discord-bot-core/client.js
-
15discord-bot-core/command.js
-
53discord-bot-core/handle-guild-message.js
-
18discord-bot-core/index.js
-
38discord-bot-core/internal-config.json
-
26discord-bot-core/monitor.js
-
74discord-bot-core/util.js
-
982package-lock.json
-
1package.json
@ -0,0 +1,35 @@ |
|||
{ |
|||
"env": { |
|||
"browser": true, |
|||
"node": true, |
|||
"commonjs": true, |
|||
"es6": true |
|||
}, |
|||
"extends": "eslint:recommended", |
|||
"parserOptions": { |
|||
"sourceType": "module" |
|||
}, |
|||
"rules": { |
|||
"indent": [ |
|||
"warn", |
|||
4, |
|||
{ |
|||
"SwitchCase": 1 |
|||
} |
|||
], |
|||
"quotes": [ |
|||
"warn", |
|||
"double" |
|||
], |
|||
"semi": [ |
|||
"error", |
|||
"always" |
|||
], |
|||
"no-undef": "error", |
|||
"no-unused-vars": "warn", |
|||
"eqeqeq": [ |
|||
"error", |
|||
"always" |
|||
] |
|||
} |
|||
} |
@ -1,102 +0,0 @@ |
|||
const CoreUtil = require("./Util.js"); |
|||
const Camo = require("camo"); |
|||
const CronJob = require("cron").CronJob; |
|||
const Discord = require("discord.js"); |
|||
const HandleGuildMessage = require("./HandleGuildMessage"); |
|||
const InternalConfig = require("./internal-config.json"); |
|||
const RequireAll = require("require-all"); |
|||
const Util = require("./Util.js"); |
|||
|
|||
let neDB; |
|||
|
|||
module.exports = class Client extends Discord.Client { |
|||
/** |
|||
* Construct a new Discord.Client with some added functionality |
|||
* @param {string} token bot token |
|||
* @param {string} commandsDir location of dir containing commands .js files |
|||
* @param {*} guildDataModel GuildData model to be used for app; must extend BaseGuildData |
|||
*/ |
|||
constructor(token, commandsDir, guildDataModel) { |
|||
super({ |
|||
messageCacheMaxSize: 16, |
|||
disabledEvents: InternalConfig.disabledEvents |
|||
}); |
|||
|
|||
this._token = token; |
|||
this.commandsDir = commandsDir; |
|||
this.guildDataModel = guildDataModel; |
|||
|
|||
this.commands = RequireAll(this.commandsDir); |
|||
|
|||
this.on("ready", this._onReady); |
|||
this.on("message", this._onMessage); |
|||
this.on("debug", this._onDebug); |
|||
this.on("guildCreate", this._onGuildCreate); |
|||
this.on("guildDelete", this._onGuildDelete); |
|||
process.on("uncaughtException", err => this._onUnhandledException(this, err)); |
|||
} |
|||
|
|||
_onReady() { |
|||
this.user.setGame(InternalConfig.website.replace(/^https?:\/\//, "")); |
|||
CoreUtil.dateLog(`Registered bot ${this.user.username}`); |
|||
|
|||
this.removeDeletedGuilds(); |
|||
} |
|||
|
|||
_onMessage(message) { |
|||
if (message.channel.type === "text" && message.member) |
|||
HandleGuildMessage(this, message, this.commands); |
|||
} |
|||
|
|||
_onDebug(info) { |
|||
info = info.replace(/Authenticated using token [^ ]+/, "Authenticated using token [redacted]"); |
|||
if (!InternalConfig.debugIgnores.some(x => info.startsWith(x))) |
|||
CoreUtil.dateDebug(info); |
|||
} |
|||
|
|||
_onGuildCreate(guild) { |
|||
CoreUtil.dateLog(`Added to guild ${guild.name}`); |
|||
} |
|||
|
|||
_onGuildDelete(guild) { |
|||
this.guildDataModel.findOneAndDelete({ guildID: guild.id }); |
|||
|
|||
CoreUtil.dateLog(`Removed from guild ${guild.name}, removing data for this guild`); |
|||
} |
|||
|
|||
_onUnhandledException(client, err) { |
|||
CoreUtil.dateError("Unhandled exception!\n", err); |
|||
CoreUtil.dateLog("Destroying existing client..."); |
|||
client.destroy().then(() => { |
|||
CoreUtil.dateLog("Client destroyed, recreating..."); |
|||
setTimeout(() => client.login(client._token), InternalConfig.reconnectTimeout); |
|||
}); |
|||
} |
|||
|
|||
bootstrap() { |
|||
Camo.connect("nedb://guilds-data").then(db => { |
|||
neDB = db; |
|||
new CronJob(InternalConfig.dbCompactionSchedule, compactCollections, null, true); |
|||
|
|||
this.emit("beforeLogin"); |
|||
this.login(this._token); |
|||
}); |
|||
} |
|||
|
|||
removeDeletedGuilds() { |
|||
this.guildDataModel.find().then(guildDatas => { |
|||
for (let guildData of guildDatas) |
|||
if (!this.guilds.get(guildData.guildID)) |
|||
guildData.delete(); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
function compactCollections() { |
|||
/*I realise it is a bit of a cheat to just access _collections in this manner, but in the absence of |
|||
camo actually having any kind of solution for this it's the easiest method I could come up with. |
|||
Maybe at some point in future I should fork camo and add this feature. The compaction function is NeDB only |
|||
and camo is designed to work with both NeDB and MongoDB, which is presumably why it doesn't alraedy exist */ |
|||
for (let collectionName of Object.keys(neDB._collections)) |
|||
neDB._collections[collectionName].persistence.compactDatafile(); |
|||
} |
@ -1,15 +0,0 @@ |
|||
module.exports = class Command { |
|||
constructor({ name, description, syntax, admin, invoke }) { |
|||
this.name = name; |
|||
this.description = description; |
|||
this.syntax = syntax; |
|||
this.admin = admin; |
|||
this.invoke = invoke; |
|||
|
|||
const params = this.syntax.split(/ +/); |
|||
const optionalParams = params.filter(x => x.match(/^\[.+\]$/)); |
|||
|
|||
this.maxParamCount = params.length - 1; |
|||
this.expectedParamCount = this.maxParamCount - optionalParams.length; |
|||
} |
|||
}; |
@ -1,53 +0,0 @@ |
|||
const RequireAll = require("require-all"); |
|||
|
|||
const internalCommands = RequireAll(__dirname + "/core-commands"); |
|||
|
|||
function handleGuildMessage(client, message, commands) { |
|||
if (isCommand(message)) |
|||
client.guildDataModel.findOne({ guildID: message.guild.id }) |
|||
.then(guildData => |
|||
handleGuildCommand( |
|||
client, |
|||
message, |
|||
Object.assign({}, internalCommands, commands), |
|||
guildData || client.guildDataModel.create({ guildID: message.guild.id }) |
|||
)); |
|||
} |
|||
|
|||
function handleGuildCommand(client, message, commands, guildData) { |
|||
const { botName, isMemberAdmin, params, command } = parseDetails(message, commands); |
|||
|
|||
if (!command) |
|||
return; |
|||
|
|||
if (params.length < command.expectedParamCount) |
|||
message.reply(`Incorrect syntax!\n**Expected:** *${botName} ${command.syntax}*\n**Need help?** *${botName} help*`); |
|||
|
|||
else if (isMemberAdmin || !command.admin) |
|||
command.invoke({ message, params, guildData, client, commands, isMemberAdmin }) |
|||
.then(response => { |
|||
guildData.save(); |
|||
if (response) message.reply(response); |
|||
}) |
|||
.catch(err => err && message.reply(err)); |
|||
} |
|||
|
|||
function parseDetails(message, commands) { |
|||
const split = message.content.split(/ +/); |
|||
const commandName = Object.keys(commands).find(x => |
|||
/**/ commands[x].name.toLowerCase() === (split[1] || "").toLowerCase()); |
|||
|
|||
return { |
|||
botName: "@" + (message.guild.me.nickname || message.guild.me.user.username), |
|||
isMemberAdmin: message.member.permissions.has("ADMINISTRATOR"), |
|||
params: split.slice(2, split.length), |
|||
command: commands[commandName] |
|||
}; |
|||
} |
|||
|
|||
function isCommand(message) { |
|||
//criteria for a command is bot being mentioned
|
|||
return new RegExp(`^<@!?${/[0-9]{18}/.exec(message.guild.me.toString())[0]}>`).exec(message.content); |
|||
} |
|||
|
|||
module.exports = handleGuildMessage; |
@ -1,74 +0,0 @@ |
|||
// @ts-ignore
|
|||
const InternalConfig = require("./internal-config.json"); |
|||
const Console = require("console"); |
|||
const SimpleFileWriter = require("simple-file-writer"); |
|||
|
|||
const logWriter = new SimpleFileWriter("./console.log"); |
|||
const debugLogWriter = new SimpleFileWriter("./debug.log"); |
|||
|
|||
function ask(client, textChannel, member, question) { |
|||
//return a promise which will resolve once the user next sends a message in this textChannel
|
|||
return new Promise((resolve, reject) => { |
|||
const cancelAsk = () => { |
|||
client.removeListener("message", handler); |
|||
textChannel.send("Response to question timed out"); |
|||
}; |
|||
|
|||
const askTimeout = setTimeout(cancelAsk, InternalConfig.askTimeout); |
|||
|
|||
const handler = responseMessage => { |
|||
if (responseMessage.channel.id === textChannel.id && responseMessage.member && responseMessage.member.id === member.id) { |
|||
clearTimeout(askTimeout); |
|||
resolve(responseMessage); |
|||
} |
|||
}; |
|||
|
|||
client.on("message", handler); |
|||
|
|||
textChannel.send(member.toString() + " " + question).catch(reject); |
|||
}); |
|||
} |
|||
|
|||
function dateLog(...args) { |
|||
doDateLog(Console.log, logWriter, args, "INFO"); |
|||
} |
|||
|
|||
function dateError(...args) { |
|||
doDateLog(Console.error, logWriter, args, "ERROR"); |
|||
} |
|||
|
|||
function dateDebugError(...args) { |
|||
doDateLog(null, null, args, "DEBUG ERROR"); |
|||
} |
|||
|
|||
function dateDebug(...args) { |
|||
doDateLog(null, null, args, "DEBUG"); |
|||
} |
|||
|
|||
function doDateLog(consoleMethod, fileWriter, args, prefix = "") { |
|||
args = formatArgs([`[${prefix}]`].concat(args)); |
|||
|
|||
if (consoleMethod !== null) |
|||
consoleMethod.apply(this, args); |
|||
|
|||
if (fileWriter !== null) |
|||
fileWriter.write(formatArgsForFile(args)); |
|||
|
|||
debugLogWriter.write(formatArgsForFile(args)); |
|||
} |
|||
|
|||
function formatArgs(args) { |
|||
return [`[${new Date().toUTCString()}]`].concat(args); |
|||
} |
|||
|
|||
function formatArgsForFile(args) { |
|||
return args.join(" ") + "\n"; |
|||
} |
|||
|
|||
module.exports = { |
|||
dateError, |
|||
dateLog, |
|||
dateDebug, |
|||
dateDebugError, |
|||
ask |
|||
}; |
@ -1,9 +1,9 @@ |
|||
const Camo = require("camo"); |
|||
|
|||
module.exports = class BaseGuildData extends Camo.Document { |
|||
constructor() { |
|||
super(); |
|||
constructor() { |
|||
super(); |
|||
|
|||
this.guildID = String; |
|||
} |
|||
this.guildID = String; |
|||
} |
|||
}; |
@ -0,0 +1,102 @@ |
|||
const CoreUtil = require("./util.js"); |
|||
const Camo = require("camo"); |
|||
const CronJob = require("cron").CronJob; |
|||
const Discord = require("discord.js"); |
|||
const HandleGuildMessage = require("./handle-guild-message.js"); |
|||
// @ts-ignore
|
|||
const InternalConfig = require("./internal-config.json"); |
|||
const RequireAll = require("require-all"); |
|||
|
|||
let neDB; |
|||
|
|||
module.exports = class Client extends Discord.Client { |
|||
/** |
|||
* Construct a new Discord.Client with some added functionality |
|||
* @param {string} token bot token |
|||
* @param {string} commandsDir location of dir containing commands .js files |
|||
* @param {*} guildDataModel GuildData model to be used for app; must extend BaseGuildData |
|||
*/ |
|||
constructor(token, commandsDir, guildDataModel) { |
|||
super({ |
|||
messageCacheMaxSize: 16, |
|||
disabledEvents: InternalConfig.disabledEvents |
|||
}); |
|||
|
|||
this._token = token; |
|||
this.commandsDir = commandsDir; |
|||
this.guildDataModel = guildDataModel; |
|||
|
|||
this.commands = RequireAll(this.commandsDir); |
|||
|
|||
this.on("ready", this._onReady); |
|||
this.on("message", this._onMessage); |
|||
this.on("debug", this._onDebug); |
|||
this.on("guildCreate", this._onGuildCreate); |
|||
this.on("guildDelete", this._onGuildDelete); |
|||
process.on("uncaughtException", err => this._onUnhandledException(this, err)); |
|||
} |
|||
|
|||
_onReady() { |
|||
this.user.setGame(InternalConfig.website.replace(/^https?:\/\//, "")); |
|||
CoreUtil.dateLog(`Registered bot ${this.user.username}`); |
|||
|
|||
this.removeDeletedGuilds(); |
|||
} |
|||
|
|||
_onMessage(message) { |
|||
if (message.channel.type === "text" && message.member) |
|||
HandleGuildMessage(this, message, this.commands); |
|||
} |
|||
|
|||
_onDebug(info) { |
|||
info = info.replace(/Authenticated using token [^ ]+/, "Authenticated using token [redacted]"); |
|||
if (!InternalConfig.debugIgnores.some(x => info.startsWith(x))) |
|||
CoreUtil.dateDebug(info); |
|||
} |
|||
|
|||
_onGuildCreate(guild) { |
|||
CoreUtil.dateLog(`Added to guild ${guild.name}`); |
|||
} |
|||
|
|||
_onGuildDelete(guild) { |
|||
this.guildDataModel.findOneAndDelete({ guildID: guild.id }); |
|||
|
|||
CoreUtil.dateLog(`Removed from guild ${guild.name}, removing data for this guild`); |
|||
} |
|||
|
|||
_onUnhandledException(client, err) { |
|||
CoreUtil.dateError("Unhandled exception!\n", err); |
|||
CoreUtil.dateLog("Destroying existing client..."); |
|||
client.destroy().then(() => { |
|||
CoreUtil.dateLog("Client destroyed, recreating..."); |
|||
setTimeout(() => client.login(client._token), InternalConfig.reconnectTimeout); |
|||
}); |
|||
} |
|||
|
|||
bootstrap() { |
|||
Camo.connect("nedb://guilds-data").then(db => { |
|||
neDB = db; |
|||
new CronJob(InternalConfig.dbCompactionSchedule, compactCollections, null, true); |
|||
|
|||
this.emit("beforeLogin"); |
|||
this.login(this._token); |
|||
}); |
|||
} |
|||
|
|||
removeDeletedGuilds() { |
|||
this.guildDataModel.find().then(guildDatas => { |
|||
for (let guildData of guildDatas) |
|||
if (!this.guilds.get(guildData.guildID)) |
|||
guildData.delete(); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
function compactCollections() { |
|||
/*I realise it is a bit of a cheat to just access _collections in this manner, but in the absence of |
|||
camo actually having any kind of solution for this it's the easiest method I could come up with. |
|||
Maybe at some point in future I should fork camo and add this feature. The compaction function is NeDB only |
|||
and camo is designed to work with both NeDB and MongoDB, which is presumably why it doesn't alraedy exist */ |
|||
for (let collectionName of Object.keys(neDB._collections)) |
|||
neDB._collections[collectionName].persistence.compactDatafile(); |
|||
} |
@ -0,0 +1,15 @@ |
|||
module.exports = class Command { |
|||
constructor({ name, description, syntax, admin, invoke }) { |
|||
this.name = name; |
|||
this.description = description; |
|||
this.syntax = syntax; |
|||
this.admin = admin; |
|||
this.invoke = invoke; |
|||
|
|||
const params = this.syntax.split(/ +/); |
|||
const optionalParams = params.filter(x => x.match(/^\[.+\]$/)); |
|||
|
|||
this.maxParamCount = params.length - 1; |
|||
this.expectedParamCount = this.maxParamCount - optionalParams.length; |
|||
} |
|||
}; |
@ -0,0 +1,53 @@ |
|||
const RequireAll = require("require-all"); |
|||
|
|||
const internalCommands = RequireAll(__dirname + "/core-commands"); |
|||
|
|||
function handleGuildMessage(client, message, commands) { |
|||
if (isCommand(message)) |
|||
client.guildDataModel.findOne({ guildID: message.guild.id }) |
|||
.then(guildData => |
|||
handleGuildCommand( |
|||
client, |
|||
message, |
|||
Object.assign({}, internalCommands, commands), |
|||
guildData || client.guildDataModel.create({ guildID: message.guild.id }) |
|||
)); |
|||
} |
|||
|
|||
function handleGuildCommand(client, message, commands, guildData) { |
|||
const { botName, isMemberAdmin, params, command } = parseDetails(message, commands); |
|||
|
|||
if (!command) |
|||
return; |
|||
|
|||
if (params.length < command.expectedParamCount) |
|||
message.reply(`Incorrect syntax!\n**Expected:** *${botName} ${command.syntax}*\n**Need help?** *${botName} help*`); |
|||
|
|||
else if (isMemberAdmin || !command.admin) |
|||
command.invoke({ message, params, guildData, client, commands, isMemberAdmin }) |
|||
.then(response => { |
|||
guildData.save(); |
|||
if (response) message.reply(response); |
|||
}) |
|||
.catch(err => err && message.reply(err)); |
|||
} |
|||
|
|||
function parseDetails(message, commands) { |
|||
const split = message.content.split(/ +/); |
|||
const commandName = Object.keys(commands).find(x => |
|||
/**/ commands[x].name.toLowerCase() === (split[1] || "").toLowerCase()); |
|||
|
|||
return { |
|||
botName: "@" + (message.guild.me.nickname || message.guild.me.user.username), |
|||
isMemberAdmin: message.member.permissions.has("ADMINISTRATOR"), |
|||
params: split.slice(2, split.length), |
|||
command: commands[commandName] |
|||
}; |
|||
} |
|||
|
|||
function isCommand(message) { |
|||
//criteria for a command is bot being mentioned
|
|||
return new RegExp(`^<@!?${/[0-9]{18}/.exec(message.guild.me.toString())[0]}>`).exec(message.content); |
|||
} |
|||
|
|||
module.exports = handleGuildMessage; |
@ -1,21 +1,21 @@ |
|||
{ |
|||
"dbCompactionSchedule": "0 * * * * *", |
|||
"restartSchedule": "0 0 0 * * *", |
|||
"restartTimeout": 5000, |
|||
"website": "https://benji7425.github.io", |
|||
"discordInvite": "https://discord.gg/SSkbwSJ", |
|||
"debugIgnores": [ |
|||
"[ws] [connection] Sending a heartbeat", |
|||
"[ws] [connection] Heartbeat acknowledged" |
|||
], |
|||
"disabledEvents": [ |
|||
"CHANNEL_PINS_UPDATE", |
|||
"GUILD_BAN_ADD", |
|||
"GUILD_BAN_REMOVE", |
|||
"PRESENCE_UPDATE", |
|||
"TYPING_START", |
|||
"USER_NOTE_UPDATE", |
|||
"USER_SETTINGS_UPDATE" |
|||
], |
|||
"askTimeout": 60000 |
|||
"dbCompactionSchedule": "0 * * * * *", |
|||
"restartSchedule": "0 0 0 * * *", |
|||
"restartTimeout": 5000, |
|||
"website": "https://benji7425.github.io", |
|||
"discordInvite": "https://discord.gg/SSkbwSJ", |
|||
"debugIgnores": [ |
|||
"[ws] [connection] Sending a heartbeat", |
|||
"[ws] [connection] Heartbeat acknowledged" |
|||
], |
|||
"disabledEvents": [ |
|||
"CHANNEL_PINS_UPDATE", |
|||
"GUILD_BAN_ADD", |
|||
"GUILD_BAN_REMOVE", |
|||
"PRESENCE_UPDATE", |
|||
"TYPING_START", |
|||
"USER_NOTE_UPDATE", |
|||
"USER_SETTINGS_UPDATE" |
|||
], |
|||
"askTimeout": 60000 |
|||
} |
@ -0,0 +1,74 @@ |
|||
// @ts-ignore
|
|||
const InternalConfig = require("./internal-config.json"); |
|||
const Console = require("console"); |
|||
const SimpleFileWriter = require("simple-file-writer"); |
|||
|
|||
const logWriter = new SimpleFileWriter("./console.log"); |
|||
const debugLogWriter = new SimpleFileWriter("./debug.log"); |
|||
|
|||
function ask(client, textChannel, member, question) { |
|||
//return a promise which will resolve once the user next sends a message in this textChannel
|
|||
return new Promise((resolve, reject) => { |
|||
const cancelAsk = () => { |
|||
client.removeListener("message", handler); |
|||
textChannel.send("Response to question timed out"); |
|||
}; |
|||
|
|||
const askTimeout = setTimeout(cancelAsk, InternalConfig.askTimeout); |
|||
|
|||
const handler = responseMessage => { |
|||
if (responseMessage.channel.id === textChannel.id && responseMessage.member && responseMessage.member.id === member.id) { |
|||
clearTimeout(askTimeout); |
|||
resolve(responseMessage); |
|||
} |
|||
}; |
|||
|
|||
client.on("message", handler); |
|||
|
|||
textChannel.send(member.toString() + " " + question).catch(reject); |
|||
}); |
|||
} |
|||
|
|||
function dateLog(...args) { |
|||
doDateLog(Console.log, logWriter, args, "INFO"); |
|||
} |
|||
|
|||
function dateError(...args) { |
|||
doDateLog(Console.error, logWriter, args, "ERROR"); |
|||
} |
|||
|
|||
function dateDebugError(...args) { |
|||
doDateLog(null, null, args, "DEBUG ERROR"); |
|||
} |
|||
|
|||
function dateDebug(...args) { |
|||
doDateLog(null, null, args, "DEBUG"); |
|||
} |
|||
|
|||
function doDateLog(consoleMethod, fileWriter, args, prefix = "") { |
|||
args = formatArgs([`[${prefix}]`].concat(args)); |
|||
|
|||
if (consoleMethod !== null) |
|||
consoleMethod.apply(this, args); |
|||
|
|||
if (fileWriter !== null) |
|||
fileWriter.write(formatArgsForFile(args)); |
|||
|
|||
debugLogWriter.write(formatArgsForFile(args)); |
|||
} |
|||
|
|||
function formatArgs(args) { |
|||
return [`[${new Date().toUTCString()}]`].concat(args); |
|||
} |
|||
|
|||
function formatArgsForFile(args) { |
|||
return args.join(" ") + "\n"; |
|||
} |
|||
|
|||
module.exports = { |
|||
dateError, |
|||
dateLog, |
|||
dateDebug, |
|||
dateDebugError, |
|||
ask |
|||
}; |
982
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
issues.context.reference_issue