Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial check in of open sourced code

  • Loading branch information...
commit 09c626b852bb53582fd603b048d41a032f00ff6e 0 parents
@Sutto authored
29 Cakefile
@@ -0,0 +1,29 @@
+fs = require 'fs'
+path = require 'path'
+{spawn, exec} = require 'child_process'
+
+# Compiles source files into the lib folder
+run = (args, cb) ->
+ proc = spawn 'coffee', args
+ proc.stderr.on 'data', (buffer) -> console.log buffer.toString()
+ proc.on 'exit', (status) ->
+ process.exit(1) if status != 0
+ cb() if typeof cb is 'function'
+
+buildUnder = (input, output) ->
+ files = fs.readdirSync input
+ files = (input + '/' + file for file in files when file.match(/\.coffee$/))
+ run ['-c', '-o', output].concat(files)
+
+task 'build:library', 'builds the library from coffee-script library', bl = ->
+ console.log "Building the private server JavaScript..."
+ buildUnder 'src', 'lib'
+
+task 'build:client', 'builds the client from coffee-script library', bc = ->
+ console.log "Building the public client JavaScript..."
+ buildUnder 'public/coffeescripts', 'public/javascripts'
+
+task 'build', 'builds both the library and the client', ->
+ console.log "Building all..."
+ bl()
+ bc()
51 README.md
@@ -0,0 +1,51 @@
+# Chainsaw App v2
+
+A complete rewrite of the old convoluted chainsaw codebase to make it:
+
+* Cleaner in terms of code
+* Simpler to run (One process, versus many)
+* Easy to extend (based on a simple publisher model)
+
+With the whole goal of making real time event streams (pulling events
+from external sources) as simple as possible.
+
+Originally built for [Rails Rumble 2010](http://www.railsrumble.com/) like
+I built prior versions for earlier rumbles.
+
+## Getting Started
+
+First, make sure you have [node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed.
+
+### Configuration
+
+Chainsaw uses a standard `config.json` file which uses nested keys according to:
+
+* Chainsaw Settings (`chainsaw`)
+* Redis settings (`redis`)
+* Publisher settings (either the publisher name or the `configNamespace` value on a given publisher).
+
+An example of this can be seen in the `config.example.json` file which contains **all** options.
+Please note that most options are optional.
+
+Lastly, in the case of the following, they can also be overridden by an environment variable:
+
+* `chainsaw.listen.host` (by `HOST`)
+* `chainsaw.listen.port` (by `PORT`)
+* `chainsaw.redis.host` (by `REDIS_HOST`)
+* `chainsaw.redis.port` (by `REDIS_HOST`)
+* `chainsaw.redis.password` (by `REDIS_PASSWORD`)
+* `chainsaw.redis.maxHistory` (by `REDIS_MAXHISTORY`)
+
+### The Public JavasScript Portion
+
+Please note the public portion can be found in `public/` and that
+your application uses Chainsaw. It'd be pretty simple to port it to use
+something other than jQuery but that is left as an exercise for the reader.
+
+## Developing Chainsaw
+
+To develop chainsaw, you'll also need to install coffee-script via:
+
+```bash
+npm install coffee-script
+````
32 config.example.json
@@ -0,0 +1,32 @@
+{
+ "chainsaw": {
+ "listen": {
+ "host": "localhost",
+ "port": 3004
+ },
+ "enabled": ["twitter", "irc"]
+ },
+ "redis": {
+ "host": "localhost",
+ "namespace": "chainsaw",
+ "password": "something",
+ "port": 6379,
+ "maxHistory": 10
+ },
+ "twitter": {
+ "consumer": {
+ "key": "your-consumer-key",
+ "secret": "your-consumer-secret"
+ },
+ "access_token": {
+ "key": "oauth-key-goes-here",
+ "secret": "oauth-secret-goes-here"
+ },
+ "track": ["a", "b", "c"]
+ },
+ "irc": {
+ "server": "irc.freenode.net",
+ "user": "chainsawv2",
+ "channels": ["#railsrumble"]
+ }
+}
48 lib/base.js
@@ -0,0 +1,48 @@
+(function() {
+ var Base, sys;
+ sys = require('sys');
+ Base = (function() {
+ Base.prototype.name = "unknown";
+ function Base(runner) {
+ var _ref, _ref2;
+ this.runner = runner;
+ if ((_ref = this.configNamespace) != null) {
+ _ref;
+ } else {
+ this.configNamespace = this.name;
+ };
+ if ((_ref2 = this.namespace) != null) {
+ _ref2;
+ } else {
+ this.namespace = this.name;
+ };
+ }
+ Base.prototype.isEnabled = function() {
+ return this.runner.get('chainsaw.enabled', []).indexOf(this.name) > -1;
+ };
+ Base.prototype.run = function() {
+ if (this.isEnabled()) {
+ sys.puts("Starting publisher: " + this.name);
+ return this.setup();
+ }
+ };
+ Base.prototype.get = function(key, defaultValue) {
+ return this.runner.get("" + this.configNamespace + "." + key, defaultValue);
+ };
+ Base.prototype.config = function() {
+ return this.runner.get(this.configNamespace);
+ };
+ Base.prototype.emit = function(key, message) {
+ if (message != null) {
+ key = "" + this.namespace + ":" + key;
+ } else {
+ message = key;
+ key = this.namespace;
+ }
+ this.runner.broadcast.sockets.emit(key, message);
+ return this.runner.redis.addHistory(key, JSON.stringify(message));
+ };
+ return Base;
+ })();
+ exports.Base = Base;
+}).call(this);
77 lib/chainsaw.js
@@ -0,0 +1,77 @@
+(function() {
+ var Chainsaw, fs, io, path, redis, sys, web;
+ path = require('path');
+ sys = require('sys');
+ fs = require('fs');
+ io = require('socket.io');
+ redis = require('./redis').RedisWrapper;
+ web = require('./web').Web;
+ Chainsaw = (function() {
+ function Chainsaw(config) {
+ this.config = config;
+ this.publishers = [];
+ }
+ Chainsaw.prototype.add = function(publisher) {
+ return this.publishers.push(new publisher(this));
+ };
+ Chainsaw.prototype.addFromRequire = function(path) {
+ return this.add(require(path).publisher);
+ };
+ Chainsaw.prototype.run = function() {
+ var publisher, _i, _len, _ref;
+ sys.puts("Starting chainsaw...");
+ this.redis = new redis(this);
+ this.web = new web(this);
+ this.broadcast = io.listen(this.web.app);
+ this.broadcast.set('log level', 0);
+ _ref = this.publishers;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ publisher = _ref[_i];
+ publisher.run();
+ }
+ return this.web.run();
+ };
+ Chainsaw.prototype.visitableURL = function() {
+ if (this._visitableURL == null) {
+ this._visitableURL = "http://" + (this.host());
+ if (this.port() != null) {
+ this.visitableURL += ":" + (this.port());
+ }
+ this._visitableURL += "/";
+ }
+ return this._visitableURL;
+ };
+ Chainsaw.prototype.host = function() {
+ return process.env.HOST || this.get('chainsaw.listen.host', 'localhost');
+ };
+ Chainsaw.prototype.port = function() {
+ return process.env.PORT || this.get('chainsaw.listen.port', 3003);
+ };
+ Chainsaw.prototype.get = function(key, defaultValue) {
+ var config, key_parts, part;
+ key_parts = key.split(".");
+ config = this.config;
+ while (key_parts.length > 0) {
+ part = key_parts.shift();
+ config = config[part];
+ if (config == null) {
+ return defaultValue;
+ }
+ }
+ return config;
+ };
+ Chainsaw.run = function(config_path, callback) {
+ var config, runner;
+ config = JSON.parse(fs.readFileSync(config_path));
+ runner = new this(config);
+ if (callback instanceof Function) {
+ callback(runner);
+ }
+ runner.run();
+ return runner;
+ };
+ return Chainsaw;
+ })();
+ Chainsaw.Base = require("./base").Base;
+ module.exports = Chainsaw;
+}).call(this);
43 lib/irc.js
@@ -0,0 +1,43 @@
+(function() {
+ var Base, IRC, irc;
+ var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
+ for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor;
+ child.__super__ = parent.prototype;
+ return child;
+ }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ irc = require('irc');
+ Base = require('./base').Base;
+ IRC = (function() {
+ __extends(IRC, Base);
+ function IRC() {
+ IRC.__super__.constructor.apply(this, arguments);
+ }
+ IRC.prototype.name = "irc";
+ IRC.prototype.setup = function() {
+ this.channels = this.get('channels');
+ this.client = new irc.Client(this.get('server'), this.get('user'), {
+ channels: this.channels
+ });
+ return this.setupListeners();
+ };
+ IRC.prototype.listeningTo = function(channel) {
+ return this.channels.indexOf(channel) > -1;
+ };
+ IRC.prototype.setupListeners = function() {
+ return this.client.addListener('message', __bind(function(from, to, message) {
+ if (this.listeningTo(to)) {
+ return this.emit('message', {
+ message: message,
+ channel: to,
+ user: from
+ });
+ }
+ }, this));
+ };
+ return IRC;
+ })();
+ exports.publisher = IRC;
+}).call(this);
74 lib/redis.js
@@ -0,0 +1,74 @@
+(function() {
+ var RedisWrapper, redis, sys;
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ redis = require('redis');
+ sys = require('sys');
+ RedisWrapper = (function() {
+ function RedisWrapper(runner) {
+ this.runner = runner;
+ this.host = process.env.REDIS_HOST || this.get('redis.host', 'localhost');
+ this.namespace = process.env.REDIS_NAMESPACE || this.get('redis.namespace', 'juggernaut');
+ this.port = process.env.REDIS_PORT || this.get('redis.port', 6379);
+ this.maxHistory = process.env.REDIS_MAXHISTORY || this.get('redis.maxHistory', 100);
+ this.password = process.env.REDIS_PASSWORD || this.get('redis.password');
+ sys.puts("Connecting to Redis at " + this.host + ":" + this.port);
+ this.redis = redis.createClient(this.port, this.host);
+ if (this.password != null) {
+ this.redis.auth(this.password);
+ }
+ }
+ RedisWrapper.prototype.publish = function(key, message) {
+ if (message != null) {
+ key = "" + this.namespace + ":" + key;
+ } else {
+ message = key;
+ key = this.namespace;
+ }
+ sys.puts("Publishing to " + key);
+ return this.redis.publish(key, message);
+ };
+ RedisWrapper.prototype.historyKeyFor = function(channel) {
+ return "" + this.namespace + ":history:" + channel;
+ };
+ RedisWrapper.prototype.incrementCountFor = function(key, offset) {
+ return this.redis.hincrby("" + this.namespace + ":" + key, offset, 1);
+ };
+ RedisWrapper.prototype.getCounts = function(key, maximum, callback) {
+ return this.redis.hgetall("" + this.namespace + ":" + key, __bind(function(err, results) {
+ var counts, i, _ref;
+ if (err) {
+ return callback(err);
+ } else {
+ counts = [];
+ for (i = 0, _ref = maximum - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) {
+ counts.push(parseInt(results[i] || "0", 10));
+ }
+ return callback(false, JSON.stringify(counts));
+ }
+ }, this));
+ };
+ RedisWrapper.prototype.addHistory = function(channel, data) {
+ var key;
+ key = this.historyKeyFor(channel);
+ this.redis.lpush(key, data);
+ return this.redis.ltrim(key, 0, this.maxHistory - 1);
+ };
+ RedisWrapper.prototype.getHistory = function(channel, callback) {
+ var key;
+ key = this.historyKeyFor(channel);
+ return this.redis.lrange(key, 0, this.maxHistory - 1, __bind(function(err, result) {
+ sys.puts("Getting history for " + key + " (up to " + this.maxHistory + " item(s))");
+ if (err) {
+ sys.puts("Error getting result for history: " + err);
+ return callback(err);
+ } else if (!result) {
+ return callback(true);
+ } else {
+ return callback(false, "[" + (result.join(", ")) + "]");
+ }
+ }, this));
+ };
+ return RedisWrapper;
+ })();
+ exports.RedisWrapper = RedisWrapper;
+}).call(this);
60 lib/twitter.js
@@ -0,0 +1,60 @@
+(function() {
+ var Base, NTwitter, Twitter, sys;
+ var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
+ for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor;
+ child.__super__ = parent.prototype;
+ return child;
+ }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ NTwitter = require('ntwitter');
+ Base = require('./base').Base;
+ sys = require('sys');
+ Twitter = (function() {
+ __extends(Twitter, Base);
+ function Twitter() {
+ Twitter.__super__.constructor.apply(this, arguments);
+ }
+ Twitter.prototype.name = "twitter";
+ Twitter.prototype.setup = function() {
+ var config, outer;
+ config = this.config();
+ this.twitter = new NTwitter({
+ consumer_key: config.consumer.key,
+ consumer_secret: config.consumer.secret,
+ access_token_key: config.access_token.key,
+ access_token_secret: config.access_token.secret
+ });
+ outer = this;
+ return this.twitter.stream('statuses/filter', {
+ track: config.track
+ }, __bind(function(stream) {
+ stream.on('data', __bind(function(tweet) {
+ return outer.emit('tweet', outer.filtered(tweet));
+ }, this));
+ stream.on('end', function(resp) {
+ return sys.puts("Twitter Connection ended, Status code was " + resp.statusCode);
+ });
+ return stream.on('error', function(error) {
+ return sys.puts("Error in Twitter: " + error.message);
+ });
+ }, this));
+ };
+ Twitter.prototype.filtered = function(tweet) {
+ return {
+ text: tweet.text,
+ created_at: tweet.created_at,
+ id_str: tweet.id_str,
+ retweeted: tweet.retweeted,
+ user: {
+ name: tweet.user.name,
+ profile_image_url: tweet.user.profile_image_url,
+ screen_name: tweet.user.screen_name
+ }
+ };
+ };
+ return Twitter;
+ })();
+ exports.publisher = Twitter;
+}).call(this);
81 lib/web.js
@@ -0,0 +1,81 @@
+(function() {
+ var EventEmitter, Web, express, sys;
+ var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
+ for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor;
+ child.__super__ = parent.prototype;
+ return child;
+ }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ express = require('express');
+ sys = require('sys');
+ EventEmitter = require('events').EventEmitter;
+ Web = (function() {
+ __extends(Web, EventEmitter);
+ function Web(runner) {
+ this.runner = runner;
+ EventEmitter.call(this);
+ this.app = express.createServer();
+ this.configuration = {};
+ }
+ Web.prototype.configure = function() {
+ this.app.configure(__bind(function() {
+ this.emit("beforeConfigure", this.app, this);
+ this.app.use(express.methodOverride());
+ this.app.use(express.bodyParser());
+ this.app.use(this.app.router);
+ return this.emit("afterConfigure", this.app, this);
+ }, this));
+ this.app.configure('development', __bind(function() {
+ this.emit("beforeDevelopmentConfigure", this.app, this);
+ this.app.use(express.errorHandler({
+ dumpExceptions: true,
+ showStack: true
+ }));
+ return this.emit("afterDevelopmentConfigure", this.app, this);
+ }, this));
+ return this.setupEndpoints();
+ };
+ Web.prototype.setupEndpoints = function() {
+ this.emit("beforeEndpoints", this.app, this);
+ this.app.get('/version', __bind(function(req, res) {
+ return this.respondWithJSON(req, res, JSON.stringify({
+ application: "Chainsaw",
+ version: "2.0.0.pre2"
+ }));
+ }, this));
+ this.app.get('/history/:key', __bind(function(req, res) {
+ var key;
+ key = req.params.key;
+ return this.runner.redis.getHistory(key, __bind(function(err, data) {
+ if (err) {
+ data = this.errorResponse("Unable to get history for channel '" + key + "'");
+ }
+ return this.respondWithJSON(req, res, data);
+ }, this));
+ }, this));
+ return this.emit("afterEndpoints", this.app, this);
+ };
+ Web.prototype.errorResponse = function(message) {
+ return JSON.stringify({
+ error: message
+ });
+ };
+ Web.prototype.respondWithJSON = function(req, res, inner) {
+ if (req.query.callback) {
+ res.header('Content-Type', 'application/javascript');
+ return res.send("" + req.query.callback + "(" + inner + ");");
+ } else {
+ res.header('Content-Type', 'application/json');
+ return res.send(inner);
+ }
+ };
+ Web.prototype.run = function() {
+ this.configure();
+ return this.app.listen(this.runner.port());
+ };
+ return Web;
+ })();
+ exports.Web = Web;
+}).call(this);
21 package.json
@@ -0,0 +1,21 @@
+{
+ "name": "chainsaw-app",
+ "version": "2.0.0",
+ "description": "Chainsaw app is a series of tools for building real time event notifiers / streams.",
+ "keys": ["realtime", "events", "stream"],
+ "author": "Darcy Laycock <sutto@sutto.net> (http://blog.ninjahideout.com/)",
+ "main": "./lib/chainsaw",
+ "directories": {
+ "lib": "./lib"
+ },
+ "dependencies": {
+ "express": "~> 2.4.3",
+ "irc": "~> 0.2.0",
+ "ntwitter": "~> 0.2.1",
+ "socket.io": "~> 0.7.7",
+ "hiredis": "~> 0.1.12",
+ "redis": "~> 0.6.0",
+ "node-static": "~> 0.5.7",
+ "ejs": "~> 0.4.3"
+ }
+}
45 public/coffeescripts/chainsaw.coffee
@@ -0,0 +1,45 @@
+# Public distributable version of the client.
+
+class Chainsaw
+
+ @run: (host, callback) ->
+ chainsaw = new Chainsaw
+ callback chainsaw
+ chainsaw
+
+ constructor: (host) ->
+ @events = {}
+ @socket = io.connect host
+
+ on: (event, callback) ->
+ @events[event]?= []
+ @events[event].push callback
+
+ watchAll: (channels...) ->
+ for name, callback of channels
+ @watch name, callback
+
+ watch: (channel, callback) ->
+ @on channel, callback
+ @loadHistoryFor @, channel, =>
+ @socket.on channel, (data) => @receive channel, data
+
+ trigger: (name, args...) ->
+ callbacks = (@events[name] ?= [])
+ for callback in callbacks
+ callback.apply this, args
+
+ disconnected: (callback) -> @on 'disconnect', callback
+ connected: (callback) -> @on 'connect', callback
+ reconnected: (callback) -> @on 'reconnect', callback
+
+ receive: (channel, message) ->
+ @trigger channel, message
+
+ loadHistory: (channel, callback) ->
+ $.getJSON "/history/#{channel}", (data) =>
+ for message in data.reverse()
+ @receive channel, message
+ callback() if callback instanceof Function
+
+window['Chainsaw'] = Chainsaw
81 public/javascripts/chainsaw.js
@@ -0,0 +1,81 @@
+(function() {
+ var Chainsaw;
+ var __slice = Array.prototype.slice, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ Chainsaw = (function() {
+ Chainsaw.run = function(host, callback) {
+ var chainsaw;
+ chainsaw = new Chainsaw;
+ callback(chainsaw);
+ return chainsaw;
+ };
+ function Chainsaw(host) {
+ this.events = {};
+ this.socket = io.connect(host);
+ }
+ Chainsaw.prototype.on = function(event, callback) {
+ var _base, _ref;
+ if ((_ref = (_base = this.events)[event]) != null) {
+ _ref;
+ } else {
+ _base[event] = [];
+ };
+ return this.events[event].push(callback);
+ };
+ Chainsaw.prototype.watchAll = function() {
+ var callback, channels, name, _results;
+ channels = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ _results = [];
+ for (name in channels) {
+ callback = channels[name];
+ _results.push(this.watch(name, callback));
+ }
+ return _results;
+ };
+ Chainsaw.prototype.watch = function(channel, callback) {
+ this.on(channel, callback);
+ return this.loadHistoryFor(this, channel, __bind(function() {
+ return this.socket.on(channel, __bind(function(data) {
+ return this.receive(channel, data);
+ }, this));
+ }, this));
+ };
+ Chainsaw.prototype.trigger = function() {
+ var args, callback, callbacks, name, _base, _i, _len, _ref, _results;
+ name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ callbacks = ((_ref = (_base = this.events)[name]) != null ? _ref : _base[name] = []);
+ _results = [];
+ for (_i = 0, _len = callbacks.length; _i < _len; _i++) {
+ callback = callbacks[_i];
+ _results.push(callback.apply(this, args));
+ }
+ return _results;
+ };
+ Chainsaw.prototype.disconnected = function(callback) {
+ return this.on('disconnect', callback);
+ };
+ Chainsaw.prototype.connected = function(callback) {
+ return this.on('connect', callback);
+ };
+ Chainsaw.prototype.reconnected = function(callback) {
+ return this.on('reconnect', callback);
+ };
+ Chainsaw.prototype.receive = function(channel, message) {
+ return this.trigger(channel, message);
+ };
+ Chainsaw.prototype.loadHistory = function(channel, callback) {
+ return $.getJSON("/history/" + channel, __bind(function(data) {
+ var message, _i, _len, _ref;
+ _ref = data.reverse();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ message = _ref[_i];
+ this.receive(channel, message);
+ }
+ if (callback instanceof Function) {
+ return callback();
+ }
+ }, this));
+ };
+ return Chainsaw;
+ })();
+ window['Chainsaw'] = Chainsaw;
+}).call(this);
32 src/base.coffee
@@ -0,0 +1,32 @@
+sys = require 'sys'
+
+class Base
+
+ name: "unknown"
+
+ constructor: (@runner) ->
+ @configNamespace?= @name
+ @namespace?= @name
+
+ isEnabled: -> @runner.get('chainsaw.enabled', []).indexOf(@name) > -1
+
+ run: ->
+ if @isEnabled()
+ sys.puts "Starting publisher: #{@name}"
+ @setup()
+
+ get: (key, defaultValue) ->
+ @runner.get "#{@configNamespace}.#{key}", defaultValue
+
+ config: -> @runner.get @configNamespace
+
+ emit: (key, message) ->
+ if message?
+ key = "#{@namespace}:#{key}"
+ else
+ message = key
+ key = @namespace
+ @runner.io.sockets.emit key, message
+ @runner.redis.addHistory key, JSON.stringify(message)
+
+exports.Base = Base
59 src/chainsaw.coffee
@@ -0,0 +1,59 @@
+path = require 'path'
+sys = require 'sys'
+fs = require 'fs'
+io = require 'socket.io'
+redis = require('./redis').RedisWrapper
+web = require('./web').Web
+
+class Chainsaw
+
+ constructor: (@config) ->
+ @publishers = []
+
+ # Adds a publisher to the current runner.
+ add: (publisher) ->
+ @publishers.push new publisher(@)
+
+ addFromRequire: (path) ->
+ @add require(path).publisher
+
+ run: ->
+ sys.puts "Starting chainsaw..."
+ @redis = new redis @
+ @web = new web @
+ @broadcast = io.listen @web.app
+ @broadcast.set 'log level', 0
+ for publisher in @publishers
+ publisher.run()
+ @web.run()
+
+ visitableURL: ->
+ unless @_visitableURL?
+ @_visitableURL = "http://#{@host()}"
+ if @port()?
+ @visitableURL += ":#{@port()}"
+ @_visitableURL += "/"
+ @_visitableURL
+
+ host: -> process.env.HOST or @get 'chainsaw.listen.host', 'localhost'
+ port: -> process.env.PORT or @get 'chainsaw.listen.port', 3003
+
+ get: (key, defaultValue) ->
+ key_parts = key.split "."
+ config = @config
+ while key_parts.length > 0
+ part = key_parts.shift()
+ config = config[part]
+ return defaultValue unless config?
+ config
+
+
+ @run: (config_path, callback) ->
+ config = JSON.parse fs.readFileSync(config_path)
+ runner = new @(config)
+ callback runner if callback instanceof Function
+ runner.run()
+ runner
+
+Chainsaw.Base = require("./base").Base
+module.exports = Chainsaw
25 src/irc.coffee
@@ -0,0 +1,25 @@
+irc = require('irc')
+Base = require('./base').Base
+
+class IRC extends Base
+
+ name: "irc"
+
+ setup: ->
+ @channels = @get('channels')
+ @client = new irc.Client @get('server'), @get('user'),
+ channels: @channels
+ @setupListeners()
+
+ listeningTo: (channel) ->
+ @channels.indexOf(channel) > -1
+
+ setupListeners: ->
+ @client.addListener 'message', (from, to, message) =>
+ if @listeningTo to
+ @emit 'message',
+ message: message
+ channel: to,
+ user: from
+
+exports.publisher = IRC
65 src/redis.coffee
@@ -0,0 +1,65 @@
+redis = require 'redis'
+sys = require 'sys'
+
+class RedisWrapper
+
+ constructor: (@runner) ->
+ @host = process.env.REDIS_HOST || @runner.get 'redis.host', 'localhost'
+ @port = process.env.REDIS_PORT || @runner.get 'redis.port', 6379
+ @password = process.env.REDIS_PASSWORD || @runner.get 'redis.password'
+ @namespace = @runner.get 'redis.namespace', 'juggernaut'
+ @maxHistory = @runner.get 'redis.maxHistory', 100
+ sys.puts "Connecting to Redis at #{@host}:#{@port}"
+ @redis = redis.createClient @port, @host
+ if @password?
+ sys.puts 'Authing with redis password.'
+ @redis.auth @password
+
+ publish: (key, message) ->
+ if message?
+ key = "#{@namespace}:#{key}"
+ else
+ message = key
+ key = @namespace
+ sys.puts "Publishing to #{key}"
+ @redis.publish key, message
+
+ historyKeyFor: (channel) -> "#{@namespace}:history:#{channel}"
+
+ incrementCountFor: (key, offset) ->
+ @redis.hincrby "#{@namespace}:#{key}", offset, 1
+
+ getCounts: (key, maximum, callback) ->
+ @redis.hgetall "#{@namespace}:#{key}", (err, results) =>
+ if err
+ callback err
+ else
+ counts = []
+ for i in [0..(maximum - 1)]
+ counts.push parseInt(results[i] || "0", 10)
+ callback false, JSON.stringify counts
+
+
+ addHistory: (channel, data) ->
+ key = @historyKeyFor channel
+ @redis.lpush key, data
+ @redis.ltrim key, 0, @maxHistory - 1
+
+ getHistory: (channel, callback) ->
+ key = @historyKeyFor channel
+ @redis.lrange key, 0, @maxHistory - 1, (err, result) =>
+ sys.puts "Getting history for #{key} - #{@maxHistory - 1}"
+ if err
+ sys.puts "Hadd error: #{err}"
+ callback err
+ else if not result
+ callback true
+ else
+ callback false, "[#{result.join(", ")}]"
+
+ debugResponse: (err, result) ->
+ sys.puts "Error: #{sys.inspect err}"
+ sys.puts "Result: #{sys.inspect result}"
+
+
+exports.RedisWrapper = RedisWrapper
36 src/twitter.coffee
@@ -0,0 +1,36 @@
+NTwitter = require 'ntwitter'
+Base = require('chainsaw/base').Base
+sys = require 'sys'
+
+class Twitter extends Base
+
+ name: "twitter"
+
+ setup: ->
+ config = @config()
+ @twitter = new NTwitter({
+ consumer_key: config.consumer.key
+ consumer_secret: config.consumer.secret
+ access_token_key: config.access_token.key
+ access_token_secret: config.access_token.secret
+ })
+ outer = @
+ @twitter.stream 'statuses/filter', track: config.track, (stream) =>
+ stream.on 'data', (tweet) =>
+ outer.emit 'tweet', outer.filtered tweet
+ stream.on 'end', (resp) ->
+ sys.puts "Twitter Connection ended, Status code was #{resp.statusCode}"
+ stream.on 'error', (error) ->
+ sys.puts "Error in Twitter: #{error.message}"
+
+ filtered: (tweet) ->
+ text: tweet.text
+ created_at: tweet.created_at
+ id_str: tweet.id_str
+ retweeted: tweet.retweeted
+ user:
+ name: tweet.user.name
+ profile_image_url: tweet.user.profile_image_url
+ screen_name: tweet.user.screen_name
+
+exports.publisher = Twitter
59 src/web.coffee
@@ -0,0 +1,59 @@
+express = require 'express'
+sys = require 'sys'
+EventEmitter = require('events').EventEmitter
+
+class Web extends EventEmitter
+
+ constructor: (@runner) ->
+ EventEmitter.call @
+ @app = express.createServer()
+ @configuration = {}
+
+ configure: ->
+ @app.configure =>
+ @emit "beforeConfigure", @app, @
+ @app.use express.methodOverride()
+ @app.use express.bodyParser()
+ @app.use @app.router
+ @emit "afterConfigure", @app, @
+ @app.configure 'development', =>
+ @emit "beforeDevelopmentConfigure", @app, @
+ @app.use express.errorHandler
+ dumpExceptions: true
+ showStack: true
+ @emit "afterDevelopmentConfigure", @app, @
+ @setupEndpoints()
+
+ setupEndpoints: ->
+ @emit "beforeEndpoints", @app, @
+ @app.get '/version', (req, res) =>
+ @respondWithJSON req, res, JSON.stringify
+ application: "Chainsaw"
+ version: "2.0.0.pre2"
+ @app.get '/history/:key', (req, res) =>
+ key = req.params.key
+ @runner.redis.getHistory key, (err, data) =>
+ if err
+ data = @errorResponse "Unable to get history for channel '#{key}'"
+ @respondWithJSON req, res, data
+ @emit "afterEndpoints", @app, @
+
+ errorResponse: (message) ->
+ JSON.stringify
+ error: message
+
+ respondWithJSON: (req, res, inner) ->
+ if req.query.callback
+ res.header 'Content-Type', 'application/javascript'
+ res.send "#{req.query.callback}(#{inner});"
+ else
+ res.header 'Content-Type', 'application/json'
+ res.send inner
+
+
+ run: ->
+ @configure()
+ @app.listen @runner.port()
+
+
+exports.Web = Web
Please sign in to comment.
Something went wrong with that request. Please try again.