Browse Source

Update naming and formatting for core .js files

master
benji7425 2 years ago
parent
commit
c4d1247be3
16 changed files with 1302 additions and 300 deletions
  1. +35
    -0
      .eslintrc
  2. +0
    -102
      discord-bot-core/Client.js
  3. +0
    -15
      discord-bot-core/Command.js
  4. +0
    -53
      discord-bot-core/HandleGuildMessage.js
  5. +0
    -74
      discord-bot-core/Util.js
  6. +3
    -3
      discord-bot-core/base-embedded-data.js
  7. +4
    -4
      discord-bot-core/base-guild-data.js
  8. +102
    -0
      discord-bot-core/client.js
  9. +15
    -0
      discord-bot-core/command.js
  10. +53
    -0
      discord-bot-core/handle-guild-message.js
  11. +9
    -9
      discord-bot-core/index.js
  12. +19
    -19
      discord-bot-core/internal-config.json
  13. +13
    -13
      discord-bot-core/monitor.js
  14. +74
    -0
      discord-bot-core/util.js
  15. +974
    -8
      package-lock.json
  16. +1
    -0
      package.json

+ 35
- 0
.eslintrc 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"
]
}
}

+ 0
- 102
discord-bot-core/Client.js 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();
}

+ 0
- 15
discord-bot-core/Command.js 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;
}
};

+ 0
- 53
discord-bot-core/HandleGuildMessage.js 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;

+ 0
- 74
discord-bot-core/Util.js 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
};

discord-bot-core/BaseEmbeddedData.js → discord-bot-core/base-embedded-data.js View File

@@ -2,7 +2,7 @@ const Camo = require("camo");

// @ts-ignore
module.exports = class BaseEmbeddedData extends Camo.EmbeddedDocument {
constructor() {
super();
}
constructor() {
super();
}
};

discord-bot-core/BaseGuildData.js → discord-bot-core/base-guild-data.js 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
- 0
discord-bot-core/client.js 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();
}

+ 15
- 0
discord-bot-core/command.js 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;
}
};

+ 53
- 0
discord-bot-core/handle-guild-message.js 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;

+ 9
- 9
discord-bot-core/index.js 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
}
};

+ 19
- 19
discord-bot-core/internal-config.json 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
}

+ 13
- 13
discord-bot-core/monitor.js 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
- 0
discord-bot-core/util.js 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
};

+ 974
- 8
package-lock.json
File diff suppressed because it is too large
View File


+ 1
- 0
package.json 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",


Loading…
Cancel
Save