/* * Possible strategies for updating the topic: * * o Set the topic unconditionally when the feed changes. This makes * it possible for users to change the topic and it won't be * overridden until the feed changes. * * o Set the topic on any topic change (making the feed control the * entire topic) * * o Support a delimiter so it can control only a part of the topic, * like "<>". Example * * Next meeting, sat 1900 <> DATA FROM FEED. * * A regexp selecting the are to be updated might conver it. * * o If the bot changed the topic the last time, it's probably safe to * just update it. */ require('tinycolor'); var cron = require('cron').CronJob , events = require('events') , fs = require('fs') , parser = require('blindparser') , url = require('url') , _ = require('underscore'); var Proxy = require('../node_modules/dynobot/proxy'); var Channel = require('../node_modules/dynobot/channel'); var irc = new Proxy(require('../irc-client.js').IrcClient.prototype, 'irc', new Channel()); function log(message) { console.log(('log ' + message).green); } var config; var state = { channelTopic: undefined, topic: undefined, newest: { timestamp: 0, text: undefined } }; var feeds = {}; function FeedState(url) { this.url = url; this.updatingFeed = false; this.last = undefined; this.job = undefined; } var eventEmitter = new events.EventEmitter; function parseFeed(url, cb) { var parserOptions = {}; parser.parseURL(url, parserOptions, cb); } function updateFeed(feedState) { log("Fetching " + feedState.url); if(feedState.updatingFeed) log("Already working"); feedState.updatingFeed = true; parseFeed(feedState.url, function(err, feed) { log("Fetched " + feedState.url + ", status=" + (err ? "failure" : "success")); if(err) { log(err); return; } var newest = processFeed(feed); if(typeof newest == "object") { eventEmitter.emit("feedChanged", feedState.url, newest); } feedState.updatingFeed = false; }); } function processFeed(feed) { // Extracts the username from the feed. // TODO: Use something better than blindparser to parse atom so that // each entry has an author too to get the full name. if(typeof feed.items[0] == "undefined") { log("feed does not contain any items"); log(feed); return undefined; } var match = /^http:\/\/twitter.com\/([a-zA-Z0-9_]+)\/.*$/.exec(feed.items[0].link) if(match.length != 2) { return undefined; } return { text: feed.items[0].title, author: match[1], timestamp: feed.items[0].date }; } eventEmitter.on("feedChanged", function(url, newest) { feeds[url].newest = newest; if(state.newest.timestamp >= newest.timestamp) { log("oold: " + newest.text); return; } /* */ var text = newest.author + ": " + newest.text; log("New topic: " + newest.timestamp + ", url=" + url + ", text=" + text); state.newest = newest; /* if(state.channelTopic != text) { irc.topic(config.channel, text); } */ log(config.channel); irc.notice(config.channel, text); }); function appendFeed(feedUrl) { var feedState = new FeedState(feedUrl); log("Job starting for " + feedUrl); feedState.job = new cron("*/10 * * * *", function() { updateFeed(feedState); }, function() { log("Job stopping for " + feedUrl); }, true); feeds[feedUrl] = feedState; log("appendFeed: keys=" + _.keys(feeds)); } function removeFeed(feedUrl) { log("removeFeed: feedUrl=" + feedUrl); log("removeFeed: keys=" + _.keys(feeds)); var feedState = feeds[feedUrl]; log("removeFeed: feedState=" + typeof feedState); if(_.isObject(feedState)) { log("removeFeed: keys feedState=" + _.keys(feedState)); feedState.job.stop(); } else { log("removeFeed: feed not found"); } delete feeds[feedUrl]; log("removeFeed: keys=" + _.keys(feeds)); } function setup() { log('Starting..'); loadConfig(function(err, config) { log('config: ' + JSON.stringify(config)); irc.join(config.channel, function(c) { irc.notice(config.channel, 'Atom plugin online. Monitoring ' + config.feeds.length + ' feeds.'); }); _.each(config.feeds, function(feed) { appendFeed(feed); }); }); } irc.on('topic', function(channel, topic) { log("new topic: " + topic); /* If we're not storing this, it is possible for people to set * the topic after the bot has set it and it will persist (until * next update from the feed). topic = t; */ state.channelTopic = topic; }); irc.on('privmsg', function(nick, channel, message) { irc.whoami(function(whoami) { // Hm, what happens if a nick contain funny characters like '. var reg = new RegExp('^' + whoami + ': atom '); if(!reg.test(message)) { return; } var args = message.substring(whoami.length + 7).split(' '); if(args.length < 1) { usage(channel); return; } var rest = _.rest(args); switch(args[0]) { case 'list-feeds': onListFeeds(channel); break; case 'add-feed': onAddFeed(channel, rest); break; case 'remove-feed': onRemoveFeed(channel, rest); break; case 'load-config': onLoadConfig(channel); break; } }); }); function usage(channel) { } function onListFeeds(channel) { _.each(config.feeds, function(feed, i) { irc.notice(channel, 'feed #' + i + ': ' + feed); }); } function onAddFeed(channel, urls) { if(urls.length != 1) { return; } var u = urls[0]; log('Adding feed: ' + u); url.parse(u); // TODO: Add error handling parseFeed(u, function(err, feed) { if(err) { log('Unable to fetch feed'); log(util.format(err)); return; } config.feeds.push(u); appendFeed(u); saveConfig(function(err) { if(err) throw err; irc.notice(channel, 'Feed added'); }); }); } function onRemoveFeed(channel, args) { if(args.length != 1) { return; } log("onRemoveFeed: args[0]=" + args[0]); var i = parseInt(args[0]); log("onRemoveFeed: i=" + i); var feedUrl = config.feeds[i]; log("onRemoveFeed: config=" + JSON.stringify(config)); config.feeds.splice(i, 1); log("onRemoveFeed: config=" + JSON.stringify(config)); log("onRemoveFeed: feedUrl=" + feedUrl); removeFeed(feedUrl); saveConfig(function(err) { if(err) { log(err); irc.notice(channel, 'Error removing feed'); return; } irc.notice(channel, 'Feed removed'); }); } function onLoadConfig(channel) { loadConfig(function(err, config) { if(err) { irc.notice(channel, 'Unable to load config'); } else { irc.notice(channel, 'Loaded configuration, has ' + config.feeds.length + ' feeds.'); } }); } function loadConfig(cb) { fs.readFile('config.json', function(err, data) { if(err) return cb(err, undefined); try { config = JSON.parse(data); } catch(e) { log('Unable to parse config.json, using defaults.'); } config.channel = _.isString(config.channel) ? config.channel : '#dynobot'; config.feeds = _.isArray(config.feeds) ? config.feeds : []; cb(undefined, config); }); } function saveConfig(cb) { log('Saving config: ' + JSON.stringify(config)); fs.writeFile('config.json', JSON.stringify(config), cb); } setup();