From a105b34ab53963ab651810f33caa2ee51869b1c7 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Fri, 1 Jun 2012 19:29:08 +0200 Subject: o Initial import. --- .gitignore | 1 + Makefile | 7 ++ lib/arktekk-bot.js | 165 +++++++++++++++++++++++++++++ lib/irc.js | 12 +++ main.js | 75 ++++++++++++++ package.json | 36 +++++++ test/feed.xml | 297 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test/irc.test.js | 11 ++ 8 files changed, 604 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 lib/arktekk-bot.js create mode 100644 lib/irc.js create mode 100644 main.js create mode 100644 package.json create mode 100644 test/feed.xml create mode 100644 test/irc.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb73cae --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +PATH := $(PATH):node_modules/.bin +all: test + +test: + mocha test/*.test.js + +.PHONY: all test diff --git a/lib/arktekk-bot.js b/lib/arktekk-bot.js new file mode 100644 index 0000000..3a00c7f --- /dev/null +++ b/lib/arktekk-bot.js @@ -0,0 +1,165 @@ +/* + * 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. + */ + +var node_irc = require('../node_modules/node-irc/IRC.js') + , cron = require('cron').CronJob + , parser = require('blindparser') + , events = require('events') + , _ = require('underscore'); + +var parserOptions = {}; + +var config; +var cronJobs = []; + +var state = { + channelTopic: undefined, + irc: undefined, + updatingFeed: false, + feeds: [], + newest: { + timestamp: 0, + text: undefined + } +}; +module.exports.state = state; + +var eventEmitter = new events.EventEmitter; + +function startIrc() { + irc = new node_irc.IRC(config.host, config.port); + irc.on('raw', function(data) { console.log(data) }); + irc.on('connected', function(server) { + console.log('Connected to ' + server); + irc.join(config.channel, function(error) { + irc.notice(config.channel, 'well hello yall'); + }); + }); + irc.topic = function(channel, topic) { + irc._socket.write('topic ' + channel + ' :' + topic + '\r\n'); + }; + + irc.on('topic', function(channel, topic) { + console.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; + }); +} + +function updateFeed(feedState) { + console.log("Fetching " + feedState.url); + if(feedState.updatingFeed) + console.log("Already working"); + feedState.updatingFeed = true; + + parser.parseURL(feedState.url, parserOptions, function(err, feed) { +// console.log("Fetched " + feedState.url + ", status=" + (err ? "failure" : "success")); + if(err) { + console.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") { + console.log("feed does not contain any items", 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 + }; +} +module.exports.processFeed = processFeed; + +eventEmitter.on("feedChanged", function(url, newest) { + state.feeds[url] = newest; + + if(state.newest.timestamp >= newest.timestamp) { +// console.log("oold: " + newest.text); + return; + } + + var text = newest.author + ": " + newest.text; + console.log("New topic", newest.timestamp, url, text); + state.newest = newest; + if(config.connect && state.topic != text) { + irc.topic(config.channel, text); + } +}); + +eventEmitter.on("configUpdated", function(c) { + setup(); +}); + +function setup() { + console.log("Stopping " + cronJobs.length + " cron jobs"); + _.each(cronJobs, function(job) { job.stop(); }); + cronJobs = []; + _.each(config.feeds, function(feed) { + var state = { + url: feed, + updatingFeed: false, + last: undefined + }; + var job = new cron("*/10 * * * *", function() { + updateFeed(state); + }, function() {}, true); + cronJobs.push(job); + }); +} + +function start(c) { + config = c; + startIrc(); + setup(); + if(config.connect) { + console.log('Connecting to ' + config.host + ':' + config.port); + irc.connect(config.nick); + } else { + console.log('Not connecting to IRC'); + } +} + +module.exports.start = start; + +module.exports.emit = function(config) { + eventEmitter.emit("configUpdated", config); +} diff --git a/lib/irc.js b/lib/irc.js new file mode 100644 index 0000000..935d2b3 --- /dev/null +++ b/lib/irc.js @@ -0,0 +1,12 @@ +module.exports.nickGenerator = function(nick) { + var nickCount = 0; + var nickAdditions = [ '', '^', '-', '_', '\\', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; + + return function() { + if (nickCount == nickAdditions.length) { + nickCount = 0; + } + + return nick + nickAdditions[nickCount++]; + } +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..700bd5f --- /dev/null +++ b/main.js @@ -0,0 +1,75 @@ +var cmdopt = require('cmdopt') + , os = require('os') + , repl = require('repl') + , _ = require('underscore'); + +var parser = new cmdopt.Parser(); +parser.option("-h, --help", "show help"); +parser.option("-c, --channel=CHANNEL", "IRC channel to join"); +parser.option("-n, --nick=NICK", "Nickname of the bot"); +parser.option("--connect=[yes,no]", "Should the bot connect to IRC").action(function(opts, val) { + opts.connect = val === "yes" +}); +parser.option("--feed=FEED", "A feed to watch").action(function(opts, val) { + if (!opts.feeds) + opts.feeds = []; + opts.feeds.push(val); +}); + +var args = process.argv.slice(2); +var defaults = { + channel: "#hoonk", + feeds: [ +// "http://search.twitter.com/search.atom?q=arktekk", +// "http://search.twitter.com/search.atom?q=from:AgileBorat" +// "http://search.twitter.com/search.atom?q=from:KongenDin" +// "http://search.twitter.com/search.atom?q=from:NestenSivJensen" + "http://search.twitter.com/search.atom?q=awesome", + "http://search.twitter.com/search.atom?q=eurovision", + ], + nick: os.hostname(), + host: "gibson.freenode.net", + port: 6667, + connect: true +}; + +var bots = [{ + name: 'arktekk', + js: './lib/arktekk-bot.js' +}]; + +try { + var config = parser.parse(args); + config = _.defaults(config, defaults); + if (config.help) { + console.log(parser.help()); + console.log("Config:"); + console.log(config); + return; + } + + _.each(bots, function(bot) { + var instance = require(bot.js) + instance.start(config); + bot.instance = instance; + }); + var r = repl.start('irc> '); + r.context.bots = bots; + r.context.config = JSON.parse(JSON.stringify(config)); + // TODO: Make the config per-plugin. + r.context.setConfig = function(c) { + // TODO: store configuration + console.log("Setting config"); +// console.log(c); + _.each(bots, function(bot) { bot.instance.emit("configUpdated", config); }); + config = c; + r.context.config = JSON.parse(JSON.stringify(c)); + return "Config updated"; + } +} catch (ex) { + if (ex instanceof cmdopt.ParseError) { + process.stderr.write(ex.message + "\n"); + process.exit(1); + } + throw ex; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d45104d --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "author": { + "name": "Trygve Laugstøl", + "email": "trygvis@inamo.no", + "url": "https://github.com/trygvis" + }, + "name": "bitraf-irc-client", + "description": "Bitraf's IRC client", + "keywords": [ + "irc", + "chat" + ], + "version": "0.0.1", + "main": "main.js", + "engines": { + "node": ">=0.4.7" + }, + "dependencies": { + "blindparser": "0.0.13" + "cmdopt": "0.2.0", + "cron": "~0.3.2", + "date": "1.0.2", + "getit": "~0.1.7", + "mocha": "~1.0.3", + "node-irc": "https://github.com/einaros/node-irc/tarball/master", + "underscore": "1.3.3", + }, + "devDependencies": {}, + "optionalDependencies": {}, + "licenses": [ + { + "type": "Apache Software License, version 2", + "url": "http://apache.org/licenses/LICENSE-2.0" + } + ] +} diff --git a/test/feed.xml b/test/feed.xml new file mode 100644 index 0000000..113c438 --- /dev/null +++ b/test/feed.xml @@ -0,0 +1,297 @@ + + + tag:search.twitter.com,2005:search/from:AgileBorat OR from:KongenDin + + + from:AgileBorat OR from:KongenDin - Twitter Search + + + 2012-05-26T12:07:25Z + 15 + + + tag:search.twitter.com,2005:206355859759046656 + 2012-05-26T12:07:25Z + + i dag skal jeg ikke kjøpe noe som det ikke står "tutti frutti" på + i dag skal jeg ikke kjøpe noe som det ikke står "tutti frutti" på + 2012-05-26T12:07:25Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:206304970952278017 + 2012-05-26T08:45:12Z + + klarer aldri å huske hvem som er knoll og hvem som er tott, men det er ikke akkurat noe stort problem + klarer aldri å huske hvem som er knoll og hvem som er tott, men det er ikke akkurat noe stort problem + 2012-05-26T08:45:12Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:206274812157505537 + 2012-05-26T06:45:22Z + + Svetlana is offer to explain term of "counter rucking". But Azamat is quite sure she is only hear wrong. + Svetlana is offer to explain term of "counter rucking". But Azamat is quite sure she is only hear wrong. + 2012-05-26T06:45:22Z + + + + recent + + <a href="http://itunes.apple.com/us/app/twitter/id409789998?mt=12" rel="nofollow">Twitter for Mac</a> + en + + AgileBorat (Agile Borat) + http://twitter.com/AgileBorat + + + + tag:search.twitter.com,2005:206273546287194112 + 2012-05-26T06:40:20Z + + Vlad is introduce more of rugby term in Scrum team. But is not sure of how do proper "counter rucking". + Vlad is introduce more of rugby term in Scrum team. But is not sure of how do proper "counter rucking". + 2012-05-26T06:40:20Z + + + + recent + + <a href="http://itunes.apple.com/us/app/twitter/id409789998?mt=12" rel="nofollow">Twitter for Mac</a> + en + + AgileBorat (Agile Borat) + http://twitter.com/AgileBorat + + + + tag:search.twitter.com,2005:206050172109721600 + 2012-05-25T15:52:43Z + + de spanske s-ene er så vriene at jeg sier bare Barfelona jeg + de spanske s-ene er så vriene at jeg sier bare Barfelona jeg + 2012-05-25T15:52:43Z + + + + recent + + <a href="http://twitter.com/">web</a> + da + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:206012501576523778 + 2012-05-25T13:23:02Z + + kongen befaler at man kan ha pinseegg i pinsen. De ligner på påskeegg, men er større. + kongen befaler at man kan ha pinseegg i pinsen. De ligner på påskeegg, men er større. + 2012-05-25T13:23:02Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:205652909323591682 + 2012-05-24T13:34:09Z + + Vlad is introduce more of rugby term in Scrum team. When Azamat is do "dump tackle", database is stay dumped! Is nice. + Vlad is introduce more of rugby term in Scrum team. When Azamat is do "dump tackle", database is stay dumped! Is nice. + 2012-05-24T13:34:09Z + + + + recent + + <a href="http://itunes.apple.com/us/app/twitter/id409789998?mt=12" rel="nofollow">Twitter for Mac</a> + en + + AgileBorat (Agile Borat) + http://twitter.com/AgileBorat + + + + tag:search.twitter.com,2005:205627784033345536 + 2012-05-24T11:54:18Z + + jeg har blitt rød i nakken!!! Har antagelig fått jentelus. + jeg har blitt rød i nakken!!! Har antagelig fått jentelus. + 2012-05-24T11:54:18Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:205584907840393216 + 2012-05-24T09:03:56Z + + hold båtisene deres unna badekaret. de kantrer veldig lett. + hold båtisene deres unna badekaret. de kantrer veldig lett. + 2012-05-24T09:03:56Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:205397941488336896 + 2012-05-23T20:41:00Z + + ett sted går grensen, og denne "Biber" er på feil side av den. HVis dere vil se 18-åringer spankulere kan dere nøye dere med garden!! + ett sted går grensen, og denne "Biber" er på feil side av den. HVis dere vil se 18-åringer spankulere kan dere nøye dere med garden!! + 2012-05-23T20:41:00Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:205307417414742018 + 2012-05-23T14:41:17Z + + ba slottsfrisøren om å klippe meg som Odd nerdrum, men endte opp som ODd einar dørum igjen :-(( + ba slottsfrisøren om å klippe meg som Odd nerdrum, men endte opp som ODd einar dørum igjen :-(( + 2012-05-23T14:41:17Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:205295504064655360 + 2012-05-23T13:53:57Z + + når jeg ser en dåp liker jeg å tenke at babyen er et glass med melk og at døpevannet er o'boy!! + når jeg ser en dåp liker jeg å tenke at babyen er et glass med melk og at døpevannet er o'boy!! + 2012-05-23T13:53:57Z + + + + recent + + <a href="http://twitter.com/">web</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:204881566177050625 + 2012-05-22T10:29:06Z + + I am wish I have one dollar for every smiling Scrum Master I am meet! Then I would have $0.50. + I am wish I have one dollar for every smiling Scrum Master I am meet! Then I would have $0.50. + 2012-05-22T10:29:06Z + + + + recent + + <a href="http://itunes.apple.com/us/app/twitter/id409789998?mt=12" rel="nofollow">Twitter for Mac</a> + en + + AgileBorat (Agile Borat) + http://twitter.com/AgileBorat + + + + tag:search.twitter.com,2005:204526346481840128 + 2012-05-21T10:57:35Z + + Ari sier at det er ku og gris inni pølsene!!! ÆÆÆÆSJ!! + Ari sier at det er ku og gris inni pølsene!!! ÆÆÆÆSJ!! + 2012-05-21T10:57:35Z + + + + recent + + <a href="http://twitter.com/">web</a> + da + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + + tag:search.twitter.com,2005:204296372051132416 + 2012-05-20T19:43:45Z + + det er ikke vanskelig å dunke i basketball. Med utendørstrampoline + det er ikke vanskelig å dunke i basketball. Med utendørstrampoline + 2012-05-20T19:43:45Z + + + + recent + + <a href="http://twitter.com/#!/download/iphone" rel="nofollow">Twitter for iPhone</a> + no + + KongenDin (Harald Rex) + http://twitter.com/KongenDin + + + diff --git a/test/irc.test.js b/test/irc.test.js new file mode 100644 index 0000000..af89a81 --- /dev/null +++ b/test/irc.test.js @@ -0,0 +1,11 @@ +var assert = require('assert'), + irc = require('../lib/irc.js'); + +describe('irc.js', function() { + it('work', function() { + var nick = irc.nickGenerator("foo"); + assert.equal("function", typeof nick); + assert.equal("foo", nick()); + assert.equal("foo^", nick()); + }); +}); -- cgit v1.2.3