diff --git a/.vscode/launch.json b/.vscode/launch.json index e660a01..9ad517f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Launch", "type": "node", "request": "launch", - "program": "${workspaceRoot}\\feed-bot.js", + "program": "${workspaceRoot}\\wrapper\\index.js", "stopOnEntry": false, "args": [], "cwd": "${workspaceRoot}", diff --git a/config.json b/config.json index 3d0046c..6723a9b 100644 --- a/config.json +++ b/config.json @@ -15,8 +15,7 @@ "help": "!help" }, "developerCommands": { - "logUpload": "!logsplease", - "cacheList": "!cacheList" + "cacheList": "!cached" }, "developers": [ "117966411548196870" diff --git a/feed-bot.js b/feed-bot.js deleted file mode 100644 index 8e7b7b7..0000000 --- a/feed-bot.js +++ /dev/null @@ -1,293 +0,0 @@ -//external library imports -var Dns = require("dns"); //for connectivity checking -var Url = require("url"); //for url parsing -var Uri = require("urijs"); //for finding urls within message strings -var Discord = require("discord.io"); //for obvious reasons -var FeedRead = require("feed-read"); //for rss feed reading -var JsonFile = require("jsonfile"); //reading/writing json - -//my imports -var Log = require("./log.js"); //some very simple logging functions I made -var BotConfig = require("./bot-config.json"); //bot config file containing bot token -var Config = require("./config.json"); //config file containing other settings - -var DiscordClient = { - bot: null, - startup: function () { - //check if we can connect to discordapp.com to authenticate the bot - Dns.resolve("discordapp.com", function (err) { - if (err) - Log.error("CONNECTION ERROR: Unable to locate discordapp.com to authenticate the bot", err); - else { - //if there was no error, go ahead and create and authenticate the bot - DiscordClient.bot = new Discord.Client({ - token: BotConfig.token, - autorun: true - }); - - //set up the bot's event handlers - DiscordClient.bot.on("ready", DiscordClient.onReady); - DiscordClient.bot.on("disconnect", DiscordClient.onDisconnect); - DiscordClient.bot.on("message", DiscordClient.onMessage); - } - }); - }, - onReady: function () { - Log.info("Registered/connected bot " + DiscordClient.bot.username + " - (" + DiscordClient.bot.id + ")"); - - DiscordClient.checkPastMessagesForLinks(); //we need to check past messages for links on startup, but also on reconnect because we don't know what has happened during the downtime - - //set the interval function to check the feed - intervalFunc = () => { - Feed.check((err, articles) => { - Links.validate(err, articles, DiscordClient.post); - }); - }; - }, - onDisconnect: function (err, code) { - Log.event("Bot was disconnected! " + (err ? err : "") + (code ? code : "No disconnect code provided.") + "\nClearing the feed timer and starting reconnect timer", "Discord.io"); - - intervalFunc = DiscordClient.startup; //reassign the interval function to try restart the bot every 5 sec - }, - onMessage: function (user, userID, channelID, message) { - //contains a link, and is not the latest link from the rss feed - if (channelID === Config.channelID && Links.messageContainsLink(message) && (message !== Links.latestFromFeedlatestFeedLink)) { - Log.event("Detected posted link in this message: " + message, "Discord.io"); - - //extract the url from the string, and cache it - Uri.withinString(message, function (url) { - Links.cache(Links.standardise(url)); - return url; - }); - } - else { - //iterate over all of our message triggers to see if the message sent requires any action - for (var i = 0; i < DiscordClient.messageTriggers.length; i++) { - var messageTrigger = DiscordClient.messageTriggers[i]; - if (message === messageTrigger.message) { - //check if its locked to a channel or to a specific user - if ((messageTrigger.channelID && messageTrigger.channelID === channelID) || (messageTrigger.userIDs && messageTrigger.userIDs.includes(userID))) - messageTrigger.action(user, userID, channelID, message); - } - } - } - - }, - checkPastMessagesForLinks: function () { - var limit = 100; - Log.info("Attempting to check past " + limit + " messages for links"); - - //get the last however many messsages from our discord channel - DiscordClient.bot.getMessages({ - channelID: Config.channelID, - limit: limit - }, function (err, messages) { - if (err) Log.error("Error fetching discord messages.", err); - else { - Log.info("Pulled last " + messages.length + " messages, scanning for links"); - - var messageContents = messages.map((x) => { return x.content; }).reverse(); //extract an array of strings from the array of message objects - - for (var messageIdx in messageContents) { - var message = messageContents[messageIdx]; - - if (Links.messageContainsLink(message)) //test if the message contains a url - //detect the url inside the string, and cache it - Uri.withinString(message, function (url) { - Links.cache(url); - return url; - }); - } - } - }); - }, - post: function (link) { - //send a messsage containing the new feed link to our discord channel - DiscordClient.bot.sendMessage({ - to: Config.channelID, - message: Subscriptions.mention() + link - }); - }, - //actions to perform when certain messages are detected, along with channel or user requirements - messageTriggers: [ - { - message: Config.userCommands.subscribe, - action: (user, userID, channelID, message) => { if (Config.allowSubscriptions) Subscriptions.subscribe(user, userID, channelID, message); }, - channelID: Config.channelID - }, - { - message: Config.userCommands.unsubscribe, - action: (user, userID, channelID, message) => { if (Config.allowSubscriptions) Subscriptions.unsubscribe(user, userID, channelID, message); }, - channelID: Config.channelID - }, - { - message: Config.userCommands.help, - action: (user, userID, channelID, message) => { - DiscordClient.bot.sendMessage({ - to: Config.channelID, - message: Config.userCommands.join(" + ") - }); - }, - channelID: Config.channelID - }, - { - message: Config.developerCommands.logUpload, - action: (user, userID, channelID, message) => { - DiscordClient.bot.uploadFile({ - to: channelID, - file: Config.logFile - }); - }, - userIDs: Config.developers - }, - { - message: Config.developerCommands.cacheList, - action: (user, userID, channelID, message) => { - DiscordClient.bot.sendMessage({ - to: channelID, - message: Links.cached.join(", ") - }); - }, - userIDs: Config.developers - } - ] -}; - -var Subscriptions = { - subscribe: function (user, userID, channelID, message) { - DiscordClient.bot.addToRole({ - serverID: Config.serverID, - userID: userID, - roleID: Config.subscribersRoleID - }, - (err) => { - if (err) Log.raw(err); //log the error if there is an error - else { //else go ahead and confirm subscription - Log.event("Subscribed user " + (user ? user + "(" + userID + ")" : userID)); - - DiscordClient.bot.sendMessage({ - to: channelID, - message: "You have successfully subscribed" - }, (err, response) => { setTimeout(() => { DiscordClient.bot.deleteMessage({ channelID: channelID, messageID: response.id }); }, Config.messageDeleteDelay); }); //delete the subscription confirmation message after a delay - } - }); - - - }, - - unsubscribe: function (user, userID, channelID, message) { - DiscordClient.bot.removeFromRole({ - serverID: Config.serverID, - userID: userID, - roleID: Config.subscribersRoleID - }, - (err) => { - if (err) Log.raw(err); //log the error if there is an error - else { //else go ahead and confirm un-subscription - Log.event("Unsubscribed user " + (user ? user + "(" + userID + ")" : userID)); - - DiscordClient.bot.sendMessage({ - to: channelID, - message: "You have successfully unsubscribed" - }, (err, response) => { setTimeout(() => { DiscordClient.bot.deleteMessage({ channelID: channelID, messageID: response.id }); }, Config.messageDeleteDelay); }); //delete the un-subscription confirmation message after a delay - } - }); - }, - - mention: function () { - return Config.allowSubscriptions ? "<@&" + Config.subscribersRoleID + "> " : ""; - } -}; - -var YouTube = { - url: { - share: "http://youtu.be/", - full: "http://www.youtube.com/watch?v=", - createFullUrl: function (shareUrl) { - return shareUrl.replace(YouTube.url.share, YouTube.url.full); - }, - createShareUrl: function (fullUrl) { - var shareUrl = fullUrl.replace(YouTube.url.full, YouTube.url.share); - - if (shareUrl.includes("&")) shareUrl = shareUrl.slice(0, fullUrl.indexOf("&")); - - return shareUrl; - } - }, -}; - -var Links = { - standardise: function (link) { - link = link.replace("https://", "http://"); //cheaty way to get around http and https not matching - if (Config.youtubeMode) link = link.split("&")[0]; //quick way to chop off stuff like &feature=youtube etc - return link; - }, - messageContainsLink: function (message) { - var messageLower = message.toLowerCase(); - return messageLower.includes("http://") || messageLower.includes("https://") || messageLower.includes("www."); - }, - cached: [], - latestFeedLink: "", - cache: function (link) { - link = Links.standardise(link); - - if (Config.youtubeMode) link = YouTube.url.createShareUrl(link); - - //store the new link if not stored already - if (!Links.isCached(link)) { - Links.cached.push(link); - Log.info("Cached URL: " + link); - } - - if (Links.cached.length > Config.numLinksToCache) Links.cached.shift(); //get rid of the first array element if we have reached our cache limit - }, - isCached: function (link) { - link = Links.standardise(link); - - if (Config.youtubeMode) - return Links.cached.includes(YouTube.url.createShareUrl(link)); - - return Links.cached.includes(link); - }, - validate: function (err, articles, callback) { - if (err) Log.error("FEED ERROR: Error reading RSS feed.", err); - else { - var latestLink = Links.standardise(articles[0].link); - if (Config.youtubeMode) latestLink = YouTube.url.createShareUrl(latestLink); - - //make sure we don't spam the latest link - if (latestLink === Links.latestFeedLink) - return; - - //make sure the latest link hasn't been posted already - if (Links.isCached(latestLink)) { - Log.info("Didn't post new feed link because already detected as posted " + latestLink); - } - else { - callback(latestLink); - - Links.cache(latestLink); //make sure the link is cached, so it doesn't get posted again - } - - Links.latestFeedLink = latestLink; //ensure our latest feed link variable is up to date, so we can track when the feed updates - } - } -}; - -var Feed = { - urlObj: Url.parse(Config.feedUrl), - check: function (callback) { - Dns.resolve(Feed.urlObj.host, function (err) { //check that we have an internet connection (well not exactly - check that we have a connection to the host of the feedUrl) - if (err) Log.error("CONNECTION ERROR: Cannot resolve host.", err); - else FeedRead(Config.feedUrl, callback); - }); - } -}; - -var intervalFunc = () => { }; //do nothing by default - -//IIFE to kickstart the bot when the app loads -(function () { - DiscordClient.startup(); - setInterval(() => { intervalFunc(); }, Config.pollingInterval); -})(); \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..8591530 --- /dev/null +++ b/index.js @@ -0,0 +1,264 @@ +//external library imports +var Dns = require("dns"); //for connectivity checking +var Url = require("url"); //for url parsing +var Uri = require("urijs"); //for finding urls within message strings +var FeedRead = require("feed-read"); //for rss feed reading +var JsonFile = require("jsonfile"); //reading/writing json +var Console = require("console"); + +//my imports +var Config = require("./config.json"); //config file containing other settings + +module.exports = { + onReady: (bot) => { + Actions.checkPastMessagesForLinks(bot); //we need to check past messages for links on startup, but also on reconnect because we don't know what has happened during the downtime + + //set the interval function to check the feed + intervalFunc = () => { + Feed.check((err, articles) => { + Links.validate(err, articles, (latestLink) => Actions.post(bot, latestLink)); + }); + }; + + setInterval(() => { intervalFunc(); }, Config.pollingInterval); + }, + onMessage: function (bot, user, userID, channelID, message) { + //contains a link, and is not the latest link from the rss feed + if (channelID === Config.channelID && Links.messageContainsLink(message) && (message !== Links.latestFromFeedlatestFeedLink)) { + Console.info("Detected posted link in this message: " + message, "Discord.io"); + + //extract the url from the string, and cache it + Uri.withinString(message, function (url) { + Links.cache(Links.standardise(url)); + return url; + }); + } + + }, + commands: [ + { + command: Config.userCommands.subscribe, + type: "equals", + action: (bot, user, userID, channelID, message) => { if (Config.allowSubscriptions) Subscriptions.subscribe(bot, user, userID, channelID, message); }, + channelIDs: [Config.channelID] + }, + { + command: Config.userCommands.unsubscribe, + type: "equals", + action: (bot, user, userID, channelID, message) => { if (Config.allowSubscriptions) Subscriptions.unsubscribe(bot, user, userID, channelID, message); }, + channelIDs: [Config.channelID] + }, + { + command: Config.userCommands.help, + type: "equals", + action: (bot, user, userID, channelID, message) => { + bot.sendMessage({ + to: Config.channelID, + message: "Available commands: " + getValues(Config.userCommands).join(", ") + }); + }, + channelIDs: [Config.channelID] + }, + { + command: Config.developerCommands.logUpload, + type: "equals", + action: (bot, user, userID, channelID, message) => { + bot.uploadFile({ + to: channelID, + file: Config.logFile + }); + }, + userIDs: Config.developers + }, + { + command: Config.developerCommands.cacheList, + type: "equals", + action: (bot, user, userID, channelID, message) => { + bot.sendMessage({ + to: channelID, + message: Links.cached.join(", ") + }); + }, + userIDs: Config.developers + } + ] +}; + +var Actions = { + post: (bot, link) => { + //send a messsage containing the new feed link to our discord channel + bot.sendMessage({ + to: Config.channelID, + message: Subscriptions.mention() + link + }); + }, + checkPastMessagesForLinks: (bot) => { + var limit = 100; + Console.info("Attempting to check past " + limit + " messages for links"); + + //get the last however many messsages from our discord channel + bot.getMessages({ + channelID: Config.channelID, + limit: limit + }, function (err, messages) { + if (err) Console.error("Error fetching discord messages.", err); + else { + Console.info("Pulled last " + messages.length + " messages, scanning for links"); + + var messageContents = messages.map((x) => { return x.content; }).reverse(); //extract an array of strings from the array of message objects + + for (var messageIdx in messageContents) { + var message = messageContents[messageIdx]; + + if (Links.messageContainsLink(message)) //test if the message contains a url + //detect the url inside the string, and cache it + Uri.withinString(message, function (url) { + Links.cache(url); + return url; + }); + } + } + }); + }, +}; + +var Subscriptions = { + subscribe: function (bot, user, userID, channelID, message) { + bot.addToRole({ + serverID: Config.serverID, + userID: userID, + roleID: Config.subscribersRoleID + }, + (err) => { + if (err) Console.log(err); //log the error if there is an error + else { //else go ahead and confirm subscription + Console.info("Subscribed user " + (user ? user + "(" + userID + ")" : userID)); + + bot.sendMessage({ + to: channelID, + message: "You have successfully subscribed" + }, (err, response) => { setTimeout(() => { bot.deleteMessage({ channelID: channelID, messageID: response.id }); }, Config.messageDeleteDelay); }); //delete the subscription confirmation message after a delay + } + }); + + + }, + + unsubscribe: function (bot, user, userID, channelID, message) { + bot.removeFromRole({ + serverID: Config.serverID, + userID: userID, + roleID: Config.subscribersRoleID + }, + (err) => { + if (err) Console.log(err); //log the error if there is an error + else { //else go ahead and confirm un-subscription + Console.info("Unsubscribed user " + (user ? user + "(" + userID + ")" : userID)); + + bot.sendMessage({ + to: channelID, + message: "You have successfully unsubscribed" + }, (err, response) => { setTimeout(() => { bot.deleteMessage({ channelID: channelID, messageID: response.id }); }, Config.messageDeleteDelay); }); //delete the un-subscription confirmation message after a delay + } + }); + }, + + mention: function () { + return Config.allowSubscriptions ? "<@&" + Config.subscribersRoleID + "> " : ""; + } +}; + +var YouTube = { + url: { + share: "http://youtu.be/", + full: "http://www.youtube.com/watch?v=", + createFullUrl: function (shareUrl) { + return shareUrl.replace(YouTube.url.share, YouTube.url.full); + }, + createShareUrl: function (fullUrl) { + var shareUrl = fullUrl.replace(YouTube.url.full, YouTube.url.share); + + if (shareUrl.includes("&")) shareUrl = shareUrl.slice(0, fullUrl.indexOf("&")); + + return shareUrl; + } + }, +}; + +var Links = { + standardise: function (link) { + link = link.replace("https://", "http://"); //cheaty way to get around http and https not matching + if (Config.youtubeMode) link = link.split("&")[0]; //quick way to chop off stuff like &feature=youtube etc + return link; + }, + messageContainsLink: function (message) { + var messageLower = message.toLowerCase(); + return messageLower.includes("http://") || messageLower.includes("https://") || messageLower.includes("www."); + }, + cached: [], + latestFeedLink: "", + cache: function (link) { + link = Links.standardise(link); + + if (Config.youtubeMode) link = YouTube.url.createShareUrl(link); + + //store the new link if not stored already + if (!Links.isCached(link)) { + Links.cached.push(link); + Console.info("Cached URL: " + link); + } + + if (Links.cached.length > Config.numLinksToCache) Links.cached.shift(); //get rid of the first array element if we have reached our cache limit + }, + isCached: function (link) { + link = Links.standardise(link); + + if (Config.youtubeMode) + return Links.cached.includes(YouTube.url.createShareUrl(link)); + + return Links.cached.includes(link); + }, + validate: function (err, articles, callback) { + if (err) Console.error("FEED ERROR: Error reading RSS feed.", err); + else { + var latestLink = Links.standardise(articles[0].link); + if (Config.youtubeMode) latestLink = YouTube.url.createShareUrl(latestLink); + + //make sure we don't spam the latest link + if (latestLink === Links.latestFeedLink) + return; + + //make sure the latest link hasn't been posted already + if (Links.isCached(latestLink)) { + Console.info("Didn't post new feed link because already detected as posted " + latestLink); + } + else { + callback(latestLink); + + Links.cache(latestLink); //make sure the link is cached, so it doesn't get posted again + } + + Links.latestFeedLink = latestLink; //ensure our latest feed link variable is up to date, so we can track when the feed updates + } + } +}; + +var Feed = { + urlObj: Url.parse(Config.feedUrl), + check: function (callback) { + Dns.resolve(Feed.urlObj.host, function (err) { //check that we have an internet connection (well not exactly - check that we have a connection to the host of the feedUrl) + if (err) Console.error("CONNECTION ERROR: Cannot resolve host.", err); + else FeedRead(Config.feedUrl, callback); + }); + } +}; + +var getValues = function (obj) { + var values = []; + for (var value in obj) + if (obj.hasOwnProperty(value)) + values.push(obj[value]); + return values; +}; + +var intervalFunc = () => { }; //do nothing by default \ No newline at end of file diff --git a/log.js b/log.js deleted file mode 100644 index ddf5f0f..0000000 --- a/log.js +++ /dev/null @@ -1,47 +0,0 @@ -//external library imports -var Console = require("console"); //access to debug console -var FileWriter = require("simple-file-writer"); //file writer for logging - -//my imports -var Config = require("./config.json"); //config file containing other settings - -var logWriter = new FileWriter(Config.logFile); -var latestLog = ""; - -function log(message) { - if (message && message !== latestLog) { - latestLog = message; //spam reduction - - //attach a formatted date string to the beginning of everything we log - var dateMessage = new Date().toLocaleString() + " " + message; - - Console.log(dateMessage); - logWriter.write(dateMessage + "\n"); - } -} - -function logRaw(obj) { - Console.log(obj); -} - -module.exports = { - info: function (message) { - if (message) - log("[INFO] " + message); - }, - event: function (message, sender) { - //if we received a message, log it - include sender information if it was passed - if (message) { - log("[EVENT] " + (sender ? sender + " has sent an event: " : "") + message); - } - }, - error: function (message, innerEx) { - if (message) { - //log the message, attach innerEx information if it was passed - log("[ERROR] " + message + (innerEx ? ". Inner exception details: " + (innerEx.message || innerEx) : "")); - } - }, - raw: function (obj) { - if (obj) logRaw(obj); - } -}; \ No newline at end of file diff --git a/package.json b/package.json index d8d8011..34266ff 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "discord-feed-bot", "main": "index.js", "scripts": { - "start": "node feed-bot.js", + "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { diff --git a/wrapper/.eslintrc.json b/wrapper/.eslintrc.json new file mode 100644 index 0000000..f67f1c2 --- /dev/null +++ b/wrapper/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "env": { + "browser": true, + "node": true, + "commonjs": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": [ + "warn", + "tab", + { "SwitchCase": 1} + ], + "quotes": [ + "warn", + "double" + ], + "semi": [ + "error", + "always" + ], + "no-undef": "error", + "no-unused-vars": "warn", + "eqeqeq": ["error", "always"] + } +} diff --git a/wrapper/.gitignore b/wrapper/.gitignore new file mode 100644 index 0000000..86d28b9 --- /dev/null +++ b/wrapper/.gitignore @@ -0,0 +1,59 @@ +# Project specific + +token.json + +# Created by https://www.gitignore.io/api/node + +### Node ### +# Logs +logs +*.log +npm-debug.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 + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# 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 + + +# End of https://www.gitignore.io/api/node + +# Project-specific cache for use with shrinkpack - https://github.com/JamieMason/shrinkpack \ No newline at end of file diff --git a/wrapper/.gitrepo b/wrapper/.gitrepo new file mode 100644 index 0000000..6daa8e4 --- /dev/null +++ b/wrapper/.gitrepo @@ -0,0 +1,11 @@ +; 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/shell-discord-bot.git + branch = master + commit = 4946884723f9c66d3c2713f109aab2cf60bd81f4 + parent = 258eff2d63b5804e1e423e9418b117ad4212d8ff + cmdver = 0.3.1 diff --git a/wrapper/.npmrc b/wrapper/.npmrc new file mode 100644 index 0000000..c637686 --- /dev/null +++ b/wrapper/.npmrc @@ -0,0 +1,3 @@ +save=true +sace-exact=true +cache=node_cache diff --git a/wrapper/README.md b/wrapper/README.md new file mode 100644 index 0000000..5b066cb --- /dev/null +++ b/wrapper/README.md @@ -0,0 +1,83 @@ +# Shell Discord bot + +The purpose of this is to act as a shell for other bot modules, so that a single bot user account can be used for a multi-function bot. + +## Setup +- Fork/clone/merge this repo into a new one +- `npm install --save discord.io` +- `npm shrinkwrap --dev` +- `shrinkpack .` +- Create *token.json* with your discord token: `{ "token": "1234567890" }` + +## Creating a bot module + +Interfacing with each bot module is done by the properties in its module.exports. Available properties are: + +| property | property type | parameters | description | +|--------------|---------------|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| onReady | method | none | called on the bot ready event | +| onDisconnect | method | none | called when the bot disconnects | +| onMessage | method | bot, user, userID, channelID, message | called when the bot receives a message - identical to the 'action' property of a command, but triggered on every message (see below) | +| commands | array | N/A | commands a user can invoke - like the onMessage event, but only triggered on expected commands (see below) | + +### Commands + +A command object contains a command, and an action to invoke if the message contains that command. Each command object needs a certain set of properties: + + +| property | optional? | value | description | +|------------|-----------|--------------------------|----------------------------------------------------------------------------------------------------| +| command | required | string | Command to look for in the message | +| type | required | "equals" or "startsWith" | Describes whether we are looking for the message to be the exact command, or just to start with it | +| action | required | function | Callback to invoke if the command is matched (see below) | +| channelIDs | optional | array of strings | If this property is present, the command will only be triggered if sent in one of these channels | +| userIDs | optional | array of strings | If this property is present, the command will only be triggered if made by one of these users | + + +#### Actions + +An action is a callback function called if the specified command is found. It must take these parameters: + +| parameter | type | description | +|-----------|--------|----------------------------------------------------------------------| +| bot | object | The [discord.io](https://github.com/izy521/discord.io) client object | +| user | string | Username of the user who sent the message | +| userID | string | User ID of the user who sent the message | +| channelID | string | Channel ID of the channel the message was sent in | +| message | string | The message/command that was sent | + +Example 1: + +```JavaScript +{ + command: "ping", + type: "equals", + action: (bot, user, userID, channelID, message) => { + bot.sendMessage({ + to: channelID, + message: "pong" + }) + }, + userIDs: ["1234567890"] +} +``` + +The above example will only call *action* if the user with ID 1234567890 sends "ping" + +Example 2: + +```JavaScript +{ + command: "!define", + type: "startsWith", + action: (bot, user, userID, channelID, message) => { + bot.sendMessage({ + to: channelID, + message: getDefinition(message.split(" ")[1]) + }) + } +} +``` + +The above example expects the user to type '!define something', ie only checking for the message to start with '!define'. You are still passed the full message, so can split it up and read it however you want. +*action* will only be called if the message begins with '!define', but has no restrictions on which channel(s) or user(s) use it \ No newline at end of file diff --git a/wrapper/index.js b/wrapper/index.js new file mode 100644 index 0000000..586d6d3 --- /dev/null +++ b/wrapper/index.js @@ -0,0 +1,67 @@ +//node imports +const Console = require("console"); + +//external module imports +var Discord = require("discord.io"); + +var BotModules = [require("../index.js")]; + +var bot; + +var EventHandlers = { + onReady: () => { + Console.info("Registered bot " + bot.username + " with id " + bot.id); + + for (let i = 0, len = BotModules.length; i < len; i++) { + let botModule = BotModules[i]; + if (botModule.onReady) botModule.onReady(bot); + } + }, + onDisconnect: (err, code) => { + Console.error("Bot was disconnected!", err, code); + + for (let i = 0, len = BotModules.length; i < len; i++) { + let botModule = BotModules[i]; + if (botModule.onDisconnect) botModule.onDisconnect(); + } + + bot.connect(); + }, + onMessage: (user, userID, channelID, message) => { + for (let i = 0, iLen = BotModules.length; i < iLen; i++) { + let botModule = BotModules[i]; + + if (botModule.commands) { + for (let j = 0, jLen = botModule.commands.length; j < jLen; j++) { + let messageTrigger = botModule.commands[j]; + + if ((!messageTrigger.channelIDs && !messageTrigger.userIDs) //if we have neither channel nor user restraint, pass + || (messageTrigger.channelIDs && messageTrigger.channelIDs.includes(channelID)) //otherwise, if we have a channel constraint, pass if we're allowed to respond in this channel + || (messageTrigger.userIDs && messageTrigger.userIDs.includes(userID))) //otherwise, if we have a user constraint, pass if we're allowed to respond to this user + switch (messageTrigger.type) { + case "startsWith": + if (message.startsWith(messageTrigger.command)) + messageTrigger.action(bot, user, userID, channelID, message); + break; + default: + if (message === messageTrigger.command) + messageTrigger.action(bot, user, userID, channelID, message); + } + } + } + + if (botModule.onMessage) botModule.onMessage(bot, user, userID, channelID, message); + } + } +}; + +(() => { + bot = new Discord.Client({ + token: require("./token.json").token, + autorun: true + }); + + bot.on("ready", EventHandlers.onReady); + bot.on("disconnect", EventHandlers.onDisconnect); + bot.on("message", EventHandlers.onMessage); +})(); \ No newline at end of file diff --git a/wrapper/npm-shrinkwrap.json b/wrapper/npm-shrinkwrap.json new file mode 100644 index 0000000..1778c1e --- /dev/null +++ b/wrapper/npm-shrinkwrap.json @@ -0,0 +1,4 @@ +{ + "name": "node-boilerplate", + "version": "1.0.0" +} \ No newline at end of file