├── .gitignore ├── settings.js.example ├── README.textile └── bot.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | tmp/* 4 | settings.js 5 | -------------------------------------------------------------------------------- /settings.js.example: -------------------------------------------------------------------------------- 1 | { "server": "irc.fa.gs", 2 | "channel": "#test", 3 | "nick": "djsbot", 4 | 5 | "mongo": { 6 | "uri": "mongodb://localhost:27017/db" 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | Logs messages to Mongo and catches links, for irc.freenode.net, #dailyjs. 2 | 3 | h3. Requirements 4 | 5 | * "Mongoose":http://github.com/LearnBoost/mongoose 6 | * "Node IRC":http://github.com/martynsmith/node-irc 7 | 8 | h3. Installation 9 | 10 | Copy settings.js.example to settings.js and edit the settings. 11 | 12 | h3. Usage 13 | 14 | At some point I'll add privmsg help and link searching, and help for that will appear here. 15 | 16 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | // _ _ _ _ _ _ 2 | // __| | __ _(_) |_ _ (_)___ | |__ ___ | |_ 3 | // / _` |/ _` | | | | | || / __|_____| '_ \ / _ \| __| 4 | // | (_| | (_| | | | |_| || \__ \_____| |_) | (_) | |_ 5 | // \__,_|\__,_|_|_|\__, |/ |___/ |_.__/ \___/ \__| 6 | // |___/__/ 7 | 8 | var irc = require('irc'), 9 | sys = require('sys'), 10 | fs = require('fs'), 11 | http = require('http'), 12 | net = { 13 | url: require('url'), 14 | }, 15 | settings, 16 | mongoose = require('mongoose').Mongoose, 17 | client, 18 | LinkCatcher, 19 | settings, 20 | db, 21 | Commands, 22 | Link, 23 | Message; 24 | 25 | try { 26 | settings = JSON.parse(fs.readFileSync('settings.js').toString()); 27 | } catch (exception) { 28 | sys.puts('Please ensure you have a valid settings.js file.'); 29 | process.exit(1); 30 | } 31 | 32 | db = mongoose.connect(settings.mongo.uri); 33 | 34 | // DB models 35 | mongoose.model('Link', { 36 | properties: ['url', 'title', 'nick', 'channel', 'server', 'updated_at', 'count'], 37 | methods: { 38 | save: function(fn) { 39 | this.updated_at = new Date(); 40 | this.__super__(fn); 41 | }, 42 | 43 | increment: function() { 44 | this.count += 1; 45 | this.save(); 46 | } 47 | }, 48 | indexes: ['url', [{ url: 1 }, { unique: true }]] 49 | }); 50 | 51 | mongoose.model('Message', { 52 | properties: ['message', 'nick', 'channel', 'server', 'updated_at'], 53 | methods: { 54 | save: function(fn) { 55 | this.updated_at = new Date(); 56 | this.__super__(fn); 57 | } 58 | } 59 | }); 60 | 61 | Link = db.model('Link'); 62 | Message = db.model('Message'); 63 | 64 | // Link collections 65 | LinkCatcher = { 66 | matcher: /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/, 67 | 68 | match: function(text) { 69 | var matches = text.match(this.matcher); 70 | if (matches && matches.length > 0) { 71 | return matches[0]; 72 | } 73 | }, 74 | 75 | title: function(urlString, fn) { 76 | var redirects = 0; 77 | 78 | function get(urlString) { 79 | if (redirects > 4) return; 80 | 81 | var url = net.url.parse(urlString), 82 | connection = http.createClient(url.port || 80, url.host, url.protocol === 'https:'), 83 | request; 84 | 85 | url.pathname = url.pathname || '/'; 86 | url.search = url.search || ''; 87 | request = connection.request('GET', url.pathname + url.search, { 88 | 'host': url.host, 89 | 'Accept-Ranges': '0..2046' 90 | }); 91 | 92 | sys.puts('Fetching: ' + url.host + url.pathname + url.search); 93 | request.end(); 94 | request.on('response', function(response) { 95 | var data = '', 96 | title = '', 97 | ended = false; 98 | 99 | if (response.statusCode >= 300 && response.statusCode < 400) { 100 | redirects += 1; 101 | request.end(); 102 | return get(response.headers.location, request); 103 | } 104 | 105 | response.on('data', function(chunk) { 106 | if (ended) return; 107 | 108 | data += chunk; 109 | 110 | if (data.match(//)) { 111 | title = data.match(/<title>([^<]*)<\/title>/); 112 | if (title) { 113 | fn(title[1]); 114 | response.socket.end(); 115 | data = null; 116 | ended = true; 117 | return; 118 | } 119 | } 120 | 121 | // I want to only download x bytes 122 | if (data.length > 2046) { 123 | response.socket.end(); 124 | ended = true; 125 | } 126 | }); 127 | }); 128 | } 129 | 130 | get(urlString); 131 | } 132 | }; 133 | 134 | // IRC commands 135 | Commands = { 136 | publicCommands: ['search', 'help'], 137 | 138 | match: function(text) { 139 | for (var i = 0; i < this.publicCommands.length; i++) { 140 | var command = this.publicCommands[i], 141 | matches = text.split(new RegExp('^`(' + command + ')\\s+(.*)', 'i')); 142 | 143 | if (matches && matches.length > 1) { 144 | return { name: matches[1], args: matches[2] }; 145 | } 146 | 147 | matches = text.split(new RegExp('^`(' + command + ')', 'i')); 148 | if (matches.length > 1) { 149 | return { name: matches[1], args: null }; 150 | } 151 | } 152 | }, 153 | 154 | search: function(commandSpec, from, to, message) { 155 | if (!commandSpec.args) return; 156 | 157 | // I can't get Mongoose to do '$or' for some reason 158 | Link.find({ title: (new RegExp(commandSpec.args, 'i')) }) 159 | .limit(4) 160 | .sort('updated_at', 1) 161 | .all(function(result) { 162 | result.forEach(function(link) { 163 | client.say(settings.channel, 'Found link: ' + link.url); 164 | }); 165 | 166 | if (result.length === 0) { 167 | client.say(settings.channel, 'No matches found'); 168 | } 169 | }); 170 | }, 171 | 172 | help: function(commandSpec, to, from, message) { 173 | client.say(settings.channel, 'I can only do `search phrase right now because Alex sucks'); 174 | } 175 | }; 176 | 177 | // IRC client 178 | client = new irc.Client(settings.server, 'djsbot', { 179 | channels: [settings.channel], 180 | debug: true 181 | }); 182 | 183 | client.addListener('raw', function(message) { 184 | sys.puts(message.command + ' ' + message.args.join(',')); 185 | }); 186 | 187 | client.addListener('message', function(from, to, message) { 188 | var commandSpec; 189 | if (commandSpec = Commands.match(message)) { 190 | Commands[commandSpec.name](commandSpec, from, to, message); 191 | } 192 | }); 193 | 194 | // rfc1459 195 | function sanitize(message) { 196 | // Remove: NUL (0x0), CR (0xd), and LF (0xa) 197 | return message.replace(/[�\n\r]/g, ''); 198 | } 199 | 200 | function say(message) { 201 | client.say(settings.channel, sanitize(message)); 202 | } 203 | 204 | client.addListener('message', function(from, to, message) { 205 | (new Message({ message: message, 206 | nick: from, 207 | channel: to, 208 | server: settings.server })).save(); 209 | 210 | // TODO: Multiple links 211 | var url; 212 | if (url = LinkCatcher.match(message)) { 213 | Link.find({ url: url }).first(function(link) { 214 | if (!link) { 215 | link = new Link({ url: url, 216 | nick: from, 217 | channel: to, 218 | count: 1, 219 | server: settings.server }); 220 | link.save(); 221 | say('Saving link: ' + url); 222 | } else { 223 | link.increment(); 224 | say('Seen: ' + url + ' ' + link.count + ' times, posted by: ' + link.nick); 225 | } 226 | LinkCatcher.title(url, function(title) { 227 | link.title = title; 228 | link.save(); 229 | say('Title: ' + title); 230 | }); 231 | }); 232 | } 233 | }); 234 | 235 | --------------------------------------------------------------------------------