Update naming and formatting for core .js files

This commit is contained in:
benji7425 2018-01-25 21:41:17 +00:00
parent 1d5ba2561b
commit 1a60292860
16 changed files with 1302 additions and 300 deletions

35
.eslintrc Normal file
View File

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

View File

@ -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();
}

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ const Camo = require("camo");
// @ts-ignore
module.exports = class BaseEmbeddedData extends Camo.EmbeddedDocument {
constructor() {
super();
}
constructor() {
super();
}
};

View File

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

102
discord-bot-core/client.js Normal file
View File

@ -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();
}

View File

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

View File

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

View File

@ -2,13 +2,13 @@
const InternalConfig = require("./internal-config.json");
module.exports = {
Client: require("./Client.js"),
BaseGuildData: require("./BaseGuildData.js"),
BaseEmbeddedData: require("./BaseEmbeddedData.js"),
Command: require("./Command.js"),
util: require("./Util.js"),
details: {
website: InternalConfig.website,
discordInvite: InternalConfig.discordInvite
}
Client: require("./client.js"),
BaseGuildData: require("./base-guild-data.js"),
BaseEmbeddedData: require("./base-embedded-data.js"),
Command: require("./command.js"),
util: require("./util.js"),
details: {
website: InternalConfig.website,
discordInvite: InternalConfig.discordInvite
}
};

View File

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

View File

@ -11,23 +11,23 @@ restart();
new CronJob(InternalConfig.restartSchedule, restart, null, true);
function restart() {
ensureKilledInstance()
.then(bootstrapNewInstance)
.catch(DiscordUtil.dateError);
ensureKilledInstance()
.then(bootstrapNewInstance)
.catch(DiscordUtil.dateError);
}
function bootstrapNewInstance() {
instance = fork(process.argv[2]);
instance = fork(process.argv[2]);
}
function ensureKilledInstance() {
return new Promise((resolve, reject) => {
if (instance) {
instance.kill();
DiscordUtil.dateLog(`Killed existing instance for scheduled restart in ${InternalConfig.restartTimeout / 1000} sec`);
setTimeout(resolve, InternalConfig.restartTimeout);
}
else
resolve();
});
return new Promise((resolve, reject) => {
if (instance) {
instance.kill();
DiscordUtil.dateLog(`Killed existing instance for scheduled restart in ${InternalConfig.restartTimeout / 1000} sec`);
setTimeout(resolve, InternalConfig.restartTimeout);
}
else
resolve();
});
}

74
discord-bot-core/util.js Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"@types/node": "9.3.0",
"camo": "git+https://github.com/scottwrobinson/camo.git",
"discord.js": "11.2.0",
"eslint": "4.16.0",
"get-urls": "7.0.0",
"jsonfile": "3.0.1",
"rss-parser": "2.12.0",