Browse Source

Sqaush merge and adapt for template update using shared core code

master
Benji 4 years ago
parent
commit
2489909da4
  1. 2
      .gitignore
  2. 1
      .npmrc
  3. 2
      .vscode/launch.json
  4. 2
      .vscode/settings.json
  5. 128
      CHANGELOG.md
  6. 55
      README.md
  7. 108
      app/bot.js
  8. 20
      app/commands.json
  9. 41
      app/config.json
  10. 204
      app/index.js
  11. 20
      app/models/guild-data.js
  12. 20
      bootstrap.js
  13. 79
      discord-bot-core/.gitignore
  14. 12
      discord-bot-core/.gitrepo
  15. 2
      discord-bot-core/.npmrc
  16. 74
      discord-bot-core/bootstrapper.js
  17. 21
      discord-bot-core/config.json
  18. 12
      discord-bot-core/index.js
  19. 99
      discord-bot-core/message-handler.js
  20. 23
      discord-bot-core/package.json
  21. 49
      discord-bot-core/util.js
  22. 3
      package.json

2
.gitignore

@ -1,5 +1,5 @@
### Discord bots ####
guilds.json
*-data.json
token.json
log

1
.npmrc

@ -1,3 +1,2 @@
save=true
save-exact=true
cache=node_cache

2
.vscode/launch.json

@ -8,7 +8,7 @@
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceRoot}/bootstrap.js"
"program": "${workspaceRoot}/app/index.js"
}
]
}

2
.vscode/settings.json

@ -4,4 +4,4 @@
"token.json": true,
"_config.yml": true
}
}
}

128
CHANGELOG.md

@ -1,129 +1,3 @@
# Changelog
## v3.0.0-b2
### Fixed
- Full and short youtube urls not being properly converted
## v3.0.0-b1
### Added
- Fancy new @bot help command
### Updated
- Significant back-end updates
- Commands now invoked with an @mention to the bot
## v2.0.0-b1
### Added
- Multi-guild support
- In-chat commands for setup and configuration
- Add a new feed
- View a list of feeds
- Remove an existing feed
### Updated
- Make save file configurable to allow use as a module with other bots
- Update config file structure
- Now uses discord.js instead of discord.io
- YouTube links automatically handled; no more separate "YouTube mode" config item
### Fixed
- Crash if trying to view feeds list before any feeds have been set up
## v1.4.0
### Added
- Support for posting links from multiple feeds
- Tagging of separate roles for each feed being checked
### Updated
- Updated bot connection code to use my discord-bot-wrapper
### Removed
- !logsplease command removed as the OTT logging was just being annoying
## v1.3.2
### Fixed
- Fixed list posting channel messages being ignored
## v1.3.1
### Fixed
- Developer commands can now be used from any channel or PM
## v1.3.0
### Added
- Deletion of "You have successfully subscribed" messages after a short delay (configurable)
- 'Developer' commands that can only be accessed by specified users
- !cacheList developer command to view the cached URLs
### Updated
- !logsplease is now a developer command
- Subscriptions are now done using a role
- !subscribe and !unsibscribe add and remove the user from the role
- !sublist command is now removed
- The role is mentioned when the link is posted, rather than a long chain of user IDs
## v1.2.1
### Fixed
- Fixed multiple users being unsubscribed when one user unsubscribes
## v1.2.0
### Added
- Chat message/command to request a list of subscribed users
- The ability for users to 'subscribe' so they are tagged whenever a new link is posted
- Logging to a file
- Ability for user to request an upload of the logs file
### Updated
- Added basic spam reduction when logging so the same message won't get logged multiple times in a row
- Refactored a bunch of code to improve efficiency
- Updated timer logic to only ever use a single timer, and share it between posting and reconnecting
## v1.1.2
### Updated
- Updated reconnect logic to hopefully be more stable
## v1.1.1
### Added
- Reconnect timer to repeatedly try reconnect at intervals
### Updated
- Updated support for https conversion to http to hopefully be more consistent
## v1.1.0
### Added
- Added togglable YouTube mode
- Converts full URLs to YouTube share URLs
- Checks against both YouTube full and share URLs to ensure same video not posted twice
- New logging class to handle logging
### Updated
- Major refactor of a significant portion of the bot's code - should be easier to maintain now, but may have introduced some new bugs
- Changed expected name for bot config file to bot-config.json rather than botConfig.json
### Fixed
- New timer being created every time the bot reconnected
## Unreleased

