Merge branch 'release/v1.2.0'

Conflicts:
	CHANGELOG.md
	feed-bot.js
This commit is contained in:
benji7425 2017-01-08 17:03:00 +00:00
commit b13ffebdc4
11 changed files with 212 additions and 88 deletions

View File

@ -23,6 +23,7 @@
"always"
],
"no-undef": "error",
"no-unused-vars": "warn"
"no-unused-vars": "warn",
"eqeqeq": ["error", "always"]
}
}

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
log
subscribers.json
# Created by https://www.gitignore.io/api/visualstudiocode,node

View File

@ -1,5 +1,20 @@
# Changelog
## 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

View File

@ -3,19 +3,9 @@
- Posts latest link from RSS feed into specified Discord channel
- Configurable polling interval
- Doesn't post the link if it has already been posted in last 100 messages
- YouTube mode - detects both youtube.com and youtu.be links, and doesn't post again if *either* have already been posted (BETA)
- YouTube mode - detects both youtube.com and youtu.be links, and doesn't post again if *either* have already been posted
- Subscription - users can subscribe and have their username tagged when a new link is posted
Please view the github wiki for details regarding installation and commands for interacting with the bot
Feel free to contact me with suggestions and feature requests - if you need a new feature, just let me know and I will see what I can do! (No promises though :p)
# Installation
1. Make sure you have nodejs (v6+) and npm installed
2. Download the zip from [releases](https://github.com/benji7425/discord-feed-bot/releases) and extract
3. Open a terminal in extracted folder
4. Run `npm install` and wait for it to finish
5. Edit *config.json* to include your RSS feed and channel ID
6. Create *bot-config.json* to include your bot token:
`{
"token": "abc123blahblahblahyourtokengoeshere"
}`
7. Run `node feed-bot.js`

View File

@ -3,5 +3,12 @@
"channelID": "264420391282409473",
"pollingInterval": 5000,
"numLinksToCache": 10,
"youtubeMode": true
"youtubeMode": true,
"logRequestMessage": "!logsplease",
"subscribeRequestMessage": "!subscribe",
"unsubscribeRequestMessage": "!unsubscribe",
"subscribersListRequestMessage": "!sublist",
"helpRequestMessage": "!help",
"logFile": "./log",
"subscribersFile": "./subscribers.json"
}

3
docs/home.md Normal file
View File

@ -0,0 +1,3 @@
If browsing on GitHub, please use the sidebar on the right to navigate the wiki.
Please use releases from the 'releases' page when installing rather than just cloning the repository yourself

10
docs/user/commands.md Normal file
View File

@ -0,0 +1,10 @@
# Commands
There is a very basic level of interaction with the bot available via chat commands, all of which are prefixed with an exclamation mark '!'
| command | action |
|--------------|-----------------------------------------------------------------------------------------|
| !help | list available commands |
| !subscribe | subscribe to feed notifications, so your username gets tagged when a new link is posted |
| !unsubscribe | unsubscribe from feed notifications |
| !sublist | view list of subscribed users |

12
docs/user/installation.md Normal file
View File