55
README.md

@ -1,63 +1,43 @@
# Discord rss bot
# Template README
<!--summary-->
Posts the latest URLs from an RSS feed, optionally @mention-ing a role when posted
Summary goes here
<!--/summary-->
## Features
<!--features-->
- Multiple feeds per server
- Configurable channel per feed
- Optional, configurable role per feed, mentioned when a URL is posted
- In-chat setup commands
- Detects if a user "beats me to it" and posts the URL before the feed updates (useful for slow feeds)
- Detects users posting both full and short YouTube urls if using a YouTube RSS feed
- Feature 1
- Feature 2
- Feature 3
<!--/features-->
## Invite
By inviting this bot to your server you agree to the [terms and conditions](#privacy-statement) laid out in the privacy section of this document.
If you agree, invite to your server with [this link](https://discordapp.com/oauth2/authorize?client_id=343909688045469698&scope=bot&permissions=0x00010c00).
If you agree, invite to your server with [this link](https://link-goes-here.com/).
## Setup
You can ask the bot for help with commands by typing `@RSS_Bot help`
You can ask the bot for help with commands by typing `@bot help`
### Add a new feed
### Use case 1
`@RSS_Bot add-feed <url> <#channel> [@role]`
- *url* must be an RSS feed URL
- *#channel* must be a channel mention
- *@role* must be a role mention (make sure "Anyone can mention this role" is turned on during setup)
`@bot command <params>`
Explanation
Example:
`@RSS_Bot add-feed http://lorem-rss.herokuapp.com/feed?unit=second&interval=30 #rss-posts @subscribers`
### View feeds configured for this server
`@RSS_Bot view-feeds`
This will display a list of RSS feeds configured for this server, along with a unique ID for each
### Remove a configured feed
`@RSS_Bot remove-feed <feed-id>`
To remove a feed you will need it's unique ID, which you can find by running the above *view-feeds* command
Example:
`@RSS_Bot remove-feed ABc-123dEF`
`@bot command example-param`
## Permissions
The bot requires certain permissions, which you are prompted for on the invite screen.
Each permission has a reason for being required, explained below.
| Permission | Reason |
|----------------------|-------------------------------------------------------------|
| Read messages | Detect when you use commands |
| Send messages | Respond when you use commands; post new RSS links |
| Read message history | Check if any new RSS links have been posted during downtime |
| Permission | Reason |
|---------------|-------------------------------|
| Read messages | Detect when you use commands |
| Send messages | Respond when you use commands |
## Privacy statement
@ -70,8 +50,7 @@ In accordance with the [Discord developer Terms of Service](https://discordapp.c
This bot will only collect data which is necessary to function.
No data collected will be shared with any third parties.
Should you wish for the data stored about your server to be removed, please contact me via [my support Discord](https://discordapp.com/invite/SSkbwSJ) and I will oblige as soon as I am able. Please note that this will require you to remove the bot from your server.
Should you wish for the data stored about your server to be removed, please contact me via [my support Discord server](https://discordapp.com/invite/SSkbwSJ) and I will oblige as soon as I am able. Please note that this will require you to remove the bot from your server.
## Want to host your own instance?
@ -83,4 +62,4 @@ Should you wish for the data stored about your server to be removed, please cont
## Need help?
I am available for contact via my [support Discord server](https://discordapp.com/invite/SSkbwSJ). I will always do my best to respond, however I am often busy so can't always be available right away, and as this is a free service I may not always be able to resolve your query.
I am available for contact via my [support Discord server](https://discordapp.com/invite/SSkbwSJ). I will always do my best to respond, however I am often busy so can't always be available right away, and as this is a free service I may not always be able to resolve your query.

108
app/bot.js

@ -1,108 +0,0 @@
//external lib imports
const GetUrls = require("get-urls"); //for extracting urls from messages
//my imports
const DiscordUtil = require("discordjs-util");
//app component imports
const GuildData = require("./models/guild-data.js");
const FeedData = require("./models/feed-data.js");
module.exports = {
onReady(client, guildsData, config) {
return new Promise((resolve, reject) => {
parseLinksInGuilds(client.guilds, guildsData)
.then(() => checkFeedsInGuilds(client.guilds, guildsData))
.then(() => setInterval(() => checkFeedsInGuilds(client.guilds, guildsData), config.feedCheckIntervalSec * 1000)); //set up an interval to check all the feeds
});
},
onCommand(commandObj, commandsObj, params, guildData, message, config, client, botName) {
switch (commandObj.command) {
case commandsObj.addFeed.command:
return addFeed(client, guildData, message, config.maxCacheSize);
case commandsObj.removeFeed.command:
return removeFeed(guildData, message, botName);
case commandsObj.viewFeeds.command:
return viewFeeds(guildData);
}
},
onNonCommandMsg(message, guildData) {
guildData.feeds.forEach(feedData => {
if (message.channel.name === feedData.channelName)
feedData.cachedLinks.push(...GetUrls(message.content)); //spread the urlSet returned by GetUrls into the cache array
});
}
};
function addFeed(client, guildData, message, maxCacheSize) {
return new Promise((resolve, reject) => {
const feedUrl = [...GetUrls(message.content)][0];
const channel = message.mentions.channels.first();
if (!feedUrl || !channel)
reject("Please provide both a channel and an RSS feed URL. You can optionally @mention a role also.");
const role = message.mentions.roles.first();
const feedData = new FeedData({
url: feedUrl,
channelName: channel.name,
roleName: role ? role.name : null,
maxCacheSize: maxCacheSize
});
//ask the user if they're happy with the details they set up, save if yes, don't if no
DiscordUtil.ask(client, message.channel, message.member, "Are you happy with this?\n" + feedData.toString())
.then(responseMessage => {
//if they responded yes, save the feed and let them know, else tell them to start again
if (responseMessage.content.toLowerCase() === "yes") {
if (!guildData)
guildData = new GuildData({ id: message.guild.id, feeds: [] });
guildData.feeds.push(feedData);
resolve("Your new feed has been saved!");
}
else
reject("Your feed has not been saved, please add it again with the correct details");
});
});
}
function removeFeed(guildData, message, botName) {
return new Promise((resolve, reject) => {
const parameters = message.content.split(" ");
if (parameters.length !== 3)
resolve(`Please use the command as such:\n\`\`\` ${botName} remove-feed feedid\`\`\``);
else {
const idx = guildData.feeds.findIndex(feed => feed.id === parameters[2]);
if (!Number.isInteger(idx))
reject("Can't find feed with id " + parameters[2]);
else {
guildData.feeds.splice(idx, 1);
resolve("Feed removed!");
}
}
});
}
function viewFeeds(guildData) {
if (!guildData)
return Promise.reject("Guild not setup");
return Promise.resolve(guildData.feeds.map(f => f.toString()).join("\n"));
}
function checkFeedsInGuilds(guilds, guildsData) {
Object.keys(guildsData).forEach(key => guildsData[key].checkFeeds(guilds));
}
function parseLinksInGuilds(guilds, guildsData) {
const promises = [];
for (let guildId of guilds.keys()) {
const guildData = guildsData[guildId];
if (guildData)
promises.push(guildData.cachePastPostedLinks(guilds.get(guildId)));
}
return Promise.all(promises);
}

20
app/commands.json

@ -0,0 +1,20 @@
{
"addFeed": {
"command": "add-feed",
"description": "Add an RSS feed to be posted in a channel, with an optional role to tag",
"syntax": "add-feed <url> <#channel> [@role]",
"admin": true
},
"removeFeed": {
"command": "remove-feed",
"description": "Remove an RSS feed by it's ID",
"syntax": "remove-feed <id>",
"admin": true
},
"viewFeeds": {
"command": "view-feeds",
"description": "View a list of configured feeds and their associated details",
"syntax": "view-feed",
"admin": true
}
}

41
app/config.json

@ -1,43 +1,4 @@
{
"generic": {
"saveFile": "./guilds.json",
"saveIntervalSec": 60,
"website": "https://benji7425.github.io",
"discordInvite": "https://discord.gg/SSkbwSJ",
"defaultDMResponse": "This bot does not have any handling for direct messages. To learn more or get help please visit %s, or join my Discord server here: %s"
},
"maxCacheSize": 100,
"feedCheckIntervalSec": 30,
"commands": {
"version": {
"command": "version",
"description": "Returns the bot version",
"syntax": "version",
"admin": false
},
"help": {
"command": "help",
"description": "Display information about commands available to you",
"syntax": "help",
"admin": false
},
"addFeed": {
"command": "add-feed",
"description": "Add an RSS feed to be posted in a channel, with an optional role to tag",
"syntax": "add-feed <url> <#channel> [@role]",
"admin": true
},
"removeFeed": {
"command": "remove-feed",
"description": "Remove an RSS feed by it's ID",
"syntax": "remove-feed <id>",
"admin": true
},
"viewFeeds": {
"command": "view-feeds",
"description": "View a list of configured feeds and their associated details",
"syntax": "view-feed",
"admin": true
}
}
"feedCheckIntervalSec": 30
}

204
app/index.js

@ -1,123 +1,103 @@
//node imports
const FileSystem = require("fs"); //manage files
const Util = require("util"); //various node utilities
//external lib imports
const Discord = require("discord.js");
const JsonFile = require("jsonfile"); //save/load data to/from json
//my imports
const DiscordUtil = require("discordjs-util"); //some discordjs helper functions of mine
//app components
const GuildData = require("./models/guild-data.js"); //data structure for guilds
const PackageJSON = require("../package.json"); //used to provide some info about the bot
const Bot = require("./bot.js");
//global vars
let writeFile = null;
//use module.exports as a psuedo "onready" function
module.exports = (client, config = null) => {
config = config || require("./config.json"); //load config file
const guildsData = FileSystem.existsSync(config.generic.saveFile) ? fromJSON(JsonFile.readFileSync(config.generic.saveFile)) : {}; //read data from file, or generate new one if file doesn't exist
//create our writeFile function that will allow other functions to save data to json without needing access to the full guildsData or config objects
//then set an interval to automatically save data to file
writeFile = () => JsonFile.writeFile(config.generic.saveFile, guildsData, err => { if (err) DiscordUtil.dateError("Error writing file", err); });
setInterval(() => writeFile(), config.generic.saveIntervalSec * 1000);
//handle messages
client.on("message", message => {
if (message.author.id !== client.user.id) { //check the bot isn't triggering itself
//check whether we need to use DM or text channel handling
if (message.channel.type === "dm")
HandleMessage.dm(client, config, message);
else if (message.channel.type === "text" && message.member)
HandleMessage.text(client, config, message, guildsData);
}
const GetUrls = require("get-urls"); //for extracting urls from messages
const Core = require("../discord-bot-core");
const GuildData = require("./models/guild-data.js");
const FeedData = require("./models/feed-data.js");
const Config = require("./config.json");
function onReady(client, guildsData) {
return new Promise((resolve, reject) => {
parseLinksInGuilds(client.guilds, guildsData)
.then(() => checkFeedsInGuilds(client.guilds, guildsData))
.then(() => setInterval(() => checkFeedsInGuilds(client.guilds, guildsData), Config.feedCheckIntervalSec * 1000)); //set up an interval to check all the feeds
});
}
Bot.onReady(client, guildsData, config).then(() => writeFile).catch(err => DiscordUtil.dateError(err));
};
function onTextMessage(message, guildData) {
guildData.feeds.forEach(feedData => {
if (message.channel.name === feedData.channelName)
feedData.cachedLinks.push(...GetUrls(message.content)); //spread the urlSet returned by GetUrls into the cache array
});
}
const HandleMessage = {
dm: (client, config, message) => {
message.reply(Util.format(config.generic.defaultDMResponse, config.generic.website, config.generic.discordInvite));
},
text: (client, config, message, guildsData) => {
const isCommand = message.content.startsWith(message.guild.me.toString());
let guildData = guildsData[message.guild.id];
if (!guildData)
guildData = guildsData[message.guild.id] = new GuildData({ id: message.guild.id });
if (isCommand) {
const userIsAdmin = message.member.permissions.has("ADMINISTRATOR");
const botName = "@" + (message.guild.me.nickname || client.user.username);
const split = message.content.toLowerCase().split(/\ +/); //split the message at whitespace
const command = split[1]; //extract the command used
const commandObj = config.commands[Object.keys(config.commands).find(x => config.commands[x].command.toLowerCase() === command)]; //find the matching command object
if (!commandObj || (!commandObj.admin && !userIsAdmin))
return;
const params = split.slice(2, split.length); //extract the parameters passed for the command
const expectedParamCount = commandObj.syntax.split(/\ +/).length - 1; //calculate the number of expected command params
let finalisedParams;
if (params.length > expectedParamCount) //if we have more params than needed
finalisedParams = params.slice(0, expectedParamCount - 1).concat([params.slice(expectedParamCount - 1, params.length).join(" ")]);
else //else we either have exactly the right amount, or not enough
finalisedParams = params;
//find which command was used and handle it
switch (command) {
case config.commands.version.command:
message.reply(`${PackageJSON.name} v${PackageJSON.version}`);
break;
case config.commands.help.command:
message.channel.send(createHelpEmbed(botName, config, userIsAdmin));
break;
default:
if (finalisedParams.length >= expectedParamCount)
Bot.onCommand(commandObj, config.commands, finalisedParams, guildData, message, config, client, botName)
.then(msg => {
message.reply(msg);
writeFile();
})
.catch(err => {
message.reply(err);
DiscordUtil.dateError(err);
});
else
message.reply(`Incorrect syntax!\n**Expected:** *${botName} ${commandObj.syntax}*\n**Need help?** *${botName} ${config.commands.help.command}*`);
break;
function addFeed(client, guildData, message, maxCacheSize) {
return new Promise((resolve, reject) => {
const feedUrl = [...GetUrls(message.content)][0];
const channel = message.mentions.channels.first();
if (!feedUrl || !channel)
reject("Please provide both a channel and an RSS feed URL. You can optionally @mention a role also.");
const role = message.mentions.roles.first();
const feedData = new FeedData({
url: feedUrl,
channelName: channel.name,
roleName: role ? role.name : null,
maxCacheSize: maxCacheSize
});
//ask the user if they're happy with the details they set up, save if yes, don't if no
Core.util.ask(client, message.channel, message.member, "Are you happy with this?\n" + feedData.toString())
.then(responseMessage => {
//if they responded yes, save the feed and let them know, else tell them to start again
if (responseMessage.content.toLowerCase() === "yes") {
if (!guildData)
guildData = new GuildData({ id: message.guild.id, feeds: [] });
guildData.feeds.push(feedData);
resolve("Your new feed has been saved!");
}
else
reject("Your feed has not been saved, please add it again with the correct details");
});
});
}
function removeFeed(guildData, message, botName) {
return new Promise((resolve, reject) => {
const parameters = message.content.split(" ");
if (parameters.length !== 3)
resolve(`Please use the command as such:\n\`\`\` ${botName} remove-feed feedid\`\`\``);
else {
const idx = guildData.feeds.findIndex(feed => feed.id === parameters[2]);
if (!Number.isInteger(idx))
reject("Can't find feed with id " + parameters[2]);
else {
guildData.feeds.splice(idx, 1);
resolve("Feed removed!");
}
}
else
Bot.onNonCommandMsg(message, guildData);
}
};
function fromJSON(json) {
const guildsData = Object.keys(json);
guildsData.forEach(guildID => { json[guildID] = new GuildData(json[guildID]); });
return json;
});
}
function createHelpEmbed(name, config, userIsAdmin) {
const commandsArr = Object.keys(config.commands).map(x => config.commands[x]).filter(x => userIsAdmin || !x.admin);
function viewFeeds(guildData) {
if (!guildData)
return Promise.reject("Guild not setup");
const embed = new Discord.RichEmbed().setTitle("__Help__");
return Promise.resolve(guildData.feeds.map(f => f.toString()).join("\n"));
}
commandsArr.forEach(command => {
embed.addField(command.command, `${command.description}\n**Usage:** *${name} ${command.syntax}*${userIsAdmin && command.admin ? "\n***Admin only***" : ""}`);
});
function checkFeedsInGuilds(guilds, guildsData) {
Object.keys(guildsData).forEach(key => guildsData[key].checkFeeds(guilds));
}
embed.addField("__Need more help?__", `[Visit my website](${config.generic.website}) or [Join my Discord](${config.generic.discordInvite})`, true);
function parseLinksInGuilds(guilds, guildsData) {
const promises = [];
for (let guildId of guilds.keys()) {
const guildData = guildsData[guildId];
if (guildData)
promises.push(guildData.cachePastPostedLinks(guilds.get(guildId)));
}
return Promise.all(promises);
}
module.exports = {
onReady,
onTextMessage,
addFeed,
removeFeed,
viewFeeds
};
return { embed };
}
Core.bootstrap(module.exports, GuildData, require("./commands.json"));

20
app/models/guild-data.js

@ -1,23 +1,7 @@
const FeedData = require("./feed-data.js");
const Util = require("discordjs-util");
const DiscordUtil = require("../../discord-bot-core").util;
module.exports = class GuildData {
constructor({ id, feeds }) {
constructor({ id }) {
this.id = id;
this.feeds = (feeds || []).map(feed => new FeedData(feed));
}
cachePastPostedLinks(guild) {
const promises = [];
this.feeds.forEach(feed => {
promises.push(feed.updatePastPostedLinks(guild).catch(Util.dateError));
});
return Promise.all(promises);
}
checkFeeds(guilds) {
this.feeds.forEach(feed => feed.check(guilds.get(this.id)));
}
};

20
bootstrap.js

@ -1,20 +0,0 @@
const Discord = require("discord.js");
const DiscordUtil = require("discordjs-util");
const client = new Discord.Client();
process.on("uncaughtException", (err) => {
DiscordUtil.dateError("Uncaught exception!", err);
});
client.login(require("./token.json").token);
client.on("ready", () => {
DiscordUtil.dateLog("Registered bot " + client.user.username);
require("./app/index.js")(client);
client.user.setPresence({ game: { name: "benji7425.github.io", type: 0 } });
});
client.on("disconnect", eventData => {
DiscordUtil.dateError("Bot was disconnected!", eventData.code, eventData.reason);
});

79
discord-bot-core/.gitignore

@ -0,0 +1,79 @@
### Discord bots ####
guilds.json
token.json
log
# Created by https://www.gitignore.io/api/node,visualstudiocode
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_cache
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
# End of https://www.gitignore.io/api/node,visualstudiocode

12
discord-bot-core/.gitrepo

@ -0,0 +1,12 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
;
[subrepo]
remote = git@github.com:benji7425/discord-bot-core.git
branch = master
commit = 270d24d2119404ba42364584868a3ed364e87678
parent = 341964a90ddca9afc684fd9331fc4177b8b68c04
method = merge
cmdver = 0.3.1

2
discord-bot-core/.npmrc

@ -0,0 +1,2 @@
save=true
save-exact=true

74
discord-bot-core/bootstrapper.js

@ -0,0 +1,74 @@
//node imports
const FileSystem = require("fs"); //manage files
const Util = require("util");
//external lib imports
const Discord = require("discord.js");
const JsonFile = require("jsonfile"); //save/load data to/from json
const Config = require("./config.json");
const ParentPackageJSON = require("../package.json"); //used to provide some info about the bot
const DiscordUtil = require("./util.js"); //some discordjs helper functions of mine
const MessageHandler = require("./message-handler.js");
function bootstrap(component, guildDataModel, commands) {
process.on("uncaughtException", (err) => {
DiscordUtil.dateError("Uncaught exception!", err);
});
const client = new Discord.Client();
client.on("ready", () => {
onReady(client, component, guildDataModel, commands);
client.user.setGame("benji7425.github.io");
DiscordUtil.dateLog("Registered bot " + client.user.username);
});
client.on("disconnect", eventData => {
DiscordUtil.dateError("Bot was disconnected!", eventData.code, eventData.reason);
});
client.login(require("../token.json").token);
}
function onReady(client, component, guildDataModel, commands) {
const saveFile = Util.format(Config.saveFile, ParentPackageJSON.name + "");
const guildsData =
FileSystem.existsSync(saveFile) ?
fromJSON(JsonFile.readFileSync(saveFile), guildDataModel) : {};
const writeFile = () =>
JsonFile.writeFile(
saveFile,
guildsData,
err => {
if (err) DiscordUtil.dateError("Error writing file", err);
});
setInterval(() => writeFile(), Config.saveIntervalSec * 1000);
client.on("message", message => {
if (message.author.id !== client.user.id) {
if (message.channel.type === "dm")
MessageHandler.handleDirectMessage({ client, message });
else if (message.channel.type === "text" && message.member)
MessageHandler.handleTextMessage({ client, commands, message, guildDataModel, guildsData, component, writeFile });
}
});
component.onReady(client, guildsData)
.then(() => writeFile())
.catch(err => DiscordUtil.dateError(err));
}
function fromJSON(json, guildDataModel) {
const guildsData = Object.keys(json);
guildsData.forEach(guildID => { json[guildID] = new guildDataModel(json[guildID]); });
return json;
}
module.exports = {
bootstrap
};

21
discord-bot-core/config.json

@ -0,0 +1,21 @@
{
"saveFile": "./%s-data.json",
"saveIntervalSec": 60,
"website": "https://benji7425.github.io",
"discordInvite": "https://discord.gg/SSkbwSJ",
"defaultDMResponse": "This bot does not have any handling for direct messages. To learn more or get help please visit %s, or join my Discord server here: %s",
"commands": {
"version": {
"command": "version",
"description": "Returns the bot version",
"syntax": "version",
"admin": false
},
"help": {
"command": "help",
"description": "Display information about commands available to you",
"syntax": "help",
"admin": false
}
}
}

12
discord-bot-core/index.js

@ -0,0 +1,12 @@
const Config = require("./config.json");
module.exports = {
bootstrap(component, guildDataModel, commands) {
require("./bootstrapper.js").bootstrap(component, guildDataModel, commands);
},
util: require("./util.js"),
details: {
website: Config.website,
discordInvite: Config.discordInvite
}
};

99
discord-bot-core/message-handler.js

@ -0,0 +1,99 @@
//node imports
const Util = require("util");
//external lib imports
const Discord = require("discord.js");
//lib components
const Config = require("./config.json"); //generic lib configuration
const DiscordUtil = require("./util.js"); //some discordjs helper functions of mine
const ParentPackageJSON = require("../package.json"); //used to provide some info about the bot
function handleDirectMessage(client, message) {
message.reply(Util.format(Config.generic.defaultDMResponse, Config.generic.website, Config.generic.discordInvite));
}
function handleTextMessage({ client, commands, message, guildDataModel, guildsData, component, writeFile }) {
const isCommand = message.content.startsWith(message.guild.me.toString());
let guildData = guildsData[message.guild.id];
if (!guildData)
guildData = guildsData[message.guild.id] = new guildDataModel({ id: message.guild.id });
if (isCommand) {
Object.assign(commands, Config.commands);
const userIsAdmin = message.member.permissions.has("ADMINISTRATOR");
const botName = "@" + (message.guild.me.nickname || client.user.username);
const { command, commandProp, params, expectedParamCount } = getCommandDetails(message, commands, userIsAdmin) || { command: null, commandProp: null, params: null, expectedParamCount: null };
const invoke = component[commandProp];
if (!command || !params || isNaN(expectedParamCount))
return;
switch (command) {
case Config.commands.version:
message.reply(`${ParentPackageJSON.name} v${ParentPackageJSON.version}`);
break;
case Config.commands.help:
message.channel.send(createHelpEmbed(botName, commands, userIsAdmin));
break;
default:
if (invoke && params.length >= expectedParamCount) {
invoke({ command, params: params, guildData, botName, message, client })
.then(msg => {
message.reply(msg);
writeFile();
})
.catch(err => {
message.reply(err);
DiscordUtil.dateError(err);
});
}
else
message.reply(`Incorrect syntax!\n**Expected:** *${botName} ${command.syntax}*\n**Need help?** *${botName} ${commands.help.command}*`);
break;
}
}
else
component.onTextMessage(message, guildData);
}
function getCommandDetails(message, commands, userIsAdmin) {
const splitMessage = message.content.toLowerCase().split(/ +/);
const commandStr = splitMessage[1];
const commandProp = Object.keys(commands).find(x => commands[x].command.toLowerCase() === commandStr);
const command = commands[commandProp];
if (!command || (command.admin && !userIsAdmin))
return;
const params = splitMessage.slice(2, splitMessage.length);
const expectedParamCount = command.syntax.split(/ +/).length - 1;
let finalisedParams;
if (params.length > expectedParamCount)
finalisedParams = params.slice(0, expectedParamCount - 1).concat([params.slice(expectedParamCount - 1, params.length).join(" ")]);
else
finalisedParams = params;
return { command, commandProp, params: finalisedParams, expectedParamCount };
}
function createHelpEmbed(name, commands, userIsAdmin) {
const commandsArr = Object.keys(commands).map(x => commands[x]).filter(x => userIsAdmin || !x.admin);
const embed = new Discord.RichEmbed().setTitle(`__Help__ for ${(ParentPackageJSON.name + "").replace("discord-bot-", "")}`);
commandsArr.forEach(command => {
embed.addField(command.command, `${command.description}\n**Usage:** *${name} ${command.syntax}*${userIsAdmin && command.admin ? "\n***Admin only***" : ""}`);
});
embed.addField("__Need more help?__", `[Visit my website](${Config.website}) or [Join my Discord](${Config.discordInvite})`, true);
return { embed };
}
module.exports = {
handleDirectMessage,
handleTextMessage
};

23
discord-bot-core/package.json

@ -0,0 +1,23 @@
{
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"discord.js": "11.2.0",
"discordjs-util": "git+https://github.com/benji7425/discordjs-util.git",
"jsonfile": "3.0.1",
"parent-package-json": "2.0.1",
"simple-file-writer": "2.0.0"
},
"name": "discord-bot-core",
"repository": {
"type": "git",
"url": "git+https://github.com/benji7425/discord-bot-core.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/benji7425/discord-bot-core/issues"
},
"homepage": "https://github.com/benji7425/discord-bot-core#readme",
"description": "Core code shared amongst my Discord bots"
}

49
discord-bot-core/util.js

@ -0,0 +1,49 @@
const Console = require("console");
const SimpleFileWriter = require("simple-file-writer");
const logWriter = new SimpleFileWriter("./log");
/**
* Returns a promise that the user will answer
* @param {TextChannel} textChannel discord.js TextChannel to ask the question in
* @param {GuildMember} member discord.js Member to ask the question to
* @param {string} question question to ask
*/
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 handler = responseMessage => {
if (responseMessage.channel.id === textChannel.id &&
responseMessage.member.id === member.id) {
client.removeListener("message", handler);
resolve(responseMessage);
}
};
client.on("message", handler);
textChannel.send(member.toString() + " " + question).catch(reject);
});
}
function dateLog(...args) {
args = formatArgs(args);
Console.log.apply(this, args);
logWriter.write(args.join("") + "\n");
}
function dateError(...args) {
args = formatArgs(args);
Console.error.apply(this, args);
logWriter.write(args.join("") + "\n");
}
function formatArgs(args) {
return ["[", new Date().toUTCString(), "] "].concat(args);
}
module.exports = {
dateError,
dateLog,
ask
};

3
package.json

@ -2,7 +2,8 @@
"version": "3.0.0-b2",
"main": "app/index.js",
"scripts": {
"start": "node bootstrap.js"
"postinstall": "cd ./discord-bot-core && npm install",
"start": "node app/index.js"
},
"dependencies": {
"discord.js": "11.1.0",

Loading…
Cancel
Save