@ -0,0 +1,12 @@
# Installation
1. Make sure you have nodejs (v6+) and npm installed
2. Download the zip from [releases](https://github.com/benji7425/discord-feed-bot/releases) and extract
3. Open a terminal in extracted folder
4. Run `npm install` and wait for it to finish
5. Edit *config.json* to include your RSS feed and channel ID
6. Create *bot-config.json* to include your bot token:
`{
"token": "abc123blahblahblahyourtokengoeshere"
}`
7. Run `node feed-bot.js`

View File

@ -4,6 +4,7 @@ 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
@ -12,8 +13,6 @@ var Config = require("./config.json"); //config file containing other settings
var DiscordClient = {
bot: null,
feedInterval: null,
reconnectInterval: null,
startup: function () {
//check if we can connect to discordapp.com to authenticate the bot
Dns.resolve("discordapp.com", function (err) {
@ -36,31 +35,60 @@ var DiscordClient = {
onReady: function () {
Log.info("Registered/connected bot " + DiscordClient.bot.username + " - (" + DiscordClient.bot.id + ")");
clearInterval(DiscordClient.reconnectInterval);
Log.info("Setting up timer to check feed every " + Config.pollingInterval + " milliseconds");
DiscordClient.feedInterval = setInterval(Feed.checkAndPost, Config.pollingInterval); //set up the timer to check the feed
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
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");
clearInterval(DiscordClient.feedInterval)
DiscordClient.reconnectInterval = setInterval(function () {
DiscordClient.startup();
}, (Config.reconnectInterval || Config.pollingInterval));
intervalFunc = DiscordClient.startup; //reassign the interval function to try restart the bot every 5 sec
},
onMessage: function (user, userID, channelID, message) {
//check if the message is in the right channel, contains a link, and is not the latest link from the rss feed
if (channelID === Config.channelID && Links.messageContainsLink(message) && (message !== Links.latestFromFeed)) {
Log.event("Detected posted link in this message: " + message, "Discord.io");
if (channelID === Config.channelID) {
//contains a link, and is not the latest link from the rss feed
if (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;
//extract the url from the string, and cache it
Uri.withinString(message, function (url) {
Links.cache(Links.standardise(url));
return url;
});
}
else {
switch (message) {
case Config.subscribeRequestMessage:
Subscriptions.subscribe(channelID, userID, user);
break;
case Config.unsubscribeRequestMessage:
Subscriptions.unsubscribe(channelID, userID, user);
break;
case Config.subscribersListRequestMessage:
DiscordClient.bot.sendMessage({
to: Config.channelID,
message: DiscordClient.bot.fixMessage("<@" + Subscriptions.subscribers.join("> <@") + ">")
});
break;
case Config.helpRequestMessage:
DiscordClient.bot.sendMessage({
to: Config.channelID,
message: Config.subscribeRequestMessage + ", " + Config.unsubscribeRequestMessage + ", " + Config.subscribersListRequestMessage
});
}
}
}
else if (message === Config.logRequestMessage) {
DiscordClient.bot.uploadFile({
to: channelID,
file: Config.logFile
}, (err, message) => {
if (err) Log.error("Failed to upload log file: " + message, err);
else Log.event("Uploaded log file for user " + user + "(" + userID + ")");
});
}
},
@ -91,6 +119,60 @@ var DiscordClient = {
}
}
});
},
post: function (link) {
var tags = "";
for (var userID in Subscriptions.subscribers)
tags += "<@" + Subscriptions.subscribers[userID] + "> ";
//send a messsage containing the new feed link to our discord channel
DiscordClient.bot.sendMessage({
to: Config.channelID,
message: tags + link
}, function (err, message) {
if (err) {
Log.error("ERROR: Failed to send message" + message ? message : "", err);
//if there is an error posting the message, check if it is because the bot isn't connected
if (!DiscordClient.bot.connected) DiscordClient.onDisconnect();
}
});
}
};
var Subscriptions = {
subscribers: [],
parse: function () {
JsonFile.readFile(Config.subscribersFile, (err, obj) => {
if (err) Log.error("Unable to parse json subscribers file", err);
this.subscribers = obj || [];
});
},
subscribe: function (channelID, userID, user) {
if (this.subscribers.indexOf(userID) === -1) {
this.subscribers.push(userID); //subscribe the user if they aren't already subscribed
this.writeToFile();
Log.event("Subscribed user " + (user ? user + "(" + userID + ")" : userID));
DiscordClient.bot.sendMessage({
to: channelID,
message: "You have successfully subscribed"
});
}
},
unsubscribe: function (channelID, userID, user) {
if (this.subscribers.indexOf(userID) > -1) {
this.subscribers.splice(this.subscribers.indexOf(userID));
this.writeToFile();
Log.event("Unsubscribed user " + (user ? user + "(" + userID + ")" : userID));
DiscordClient.bot.sendMessage({
to: channelID,
message: "You have successfully unsubscribed"
});
}
},
writeToFile: function () {
JsonFile.writeFile(Config.subscribersFile, this.subscribers, (err) => { if (err) Log.error("Unable to write subscribers to json file", err); });
}
};
@ -98,14 +180,13 @@ var YouTube = {
url: {
share: "http://youtu.be/",
full: "http://www.youtube.com/watch?v=",
convertShareToFull: function (shareUrl) {
createFullUrl: function (shareUrl) {
return shareUrl.replace(YouTube.url.share, YouTube.url.full);
},
convertFullToShare: function (fullUrl) {
createShareUrl: function (fullUrl) {
var shareUrl = fullUrl.replace(YouTube.url.full, YouTube.url.share);
if (shareUrl.includes("&"))
shareUrl = shareUrl.slice(0, fullUrl.indexOf("&"));
if (shareUrl.includes("&")) shareUrl = shareUrl.slice(0, fullUrl.indexOf("&"));
return shareUrl;
}
@ -123,81 +204,68 @@ var Links = {
return messageLower.includes("http://") || messageLower.includes("https://") || messageLower.includes("www.");
},
cached: [],
latestFromFeed: "",
latestFeedLink: "",
cache: function (link) {
link = Links.standardise(link);
if (Config.youtubeMode && link.includes(YouTube.url.full)) {
link = YouTube.url.convertFullToShare(link);
}
if (Config.youtubeMode) link = YouTube.url.createShareUrl(link);
//store the new link if not stored already
if (!Links.checkCache(link)) {
if (!Links.isCached(link)) {
Links.cached.push(link);
Log.info("Cached URL: " + link);
}
//get rid of the first array element if we have reached our cache limit
if (Links.cached.length > (Config.numLinksToCache || 10))
Links.cached.shift();
if (Links.cached.length > Config.numLinksToCache) Links.cached.shift(); //get rid of the first array element if we have reached our cache limit
},
checkCache: function (link) {
isCached: function (link) {
link = Links.standardise(link);
if (Config.youtubeMode && link.includes(YouTube.url.full)) {
return Links.cached.includes(YouTube.url.convertFullToShare(link));
}
if (Config.youtubeMode)
return Links.cached.includes(YouTube.url.createShareUrl(link));
return Links.cached.includes(link);
},
validateAndPost: function (err, articles) {
validate: function (err, articles, callback) {
if (err) Log.error("FEED ERROR: Error reading RSS feed.", err);
else {
var latestLink = Links.standardise(articles[0].link); //get the latest link and check if it has already been posted and cached
var latestLink = Links.standardise(articles[0].link);
if (Config.youtubeMode) latestLink = YouTube.url.createShareUrl(latestLink);
//check whether the latest link out the feed exists in our cache
if (!Links.checkCache(latestLink)) {
if (Config.youtubeMode && latestLink.includes(YouTube.url.full))
latestLink = YouTube.url.convertFullToShare(latestLink);
Log.info("Attempting to post new link: " + latestLink);
//make sure we don't spam the latest link
if (latestLink === Links.latestFeedLink)
return;
//send a messsage containing the new feed link to our discord channel
DiscordClient.bot.sendMessage({
to: Config.channelID,
message: latestLink
}, function (err, message) {
if (err) {
Log.error("ERROR: Failed to send message: " + message.substring(0, 15) + "...", err);
//if there is an error posting the message, check if it is because the bot isn't connected
if (DiscordClient.bot.connected)
Log.info("Connectivity seems fine - I have no idea why the message didn't post");
else {
Log.error("DiscordClient appears to be disconnected! Attempting to reconnect...", err);
DiscordClient.bot.connect(); //attempt to reconnect
}
}
});
Links.cache(latestLink); //finally make sure the link is cached, so it doesn't get posted again
//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 if (Links.latestFromFeed != latestLink)
Log.info("Didn't post new feed link because already detected as posted " + latestLink); //alternatively, if we have a new link from the feed, but its been posted already, just alert the console
else {
callback(latestLink);
Links.latestFromFeed = latestLink; //ensure our latest feed link variable is up to date, so we can track when the feed updates
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),
checkAndPost: function () {
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, Links.validateAndPost);
else FeedRead(Config.feedUrl, callback);
});
}
};
var intervalFunc = () => { }; //do nothing by default
//IIFE to kickstart the bot when the app loads
(function () {
Subscriptions.parse();
DiscordClient.startup();
setInterval(() => { intervalFunc(); }, Config.pollingInterval);
})();

26
log.js
View File

@ -1,26 +1,40 @@
var console = require("console");
//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)
if (message && message !== latestLog) {
latestLog = message; //spam reduction
//attach a formatted date string to the beginning of everything we log
console.log(new Date().toLocaleString() + " " + message);
var dateMessage = new Date().toLocaleString() + " " + message;
Console.log(dateMessage);
logWriter.write(dateMessage + "\n");
}
}
module.exports = {
info: function (message) {
if (message)
log("INFO: " + 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);
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) : ""));
log("[ERROR] " + message + (innerEx ? ". Inner exception details: " + (innerEx.message || innerEx) : ""));
}
}
};

View File

@ -19,6 +19,8 @@
"dependencies": {
"discord.io": "^2.2.4",
"feed-read": "0.0.1",
"simple-file-writer": "^2.0.0",
"jsonfile": "^2.4.0",
"urijs": "^1.18.2"
}
}