<commit_before>"use strict";

var _ = require('lodash');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var sira = require('sira');
var express = require('express');
var debug = require('debug')('sira:rest');

var bodyParser = require('body-parser');
var cors = require('cors');

var json = bodyParser.json;
var urlencoded = bodyParser.urlencoded;

var SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript'];

exports = module.exports = function (remotes, handler) {
    if (remotes.remotes && remotes.handle) {
        handler = handler || remotes;
        remotes = remotes.remotes;
    }
    var rest = new Rest(remotes, handler);
    return rest.createHandler();
};

exports.Rest = Rest;
exports.RestClass = RestClass;
exports.RestMethod = RestMethod;
exports.sortRoutes = sortRoutes;

function Rest(remotes, handler) {
    EventEmitter.call(this);

    this.remotes = remotes;
    this.handler = handler && (typeof handler === 'function' ? handler : _.bind(handler.handle, handler));
}

util.inherits(Rest, EventEmitter);

Rest.prototype.createHandler = function () {
    var root = express.Router();
    var rest = this;
    var classes = this.buildClasses();

    // Add a handler to tolerate empty json as connect's json middleware throws an error
    root.use(function(req, res, next) {
        if(req.is('application/json')) {
            if(req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
                req._body = true; // Mark it as parsed
                req.body = {};
            }
        }
        next();
    });

    // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
    debug("remoting options: %j", this.remotes.options);
    var urlencodedOptions = this.remotes.options.urlencoded || {extended: true};
    if (urlencodedOptions.extended === undefined) {
        urlencodedOptions.extended = true;
    }
    var jsonOptions = this.remotes.options.json || {strict: false};
    var corsOptions = this.remotes.options.cors || {};
    root.use(urlencoded(urlencodedOptions));
    root.use(json(jsonOptions));
    root.use(cors(corsOptions));

    classes.forEach(function (restClass) {
        var router = express.Router();
        var className = restClass.sharedClass.name;

        debug('registering REST handler for class %j', className);

        var methods = [];
        // Register handlers for all shared methods of this class sharedClass
        restClass
            .methods
            .forEach(function(restMethod) {
                var sharedMethod = restMethod.sharedMethod;
                debug('    method %s', sharedMethod.stringName);
                restMethod.routes.forEach(function(route) {
                    methods.push({route: route, method: sharedMethod});
                });
            });

        // Sort all methods based on the route path
        methods.sort(sortRoutes);

        methods.forEach(function(m) {
            rest._registerMethodRouteHandlers(router, m.method, m.route);
        });

        // Convert requests for unknown methods of this sharedClass into 404.
        // Do not allow other middleware to invade our URL space.
        router.use(Rest.remoteMethodNotFoundHandler(className));

        // Mount the remoteClass router on all class routes.
        restClass
            .routes
            .forEach(function (route) {
                debug('    at %s', route.path);
                root.use(route.path, router);
            });

    });

    // Convert requests for unknown URLs into 404.
    // Do not allow other middleware to invade our URL space.
    root.use(Rest.urlNotFoundHandler());

    // Use our own error handler to make sure the error response has
    // always the format expected by remoting clients.
    root.use(Rest.errorHandler());

    return root;
};

Rest.prototype.buildClasses = function() {
    return this.remotes.classes().map(function(c) {
        return new RestClass(c);
    });
};

Rest.prototype._registerMethodRouteHandlers = function(router, sharedMethod, route) {
    debug('        %s %s %s', route.verb, route.path, sharedMethod.stringName);
    var verb = route.verb;
    if(verb === 'del') {
        // Express 4.x only supports delete
        verb = 'delete';
    }
    var self = this;
    router[verb](route.path, function (req, res, next) {
        var context = new RestContext(req, res, self.handler);
        self._handleRequest(context, sharedMethod, next);
    });
};

Rest.prototype._handleRequest = function(context, method, next) {
    context.handle(method, function (err) {
        if (err) return next(err);
    })
};

Rest.remoteMethodNotFoundHandler = function(className) {
    className = className || '(unknown)';
    return function restRemoteMethodNotFound(req, res, next) {
        var message = 'Shared class "' + className + '"' +
            ' has no method handling ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.urlNotFoundHandler = function() {
    return function restUrlNotFound(req, res, next) {
        var message = 'There is no method to handle ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.errorHandler = function() {
    return function restErrorHandler(err, req, res, next) {
        if(typeof err === 'string') {
            err = new Error(err);
            err.status = err.statusCode = 500;
        }

        res.statusCode = err.statusCode || err.status || 500;

        debug('Error in %s %s: %s', req.method, req.url, err.stack);
        var data = {
            name: err.name,
            status: res.statusCode,
            message: err.message || 'An unknown error occurred'
        };

        for (var prop in err)
            data[prop] = err[prop];

        // TODO(bajtos) Remove stack info when running in production
        data.stack = err.stack;

        res.send({ error: data });
    };
};

Rest.prototype.allRoutes = function () {
    var routes = [];
    var adapter = this;
    var classes = this.remotes.classes();
    var currentRoot = '';

    classes.forEach(function (sc) {
        adapter
            .getRoutes(sc)
            .forEach(function (classRoute) {
                currentRoot = classRoute.path;
                var methods = sc.methods();

                methods.forEach(function (method) {
                    adapter.getRoutes(method).forEach(function (route) {
                        if(method.isStatic) {
                            addRoute(route.verb, route.path, method);
                        } else {
                            adapter
                                .getRoutes(method.sharedCtor)
                                .forEach(function (sharedCtorRoute) {
                                    addRoute(route.verb, sharedCtorRoute.path + route.path, method);
                                });
                        }
                    });
                });
            });
    });

    return routes;

    function addRoute(verb, path, method) {
        if(path === '/' || path === '//') {
            path = currentRoot;
        } else {
            path = currentRoot + path;
        }

        if(path[path.length - 1] === '/') {
            path = path.substr(0, path.length - 1);
        }

        // TODO this could be cleaner
        path = path.replace(/\/\//g, '/');

        routes.push({
            verb: verb,
            path: path,
            description: method.description,
            method: method.stringName,
            accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
            returns: (method.returns && method.returns.length) ? method.returns : undefined
        });
    }
};

/*!
 * Compare two routes
 * @param {Object} r1 The first route {route: {verb: 'get', path: '/:id'}, method: ...}
 * @param [Object} r2 The second route route: {verb: 'get', path: '/findOne'}, method: ...}
 * @returns {number} 1: r1 comes after 2, -1: r1 comes before r2, 0: equal
 */
function sortRoutes(r1, r2) {
    var a = r1.route;
    var b = r2.route;

    // Normalize the verbs
    var verb1 = a.verb.toLowerCase();
    var verb2 = b.verb.toLowerCase();

    if (verb1 === 'del') {
        verb1 = 'delete';
    }
    if (verb2 === 'del') {
        verb2 = 'delete';
    }
    // First sort by verb
    if (verb1 > verb2) {
        return -1;
    } else if (verb1 < verb2) {
        return 1;
    }

    // Sort by path part by part using the / delimiter
    // For example '/:id' will become ['', ':id'], '/findOne' will become
    // ['', 'findOne']
    var p1 = a.path.split('/');
    var p2 = b.path.split('/');
    var len = Math.min(p1.length, p2.length);

    // Loop through the parts and decide which path should come first
    for (var i = 0; i < len; i++) {
        // Empty part has lower weight
        if (p1[i] === '' && p2[i] !== '') {
            return 1;
        } else if (p1[i] !== '' && p2[i] === '') {
            return -1;
        }
        // Wildcard has lower weight
        if (p1[i][0] === ':' && p2[i][0] !== ':') {
            return 1;
        } else if (p1[i][0] !== ':' && p2[i][0] === ':') {
            return -1;
        }
        // Now the regular string comparision
        if (p1[i] > p2[i]) {
            return 1;
        } else if (p1[i] < p2[i]) {
            return -1;
        }
    }
    // Both paths have the common parts. The longer one should come before the
    // shorter one
    return p2.length - p1.length;
}

/**
 * Get the path for the given method.
 */

//Rest.prototype.buildRoutes = buildRoutes;
function buildRoutes(obj) {
    var routes = obj.http;

    if(routes && !Array.isArray(routes)) {
        routes = [routes];
    }

    // overidden
    if(routes) {
        // patch missing verbs / routes
        routes.forEach(function (r) {
            r.verb = String(r.verb || 'all').toLowerCase();
            r.path = r.path || ('/' + obj.name);
        });
    } else {
        if(obj.name === 'sharedCtor') {
            routes = [{
                verb: 'all',
                path: '/prototype'
            }];
        } else {
            // build default route
            routes = [{
                verb: 'all',
                path: obj.name ? ('/' + obj.name) : ''
            }];
        }
    }

    return routes;
}

function RestClass(sharedClass) {
    hiddenConstProperty(this, 'sharedClass', sharedClass);

    var self = this;
    this.name = sharedClass.name;
    this.routes = buildRoutes(sharedClass);

    this.ctor = sharedClass.sharedCtor && new RestMethod(this, sharedClass.sharedCtor);

    this.methods = sharedClass.methods()
        .filter(function(sm) {
            return !sm.isSharedCtor;
        })
        .map(function(sm) {
            return new RestMethod(self, sm);
        });
}

RestClass.prototype.getPath = function() {
    return this.routes[0].path;
};

function RestMethod(restClass, sharedMethod) {
    hiddenConstProperty(this, 'restClass', restClass);
    hiddenConstProperty(this, 'sharedMethod', sharedMethod);

    // The full name is ClassName.methodName or ClassName.prototype.methodName
    this.fullName = sharedMethod.stringName;
    this.name = this.fullName.split('.').slice(1).join('.');

    this.accepts = sharedMethod.accepts;
    this.returns = sharedMethod.returns;
    this.description = sharedMethod.description;

    var methodRoutes = buildRoutes(sharedMethod);
    if (sharedMethod.isStatic || !restClass.ctor) {
        this.routes = methodRoutes;
    } else {
        var routes = this.routes = [];
        methodRoutes.forEach(function(route) {
            restClass.ctor.routes.forEach(function(ctorRoute) {
                var fullRoute = util._extend({}, route);
                fullRoute.path = joinPaths(ctorRoute.path, route.path);
                routes.push(fullRoute);
            });
        });
    }
}

RestMethod.prototype.isReturningArray = function() {
    return this.returns.length == 1 &&
        this.returns[0].root &&
        getTypeString(this.returns[0].type) === 'array' || false;
};

RestMethod.prototype.acceptsSingleBodyArgument = function() {
    if (this.accepts.length != 1) return false;
    var accepts = this.accepts[0];

    return accepts.http &&
        accepts.http.source == 'body' &&
        getTypeString(accepts.type) == 'object' || false;
};

RestMethod.prototype.getHttpMethod = function() {
    var verb = this.routes[0].verb;
    if (verb == 'all') return 'POST';
    if (verb == 'del') return 'DELETE';
    return verb.toUpperCase();
};

RestMethod.prototype.getPath = function() {
    return this.routes[0].path;
};

RestMethod.prototype.getFullPath = function() {
    return joinPaths(this.restClass.getPath(), this.getPath());
};

function RestContext(req, res, handler) {
    this.req = req;
    this.res = res;
    this.handler = handler;
}

RestContext.prototype._buildRequest = function (method) {
    return sira.rekuest(method, this.req.query, this.req.params, this.req.body)
        .prop('body', this.req.body)
        .prop('query', this.req.query)
        .prop('path', this.req.params)
        .prop('accessToken', this.req.accessToken)
        .req;
};

RestContext.prototype.handle = function (method, cb) {
    var self = this, req = this.req;
    var future = this.handler(this._buildRequest(method), function (err, ctx) { // sira context
        if (err === 'canceled') return debug('request has been canceled - ' + req.url);
        if (err) return cb(err);
        self.done(ctx.result);
        cb();
    });

    if (future) {
        this.req.on('close',future.cancel);
    }
};

RestContext.prototype.done = function (data) {
    // send the result back as
    // the requested content type
    var res = this.res;
    var accepts = this.req.accepts(SUPPORTED_TYPES);
    var dataExists = typeof data !== 'undefined';

    if(dataExists) {
        switch(accepts) {
            case 'json':
                res.json(data);
                break;
            case 'application/javascript':
            case 'text/javascript':
                res.jsonp(data);
                break;
            default:
                // not acceptable
                res.send(406);
                break;
        }
    } else {
        res.header('Content-Type', 'application/json');
        res.statusCode = 204;
        res.end();
    }
};

function hiddenConstProperty(where, property, value) {
    Object.defineProperty(where, property, {
        writable: false,
        enumerable: false,
        configurable: false,
        value: value
    });
}

function getTypeString(ctorOrName) {
    if (typeof ctorOrName === 'function')
        ctorOrName = ctorOrName.name;
    if (typeof ctorOrName === 'string') {
        return ctorOrName.toLowerCase();
    } else {
        debug('WARNING: unknown ctorOrName of type %s: %j',  typeof ctorOrName, ctorOrName);
        return typeof undefined;
    }
}

function joinPaths(left, right) {
    if (!left) return right;
    if (!right || right == '/') return left;

    var glue = left[left.length-1] + right[0];
    if (glue == '//')
        return left + right.slice(1);
    else if (glue[0] == '/' || glue[1] == '/')
        return left + right;
    else
        return left + '/' + right;
}

<commit_msg>refactor<commit_after>"use strict";

var _ = require('lodash');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var sira = require('sira');
var express = require('express');
var debug = require('debug')('sira:rest');

var bodyParser = require('body-parser');
var cors = require('cors');

var json = bodyParser.json;
var urlencoded = bodyParser.urlencoded;

var SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript'];

exports = module.exports = function (remotes, handler) {
    if (remotes.remotes && remotes.handle) {
        handler = handler || remotes;
        remotes = remotes.remotes;
    }
    var rest = new Rest(remotes, handler);
    return rest.createHandler();
};

exports.Rest = Rest;
exports.RestClass = RestClass;
exports.RestMethod = RestMethod;
exports.sortRoutes = sortRoutes;

function Rest(remotes, handler) {
    EventEmitter.call(this);

    this.remotes = remotes;
    this.handler = handler && (typeof handler === 'function' ? handler : _.bind(handler.handle, handler));
}

util.inherits(Rest, EventEmitter);

Rest.prototype.createHandler = function () {
    var root = express.Router();
    var rest = this;
    var classes = this.buildClasses();

    // Add a handler to tolerate empty json as connect's json middleware throws an error
    root.use(function(req, res, next) {
        if(req.is('application/json')) {
            if(req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
                req._body = true; // Mark it as parsed
                req.body = {};
            }
        }
        next();
    });

    // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
    debug("remoting options: %j", this.remotes.options);
    var urlencodedOptions = this.remotes.options.urlencoded || {extended: true};
    if (urlencodedOptions.extended === undefined) {
        urlencodedOptions.extended = true;
    }
    var jsonOptions = this.remotes.options.json || {strict: false};
    var corsOptions = this.remotes.options.cors || {};
    root.use(urlencoded(urlencodedOptions));
    root.use(json(jsonOptions));
    root.use(cors(corsOptions));

    classes.forEach(function (restClass) {
        var router = express.Router();
        var className = restClass.sharedClass.name;

        debug('registering REST handler for class %j', className);

        var methods = [];
        // Register handlers for all shared methods of this class sharedClass
        restClass
            .methods
            .forEach(function(restMethod) {
                var sharedMethod = restMethod.sharedMethod;
                debug('    method %s', sharedMethod.stringName);
                restMethod.routes.forEach(function(route) {
                    methods.push({route: route, method: sharedMethod});
                });
            });

        // Sort all methods based on the route path
        methods.sort(sortRoutes);

        methods.forEach(function(m) {
            rest._registerMethodRouteHandlers(router, m.method, m.route);
        });

        // Convert requests for unknown methods of this sharedClass into 404.
        // Do not allow other middleware to invade our URL space.
        router.use(Rest.remoteMethodNotFoundHandler(className));

        // Mount the remoteClass router on all class routes.
        restClass
            .routes
            .forEach(function (route) {
                debug('    at %s', route.path);
                root.use(route.path, router);
            });

    });

    // Convert requests for unknown URLs into 404.
    // Do not allow other middleware to invade our URL space.
    root.use(Rest.urlNotFoundHandler());

    // Use our own error handler to make sure the error response has
    // always the format expected by remoting clients.
    root.use(Rest.errorHandler());

    return root;
};

Rest.prototype.buildClasses = function() {
    return this.remotes.classes().map(function(c) {
        return new RestClass(c);
    });
};

Rest.prototype._registerMethodRouteHandlers = function(router, sharedMethod, route) {
    debug('        %s %s %s', route.verb, route.path, sharedMethod.stringName);
    var verb = route.verb;
    if(verb === 'del') {
        // Express 4.x only supports delete
        verb = 'delete';
    }
    var self = this;
    router[verb](route.path, function (req, res, next) {
        var context = new RestContext(req, res, self.handler);
        self._handleRequest(context, sharedMethod, next);
    });
};

Rest.prototype._handleRequest = function(context, method, next) {
    context.handle(method, function (err) {
        if (err) return next(err);
    })
};

Rest.remoteMethodNotFoundHandler = function(className) {
    className = className || '(unknown)';
    return function restRemoteMethodNotFound(req, res, next) {
        var message = 'Shared class "' + className + '"' +
            ' has no method handling ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.urlNotFoundHandler = function() {
    return function restUrlNotFound(req, res, next) {
        var message = 'There is no method to handle ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.errorHandler = function() {
    return function restErrorHandler(err, req, res, next) {
        if(typeof err === 'string') {
            err = new Error(err);
            err.status = err.statusCode = 500;
        }

        res.statusCode = err.statusCode || err.status || 500;

        debug('Error in %s %s: %s', req.method, req.url, err.stack);
        var data = {
            name: err.name,
            status: res.statusCode,
            message: err.message || 'An unknown error occurred'
        };

        for (var prop in err)
            data[prop] = err[prop];

        // TODO(bajtos) Remove stack info when running in production
        data.stack = err.stack;

        res.send({ error: data });
    };
};

Rest.prototype.allRoutes = function () {
    var routes = [];
    var adapter = this;
    var classes = this.remotes.classes();
    var currentRoot = '';

    classes.forEach(function (sc) {
        adapter
            .getRoutes(sc)
            .forEach(function (classRoute) {
                currentRoot = classRoute.path;
                var methods = sc.methods();

                methods.forEach(function (method) {
                    adapter.getRoutes(method).forEach(function (route) {
                        if(method.isStatic) {
                            addRoute(route.verb, route.path, method);
                        } else {
                            adapter
                                .getRoutes(method.sharedCtor)
                                .forEach(function (sharedCtorRoute) {
                                    addRoute(route.verb, sharedCtorRoute.path + route.path, method);
                                });
                        }
                    });
                });
            });
    });

    return routes;

    function addRoute(verb, path, method) {
        if(path === '/' || path === '//') {
            path = currentRoot;
        } else {
            path = currentRoot + path;
        }

        if(path[path.length - 1] === '/') {
            path = path.substr(0, path.length - 1);
        }

        // TODO this could be cleaner
        path = path.replace(/\/\//g, '/');

        routes.push({
            verb: verb,
            path: path,
            description: method.description,
            method: method.stringName,
            accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
            returns: (method.returns && method.returns.length) ? method.returns : undefined
        });
    }
};

/*!
 * Compare two routes
 * @param {Object} r1 The first route {route: {verb: 'get', path: '/:id'}, method: ...}
 * @param [Object} r2 The second route route: {verb: 'get', path: '/findOne'}, method: ...}
 * @returns {number} 1: r1 comes after 2, -1: r1 comes before r2, 0: equal
 */
function sortRoutes(r1, r2) {
    var a = r1.route;
    var b = r2.route;

    // Normalize the verbs
    var verb1 = a.verb.toLowerCase();
    var verb2 = b.verb.toLowerCase();

    if (verb1 === 'del') {
        verb1 = 'delete';
    }
    if (verb2 === 'del') {
        verb2 = 'delete';
    }
    // First sort by verb
    if (verb1 > verb2) {
        return -1;
    } else if (verb1 < verb2) {
        return 1;
    }

    // Sort by path part by part using the / delimiter
    // For example '/:id' will become ['', ':id'], '/findOne' will become
    // ['', 'findOne']
    var p1 = a.path.split('/');
    var p2 = b.path.split('/');
    var len = Math.min(p1.length, p2.length);

    // Loop through the parts and decide which path should come first
    for (var i = 0; i < len; i++) {
        // Empty part has lower weight
        if (p1[i] === '' && p2[i] !== '') {
            return 1;
        } else if (p1[i] !== '' && p2[i] === '') {
            return -1;
        }
        // Wildcard has lower weight
        if (p1[i][0] === ':' && p2[i][0] !== ':') {
            return 1;
        } else if (p1[i][0] !== ':' && p2[i][0] === ':') {
            return -1;
        }
        // Now the regular string comparision
        if (p1[i] > p2[i]) {
            return 1;
        } else if (p1[i] < p2[i]) {
            return -1;
        }
    }
    // Both paths have the common parts. The longer one should come before the
    // shorter one
    return p2.length - p1.length;
}

/**
 * Get the path for the given method.
 */

//Rest.prototype.buildRoutes = buildRoutes;
function buildRoutes(obj) {
    var routes = obj.http;

    if(routes && !Array.isArray(routes)) {
        routes = [routes];
    }

    // overidden
    if(routes) {
        // patch missing verbs / routes
        routes.forEach(function (r) {
            r.verb = String(r.verb || 'all').toLowerCase();
            r.path = r.path || ('/' + obj.name);
        });
    } else {
        if(obj.name === 'sharedCtor') {
            routes = [{
                verb: 'all',
                path: '/prototype'
            }];
        } else {
            // build default route
            routes = [{
                verb: 'all',
                path: obj.name ? ('/' + obj.name) : ''
            }];
        }
    }

    return routes;
}

function RestClass(sharedClass) {
    hiddenConstProperty(this, 'sharedClass', sharedClass);

    var self = this;
    this.name = sharedClass.name;
    this.routes = buildRoutes(sharedClass);

    this.ctor = sharedClass.sharedCtor && new RestMethod(this, sharedClass.sharedCtor);

    this.methods = sharedClass.methods()
        .filter(function(sm) {
            return !sm.isSharedCtor;
        })
        .map(function(sm) {
            return new RestMethod(self, sm);
        });
}

RestClass.prototype.getPath = function() {
    return this.routes[0].path;
};

function RestMethod(restClass, sharedMethod) {
    hiddenConstProperty(this, 'restClass', restClass);
    hiddenConstProperty(this, 'sharedMethod', sharedMethod);

    // The full name is ClassName.methodName or ClassName.prototype.methodName
    this.fullName = sharedMethod.stringName;
    this.name = this.fullName.split('.').slice(1).join('.');

    this.accepts = sharedMethod.accepts;
    this.returns = sharedMethod.returns;
    this.description = sharedMethod.description;

    var methodRoutes = buildRoutes(sharedMethod);
    if (sharedMethod.isStatic || !restClass.ctor) {
        this.routes = methodRoutes;
    } else {
        var routes = this.routes = [];
        methodRoutes.forEach(function(route) {
            restClass.ctor.routes.forEach(function(ctorRoute) {
                var fullRoute = util._extend({}, route);
                fullRoute.path = joinPaths(ctorRoute.path, route.path);
                routes.push(fullRoute);
            });
        });
    }
}

RestMethod.prototype.isReturningArray = function() {
    return this.returns.length == 1 &&
        this.returns[0].root &&
        getTypeString(this.returns[0].type) === 'array' || false;
};

RestMethod.prototype.acceptsSingleBodyArgument = function() {
    if (this.accepts.length != 1) return false;
    var accepts = this.accepts[0];

    return accepts.http &&
        accepts.http.source == 'body' &&
        getTypeString(accepts.type) == 'object' || false;
};

RestMethod.prototype.getHttpMethod = function() {
    var verb = this.routes[0].verb;
    if (verb == 'all') return 'POST';
    if (verb == 'del') return 'DELETE';
    return verb.toUpperCase();
};

RestMethod.prototype.getPath = function() {
    return this.routes[0].path;
};

RestMethod.prototype.getFullPath = function() {
    return joinPaths(this.restClass.getPath(), this.getPath());
};

function RestContext(req, res, handler) {
    this.req = req;
    this.res = res;
    this.handler = handler;
}

RestContext.prototype._buildRequest = function (method) {
    return sira.rekuest(method)
        .params(_.assign({}, this.req.query, this.req.params))
        .payload(this.req.body)
        .prop('body', this.req.body)
        .prop('query', this.req.query)
        .prop('path', this.req.params)
        .prop('accessToken', this.req.accessToken)
        .req;
};

RestContext.prototype.handle = function (method, cb) {
    var self = this, req = this.req;
    var future = this.handler(this._buildRequest(method), function (err, ctx) { // sira context
        if (err === 'canceled') return debug('request has been canceled - ' + req.url);
        if (err) return cb(err);
        self.done(ctx.result);
        cb();
    });

    if (future) {
        this.req.on('close',future.cancel);
    }
};

RestContext.prototype.done = function (data) {
    // send the result back as
    // the requested content type
    var res = this.res;
    var accepts = this.req.accepts(SUPPORTED_TYPES);
    var dataExists = typeof data !== 'undefined';

    if(dataExists) {
        switch(accepts) {
            case 'json':
                res.json(data);
                break;
            case 'application/javascript':
            case 'text/javascript':
                res.jsonp(data);
                break;
            default:
                // not acceptable
                res.send(406);
                break;
        }
    } else {
        res.header('Content-Type', 'application/json');
        res.statusCode = 204;
        res.end();
    }
};

function hiddenConstProperty(where, property, value) {
    Object.defineProperty(where, property, {
        writable: false,
        enumerable: false,
        configurable: false,
        value: value
    });
}

function getTypeString(ctorOrName) {
    if (typeof ctorOrName === 'function')
        ctorOrName = ctorOrName.name;
    if (typeof ctorOrName === 'string') {
        return ctorOrName.toLowerCase();
    } else {
        debug('WARNING: unknown ctorOrName of type %s: %j',  typeof ctorOrName, ctorOrName);
        return typeof undefined;
    }
}

function joinPaths(left, right) {
    if (!left) return right;
    if (!right || right == '/') return left;

    var glue = left[left.length-1] + right[0];
    if (glue == '//')
        return left + right.slice(1);
    else if (glue[0] == '/' || glue[1] == '/')
        return left + right;
    else
        return left + '/' + right;
}

<|endoftext|>


<commit_before>var ginga       = require('ginga'),
    params      = ginga.params,
    H           = require('highland'),
    levelup     = require('levelup'),
    sublevel    = require('level-sublevel'),
    transaction = require('level-async-transaction'),
    util        = require('./util'),
    range       = require('./range');

module.exports = function(path, opts){

  //default options
  opts = H.extend({}, opts);
  opts.keyEncoding = 'utf8';
  opts.valueEncoding = 'json';

  //map of roda
  var roda = {};

  //level-sublevel
  var db = sublevel(levelup(path, opts));
  //level-async-transaction
  transaction(db);

  var clocks = db.sublevel('clocks');
  var ids = db.sublevel('ids');

  function Roda(name){
    name = String(name);

    if(!(this instanceof Roda))
      return roda[name] || new Roda(name);

    if(roda[name] && this !== roda[name])
      throw new Error('Roda `'+name+'` has already been created.');
    else
      roda[name] = this;

    // roda "/" prefixed
    this.store = db.sublevel('/'+name);
    //can retrieve global clock without scanning through rodas

    this._name    = name;
    this._changes = this.store.sublevel('changes');
    this._merges  = this.store.sublevel('merges');
    this._indices = this.store.sublevel('indices');
    this._indexed = this.store.sublevel('indexed');

    this._mapper = null;
  }

  ginga(Roda);

  Roda.db = db;
  Roda.transaction = db.transaction;
  Roda.util = util;
  Roda.id = db.mid;

  //Methods
  Roda.fn = ginga(Roda.prototype);

  Roda.fn.name = function(){
    return this._name;
  };
  Roda.fn.queue = require('./queue');

  //Read Operations

  Roda.fn.define('get', params('id:string','tx?'), function(ctx, done){
    function cb(err, val){
      //notFound should not return error but null value
      if(err && !err.notFound)
        done(err);
      else
        done(null, val || null);
    }
    if(ctx.params.tx){
      if(ctx.params.tx.db !== db)
        return next(new Error('Invalid transaction object.'));
      ctx.params.tx.get(util.encode(ctx.params.id), {
        prefix: this.store
      }, cb);
    }else{
      this.store.get(util.encode(ctx.params.id), cb);
    }
  });

  Roda.fn.readStream =
  Roda.fn.createReadStream =
  function(opts){
    opts = opts || {};

    var store = typeof opts.index === 'string' ?
      this._indices.sublevel(opts.index) : this.store;

    return H(store.createValueStream(range(opts)));
  };

  //Hooks
  function extend(ctx, next){
    H.extend(ctx.params, ctx);
    next();
  }

  Roda.fn.define('validate', params(
    'result', 'transaction'
  ), extend, function(ctx, done){
    done(null, ctx.result);
  });

  Roda.fn.define('diff', params(
    'clock','current', 'result', 'transaction'
  ), extend, null);

  //Write Operations

  Roda.fn.define('put', params('id:string?','result:object','tx?'),
    prepare, validate, id, current, rev, diff, put, invoke);

  Roda.fn.define('del', params('id:string','tx?'),
    prepare, id, current, rev, diff, del, invoke);

  Roda.fn.delete = Roda.fn.del;

  function prepare(ctx, next, end){
    //prepare result
    ctx.result = H.extend(ctx.params.result, {});

    var deleted = (!ctx.params.result) && ('id' in ctx.params);

    if(deleted)
      ctx.result._deleted = true;

    //init transaction
    if(ctx.params.tx){
      if(ctx.params.tx.db !== db)
        return next(new Error('Invalid transaction object.'));
      ctx.transaction = ctx.params.tx;
      //defer if within another transaction
      ctx.transaction.defer(function(cb){
        end(cb);
        next();
      });
    }else{
      ctx.transaction = db.transaction();
      next();
    }
  }

  function validate(ctx, next){
    //trigger validate transaction hook
    this.validate(
      ctx.result,
      ctx.transaction,
      function(err, result){
        if(err) return next(err);

        delete ctx.result._deleted;
        delete ctx.result._from;
        ctx.result = result;
        next();
      }
    );
  }

  function id(ctx, next){
    //get database unique id
    var self = this;
    ctx.transaction.get(this._name, {
      prefix: ids
    }, function(err, mid){
      if(mid){
        ctx.mid = mid;
        next();
      }else{
        require('crypto').randomBytes(6, function(err, buf){
          if(err) return next(err);

          ctx.mid = buf.toString('base64')
            .replace(/\//g,'_')
            .replace(/\+/g,'-');

          if(ctx.mid.length !== 8)
            next(new Error('MID must be length 8'));

          ctx.transaction.put(self._name, ctx.mid, {
            prefix: ids
          });

          next();
        });
      }
    });
  }

  function current(ctx, next){
    var self = this;
    if('id' in ctx.params)
      ctx.result._id = ctx.params.id;
    else
      //monotonic timestamp + mid
      ctx.result._id = util.encode64(util.timestamp()) + ctx.mid;

    //get current document by id
    ctx.transaction.get(util.encode(ctx.result._id), {
      prefix: this.store
    }, function(err, val){
      //return IO/other errors
      if(err && !err.notFound)
        return next(err);
      //dont proceed if delete non existing item
      if(!val && ctx.result._deleted)
        return next(null);
      if(val){
        ctx.current = val;

        //delete current change
        ctx.transaction.del(ctx.current._rev, {
          prefix: self._changes
        });
      }
      next();
    });
  }

  function rev(ctx, next){
    var self = this;

    //get current clock,
    //hence genreate new _rev based on current clock and _rev
    var key = this._name + ctx.mid;
    ctx.transaction.get(key, {
      prefix: clocks
    }, function(err, val){
      if(val)
        ctx.clock = ctx.mid + val;

      var time = util.trim(util.encode64(
        Math.max(
          val ? util.decode64(
            util.pad(val, 12)
          ) : 0,
          ctx.current ? util.decode64(
            util.pad(ctx.current._rev.slice(8), 12)
          ) : 0
        ) + 1
      ));

      //generate rev id
      ctx.result._rev = ctx.mid + time;

      //clock update
      ctx.transaction.put(key, time, {
        prefix: clocks
      });

      next();
    });
  }

  function diff(ctx, next){
    ctx.change = H.extend(ctx.result, {});
    ctx.change._from = {};
    if(ctx.current)
      ctx.change._from.rev = ctx.current._rev;
    if(ctx.clock)
      ctx.change._from.clock = ctx.clock;

    //trigger diff transaction hook
    this.diff(
      ctx.clock || null,
      ctx.current || null,
      H.extend(ctx.result, {}), //dont modify result
      ctx.transaction,
      function(err, res){
        if(err) return next(err);
        next();
      }
    );
  }

  function put(ctx, next){
    //put: put doc, put changes
    ctx.transaction
      .put(util.encode(ctx.result._id), ctx.result, {
        prefix: this.store
      })
      .put(ctx.result._rev, ctx.change, {
        prefix: this._changes
      });
    next();
  }

  function del(ctx, next){
    //del: del doc, put changes
    ctx.transaction
      .del(util.encode(ctx.result._id), {
        prefix: this.store
      })
      .put(ctx.result._rev, ctx.change, {
        prefix: this._changes
      });
    next();
  }

  function invoke(ctx, done){
    //no need commit for nested transaction,
    //root will take care of it
    var result = H.extend(ctx.result, {});
    if(ctx.params.tx){
      //batched not yet committed
      done(null, result);
    }else{
      ctx.transaction.commit(function(err){
        if(err) done(err, null);
        else done(null, result);
      });
    }
  }

  //_rev validation
  Roda.fn.use('diff', function(ctx, next){
    if(ctx.current && ctx.result._rev <= ctx.current._rev)
      return next(new Error('Revision must be incremental with current.'));

    if(ctx.clock && ctx.result._rev <= ctx.clock)
      return next(new Error('Revision must be incremental with clock.'));

    next();
  });

  //Index API

  Roda.fn.index = function(name, mapper){
    this._mapper = this._mapper || {};

    if(typeof name === 'string' && typeof mapper === 'function'){
      if(this._mapper[name])
        throw new Error('Index `'+name+'` has already been assigned.');
      this._mapper[name] = mapper;
    }else{
      throw new Error('Invalid index mapper');
    }
    return this;
  };

  Roda.fn.use('diff', function(ctx, next){
    if(!this._mapper) return next();

    var self = this;
    var tx = ctx.transaction;

    tx.get(ctx.result._id, {
      prefix: this._indexed
    }, function(err, current){
      //delete current indices
      var name, i, l, keys, key;
      if(current){
        for(name in current){
          keys = current[name];
          for(i = 0, l = keys.length; i < l; i++){
            tx.del(keys[i], {
              prefix: self._indices.sublevel(name)
            });
          }
        }
      }

      //when delete
      if(!ctx.result){
        tx.put(ctx.result._id, {}, {
          prefix: self._indexed
        });
        return next();
      }

      var result = H.extend(ctx.result, {});
      var indexed = {};
      var async = false;

      var plan = 0;

      function emit(name, key, value, unique){
        if(async)
          throw new Error('Index mapper must not be async.');

        //optional value arg
        if(value === true){
          unique = true;
          value = null;
        }

        //append unique timestamp for non-unqiue key
        var enKey = util.encode(key) + '\x00';
        if(unique !== true)
          enKey += util.encode(util.timestamp());

        //record encoded key
        indexed[name].push(enKey);

        //check unique
        if(unique === true)
          tx.defer(function(cb){
            tx.get(enKey, {
              prefix: self._indices.sublevel(name)
            }, function(err, val){
              if(val) cb(new Error(key + ' must be unique.'));
              else cb();
            });
          });

        //put index store
        tx.defer(function(cb){
          tx.put(enKey, H.extend({
            _id: ctx.result._id, _key: key
          }, value || H.extend(result, {})), {
            prefix: self._indices.sublevel(name)
          }, cb);
        });
      }

      for(name in self._mapper){
        indexed[name] = [];
        self._mapper[name](result, emit.bind(null, name));
      }
      //new indexed keys
      tx.put(ctx.result._id, indexed, {
        prefix: self._indexed
      });
      async = true;

      next();
    });
  });

  //Changes

  Roda.fn.clockStream =
  Roda.fn.createClockStream =
  function(){
    return H(clocks.createReadStream({
      gt: this._name, lt: this._name + '~'
    })).map(function(data){
      return data.key.slice(-8) + data.value;
    });
  };

  Roda.fn.liveStream =
  Roda.fn.createLiveStream =
  function(){
    var changes = this._changes;
    return H(function(push){
      changes.post(function(data){
        if(data.value) //omit del operations
          push(null, data.value);
      });
    });
  };

  function readChangeStream(sinceObj, limit){
    var count = 0;
    var self = this;
    if(!sinceObj)
      return H(self._changes.createValueStream({ limit: limit }));

    return self.clockStream()
      .map(function(data){
        var mid = data.slice(0, 8);
        return {
          mid:  mid,
          from: sinceObj[mid],
          to:   data.slice(8)
        };
      })
      .filter(function(info){
        return !info.from || info.from < info.to;
      })
      .reject(function(){
        return limit > -1 && count >= limit;
      })
      .map(function(info){
        return H(self._changes.createValueStream({
          gt: info.mid + info.from,
          lt: info.mid + '~',
          limit: limit > -1 ? limit - count : -1
        }));
      })
      .parallel(1)
      .map(function(data){
        count++;
        return data;
      });
  }

  Roda.fn.changeStream =
  Roda.fn.createChangeStream =
  function(opts){
    opts = opts || {};
    var limit = opts.limit || -1;
    var live = opts.live === true;
    var since = Array.isArray(opts.since) ? opts.since : null;

    var self = this;

    function stream(since){
      var sinceObj = since && since.length > 0 ? util.clockObject(since) : null;

      return live ? H(function(push, next){
        //live changes = readChangeStream + liveStream
        readChangeStream.call(self, sinceObj, 1)
          .collect()
          .pull(function(err, arr){
            //pull readStream 1 by 1
            if(err){
              return push(err);
            }else if(arr.length > 0){
              var doc = arr[0];
              sinceObj = sinceObj || {};
              sinceObj[doc._rev.slice(0,8)] = doc._rev.slice(8);
              push(null, doc);
              next();
            }else{
              //switch to liveStream when readStream done
              next(self.liveStream());
            }
          });
      }): readChangeStream.call(self, sinceObj, limit);
    }
    return since ?
      stream(since) :
      H.pipeline(function (clockStream) {
        return clockStream.collect().map(stream).parallel(1);
      });
  };

  Roda.fn.define('merge', params(
    'change:object'
  ), function(ctx, next){

  });

  Roda.fn.mergeStream =
  Roda.fn.createMergeStream =
  function(){
    var merge = _.wrapCallback(Roda.fn.merge.bind(this));
    return H.pipeline(function (changeStream){
      return changeStream
        .map(merge)
        .parallel(1);
    });
  };

  return Roda;
};
<commit_msg>Clean up<commit_after>var ginga       = require('ginga'),
    params      = ginga.params,
    H           = require('highland'),
    levelup     = require('levelup'),
    sublevel    = require('level-sublevel'),
    transaction = require('level-async-transaction'),
    util        = require('./util'),
    range       = require('./range');

module.exports = function(path, opts){

  //default options
  opts = H.extend({}, opts);
  opts.keyEncoding = 'utf8';
  opts.valueEncoding = 'json';

  //map of roda
  var roda = {};

  //level-sublevel
  var db = sublevel(levelup(path, opts));
  //level-async-transaction
  transaction(db);

  var clocks = db.sublevel('clocks');
  var mids = db.sublevel('mids');

  function Roda(name){
    name = String(name);

    if(!(this instanceof Roda))
      return roda[name] || new Roda(name);

    if(roda[name] && this !== roda[name])
      throw new Error('Roda `'+name+'` has already been created.');
    else
      roda[name] = this;

    // roda "/" prefixed
    this.store = db.sublevel('/'+name);
    //can retrieve global clock without scanning through rodas

    this._name    = name;
    this._changes = this.store.sublevel('changes');
    this._merges  = this.store.sublevel('merges');
    this._indices = this.store.sublevel('indices');
    this._indexed = this.store.sublevel('indexed');

    this._mapper = null;
  }

  ginga(Roda);

  Roda.db = db;
  Roda.transaction = db.transaction;
  Roda.util = util;
  Roda.id = db.mid;

  //Methods
  Roda.fn = ginga(Roda.prototype);

  Roda.fn.name = function(){
    return this._name;
  };
  Roda.fn.queue = require('./queue');

  //Read Operations

  Roda.fn.define('get', params('id:string','tx?'), function(ctx, done){
    function cb(err, val){
      //notFound should not return error but null value
      if(err && !err.notFound)
        done(err);
      else
        done(null, val || null);
    }
    if(ctx.params.tx){
      if(ctx.params.tx.db !== db)
        return next(new Error('Invalid transaction object.'));
      ctx.params.tx.get(util.encode(ctx.params.id), {
        prefix: this.store
      }, cb);
    }else{
      this.store.get(util.encode(ctx.params.id), cb);
    }
  });

  Roda.fn.readStream =
  Roda.fn.createReadStream =
  function(opts){
    opts = opts || {};

    var store = typeof opts.index === 'string' ?
      this._indices.sublevel(opts.index) : this.store;

    return H(store.createValueStream(range(opts)));
  };

  //Hooks
  function extend(ctx, next){
    H.extend(ctx.params, ctx);
    next();
  }

  Roda.fn.define('validate', params(
    'result', 'transaction'
  ), extend, function(ctx, done){
    done(null, ctx.result);
  });

  Roda.fn.define('diff', params(
    'clock','current', 'result', 'transaction'
  ), extend, null);

  //Write Operations

  Roda.fn.define('put', params('id:string?','result:object','tx?'),
    prepare, validate, id, current, rev, diff, put, invoke);

  Roda.fn.define('del', params('id:string','tx?'),
    prepare, id, current, rev, diff, del, invoke);

  Roda.fn.delete = Roda.fn.del;

  function prepare(ctx, next, end){
    //prepare result
    ctx.result = H.extend(ctx.params.result, {});

    var deleted = (!ctx.params.result) && ('id' in ctx.params);

    if(deleted)
      ctx.result._deleted = true;

    //init transaction
    if(ctx.params.tx){
      if(ctx.params.tx.db !== db)
        return next(new Error('Invalid transaction object.'));
      ctx.transaction = ctx.params.tx;
      //defer if within another transaction
      ctx.transaction.defer(function(cb){
        end(cb);
        next();
      });
    }else{
      ctx.transaction = db.transaction();
      next();
    }
  }

  function validate(ctx, next){
    //trigger validate transaction hook
    this.validate(
      ctx.result,
      ctx.transaction,
      function(err, result){
        if(err) return next(err);

        delete ctx.result._deleted;
        delete ctx.result._from;
        ctx.result = result;
        next();
      }
    );
  }

  function id(ctx, next){
    //get database unique id
    var self = this;
    ctx.transaction.get(this._name, {
      prefix: mids
    }, function(err, mid){
      if(mid){
        ctx.mid = mid;
        next();
      }else{
        require('crypto').randomBytes(6, function(err, buf){
          if(err) return next(err);

          ctx.mid = buf.toString('base64')
            .replace(/\//g,'_')
            .replace(/\+/g,'-');

          if(ctx.mid.length !== 8)
            next(new Error('MID must be length 8'));

          ctx.transaction.put(self._name, ctx.mid, {
            prefix: mids
          });

          next();
        });
      }
    });
  }

  function current(ctx, next){
    var self = this;
    if('id' in ctx.params)
      ctx.result._id = ctx.params.id;
    else
      //monotonic timestamp + mid
      ctx.result._id = util.encode64(util.timestamp()) + ctx.mid;

    //get current document by id
    ctx.transaction.get(util.encode(ctx.result._id), {
      prefix: this.store
    }, function(err, val){
      //return IO/other errors
      if(err && !err.notFound)
        return next(err);
      //dont proceed if delete non existing item
      if(!val && ctx.result._deleted)
        return next(null);
      if(val){
        ctx.current = val;

        //delete current change
        ctx.transaction.del(ctx.current._rev, {
          prefix: self._changes
        });
      }
      next();
    });
  }

  function rev(ctx, next){
    var self = this;

    //get current clock,
    //hence genreate new _rev based on current clock and _rev
    var key = this._name + ctx.mid;
    ctx.transaction.get(key, {
      prefix: clocks
    }, function(err, val){
      if(val)
        ctx.clock = ctx.mid + val;

      var time = util.trim(util.encode64(
        Math.max(
          val ? util.decode64(
            util.pad(val, 12)
          ) : 0,
          ctx.current ? util.decode64(
            util.pad(ctx.current._rev.slice(8), 12)
          ) : 0
        ) + 1
      ));

      //generate rev id
      ctx.result._rev = ctx.mid + time;

      //clock update
      ctx.transaction.put(key, time, {
        prefix: clocks
      });

      next();
    });
  }

  function diff(ctx, next){
    ctx.change = H.extend(ctx.result, {});
    ctx.change._from = {};
    if(ctx.current)
      ctx.change._from.rev = ctx.current._rev;
    if(ctx.clock)
      ctx.change._from.clock = ctx.clock;

    //trigger diff transaction hook
    this.diff(
      ctx.clock || null,
      ctx.current || null,
      H.extend(ctx.result, {}), //dont modify result
      ctx.transaction,
      function(err, res){
        if(err) return next(err);
        next();
      }
    );
  }

  function put(ctx, next){
    //put: put doc, put changes
    ctx.transaction
      .put(util.encode(ctx.result._id), ctx.result, {
        prefix: this.store
      })
      .put(ctx.result._rev, ctx.change, {
        prefix: this._changes
      });
    next();
  }

  function del(ctx, next){
    //del: del doc, put changes
    ctx.transaction
      .del(util.encode(ctx.result._id), {
        prefix: this.store
      })
      .put(ctx.result._rev, ctx.change, {
        prefix: this._changes
      });
    next();
  }

  function invoke(ctx, done){
    //no need commit for nested transaction,
    //root will take care of it
    var result = H.extend(ctx.result, {});
    if(ctx.params.tx){
      //batched not yet committed
      done(null, result);
    }else{
      ctx.transaction.commit(function(err){
        if(err) done(err, null);
        else done(null, result);
      });
    }
  }

  //_rev validation
  Roda.fn.use('diff', function(ctx, next){
    if(ctx.current && ctx.result._rev <= ctx.current._rev)
      return next(new Error('Revision must be incremental with current.'));

    if(ctx.clock && ctx.result._rev <= ctx.clock)
      return next(new Error('Revision must be incremental with clock.'));

    next();
  });

  //Index API

  Roda.fn.index = function(name, mapper){
    this._mapper = this._mapper || {};

    if(typeof name === 'string' && typeof mapper === 'function'){
      if(this._mapper[name])
        throw new Error('Index `'+name+'` has already been assigned.');
      this._mapper[name] = mapper;
    }else{
      throw new Error('Invalid index mapper');
    }
    return this;
  };

  Roda.fn.use('diff', function(ctx, next){
    if(!this._mapper) return next();

    var self = this;
    var tx = ctx.transaction;

    tx.get(ctx.result._id, {
      prefix: this._indexed
    }, function(err, current){
      //delete current indices
      var name, i, l, keys, key;
      if(current){
        for(name in current){
          keys = current[name];
          for(i = 0, l = keys.length; i < l; i++){
            tx.del(keys[i], {
              prefix: self._indices.sublevel(name)
            });
          }
        }
      }

      //when delete
      if(!ctx.result){
        tx.put(ctx.result._id, {}, {
          prefix: self._indexed
        });
        return next();
      }

      var result = H.extend(ctx.result, {});
      var indexed = {};
      var async = false;

      var plan = 0;

      function emit(name, key, value, unique){
        if(async)
          throw new Error('Index mapper must not be async.');

        //optional value arg
        if(value === true){
          unique = true;
          value = null;
        }

        //append unique timestamp for non-unqiue key
        var enKey = util.encode(key) + '\x00';
        if(unique !== true)
          enKey += util.encode(util.timestamp());

        //record encoded key
        indexed[name].push(enKey);

        //check unique
        if(unique === true)
          tx.defer(function(cb){
            tx.get(enKey, {
              prefix: self._indices.sublevel(name)
            }, function(err, val){
              if(val) cb(new Error(key + ' must be unique.'));
              else cb();
            });
          });

        //put index store
        tx.defer(function(cb){
          tx.put(enKey, H.extend({
            _id: ctx.result._id, _key: key
          }, value || H.extend(result, {})), {
            prefix: self._indices.sublevel(name)
          }, cb);
        });
      }

      for(name in self._mapper){
        indexed[name] = [];
        self._mapper[name](result, emit.bind(null, name));
      }
      //new indexed keys
      tx.put(ctx.result._id, indexed, {
        prefix: self._indexed
      });
      async = true;

      next();
    });
  });

  //Changes

  Roda.fn.clockStream =
  Roda.fn.createClockStream =
  function(){
    return H(clocks.createReadStream({
      gt: this._name, lt: this._name + '~'
    })).map(function(data){
      return data.key.slice(-8) + data.value;
    });
  };

  Roda.fn.liveStream =
  Roda.fn.createLiveStream =
  function(){
    var changes = this._changes;
    return H(function(push){
      changes.post(function(data){
        if(data.value) //omit del operations
          push(null, data.value);
      });
    });
  };

  function readChangeStream(sinceObj, limit){
    var count = 0;
    var self = this;
    if(!sinceObj)
      return H(self._changes.createValueStream({ limit: limit }));

    return self.clockStream()
      .map(function(data){
        var mid = data.slice(0, 8);
        return {
          mid:  mid,
          from: sinceObj[mid],
          to:   data.slice(8)
        };
      })
      .filter(function(info){
        return !info.from || info.from < info.to;
      })
      .reject(function(){
        return limit > -1 && count >= limit;
      })
      .map(function(info){
        return H(self._changes.createValueStream({
          gt: info.mid + info.from,
          lt: info.mid + '~',
          limit: limit > -1 ? limit - count : -1
        }));
      })
      .parallel(1)
      .map(function(data){
        count++;
        return data;
      });
  }

  Roda.fn.changeStream =
  Roda.fn.createChangeStream =
  function(opts){
    opts = opts || {};
    var limit = opts.limit || -1;
    var live = opts.live === true;
    var since = Array.isArray(opts.since) ? opts.since : null;

    var self = this;

    function stream(since){
      var sinceObj = since && since.length > 0 ? util.clockObject(since) : null;

      return live ? H(function(push, next){
        //live changes = readChangeStream + liveStream
        readChangeStream.call(self, sinceObj, 1)
          .collect()
          .pull(function(err, arr){
            //pull readStream 1 by 1
            if(err){
              return push(err);
            }else if(arr.length > 0){
              var doc = arr[0];
              sinceObj = sinceObj || {};
              sinceObj[doc._rev.slice(0,8)] = doc._rev.slice(8);
              push(null, doc);
              next();
            }else{
              //switch to liveStream when readStream done
              next(self.liveStream());
            }
          });
      }): readChangeStream.call(self, sinceObj, limit);
    }
    return since ?
      stream(since) :
      H.pipeline(function (clockStream) {
        return clockStream.collect().map(stream).parallel(1);
      });
  };

  Roda.fn.define('merge', params(
    'change:object'
  ), function(ctx, next){

  });

  Roda.fn.mergeStream =
  Roda.fn.createMergeStream =
  function(){
    var merge = _.wrapCallback(Roda.fn.merge.bind(this));
    return H.pipeline(function (changeStream){
      return changeStream
        .map(merge)
        .parallel(1);
    });
  };

  return Roda;
};
<|endoftext|>


<commit_before>var Step     = require('step');
var xmpp     = require('node-xmpp');

var _campfire = require('../vendor/node-campfire/lib/campfire');
var Campfire  = _campfire.Campfire;
var Room      = _campfire.Room;

var ERROR_NS    = 'urn:ietf:params:xml:ns:xmpp-stanzas';
var MUC_NS      = 'http://jabber.org/protocol/muc';
var MUC_USER_NS = MUC_NS + '#user';

var TWITTER_STATUS_PREFIX = 'http://twitter.com/twitter/status/';

var XmppCampfireRoom = function(client, room_jid) {
  if (typeof(room_jid) === 'string') {
    room_jid = new xmpp.JID(room_jid);
  }

  this.client = client;
  this.jid = room_jid.bare();
  this.present = false;

  this.campfire = null;
  this.campfire_user = {};
  this.campfire_room = null;
  this.campfire_roster = [];
  this.campfire_users = {};

  // JID "buffer"
  this._jid = room_jid;
};

XmppCampfireRoom.prototype = {

  handle_stanza: function(stanza) {
    this.log('>>>', stanza.name);

    if (stanza.is('presence')) {
      if (stanza.attrs.type === 'unavailable') {
        if (this.present) {
          this.handle_exit(stanza);
        }
      } else if (stanza.children.length) {
        this.handle_enter(stanza);
      }

      if (stanza.attrs.type !== 'unsubscribed') {
        this.handle_xmpp_presence(stanza);
      }
    } else if (!this.present) {
      this.send_stanza(
        null, build_xmpp_error(
          stanza, 'modify', 'not-acceptable', 'Not in room'));
    } else if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
      this.handle_xmpp_message(stanza);
    } else if (stanza.is('iq')) {
      if (stanza.attrs.type === 'get' && stanza.getChild('vCard', 'vcard-temp')) {
        this.handle_xmpp_vcard(stanza);
      }
      this.send_stanza(
        stanza.attrs.from.split('/')[1], build_xmpp_error(
          stanza, 'cancel', 'service-unavailable'));
    } else {
      this.log('unhandled stanza:', stanza.toString());
    }
  },

  handle_enter: function(stanza) {
    var self = this;
    var options = {ssl: true};
    var room_name;

    Step(
      function auth() {
        if (self.present &&
            self._prev_id === stanza.attrs.id) {
          self.log('duplicate presence received');
          return;
        }
        self._prev_id = stanza.attrs.id;

        var i = self.jid.user.indexOf('+');
        if (i > -1) {
          options.account = self.jid.user.substr(0, i).trim();
          room_name = self.jid.user.substr(i + 1).trim();
        } else {
          throw 302;
        }

        var x = stanza.getChild('x', MUC_NS);
        options.token = x ? x.getChildText('password') : null;
        if (!options.token) {
          throw 401;
        }

        self.campfire = new Campfire(options);

        if (options.token.length != 40) {
          var username = stanza.attrs.to.split('/')[1];
          self.campfire.authorization = 'Basic ' + new Buffer(
            username + ':' + options.token).toString('base64');
        }

        self.campfire.me(this);
      },

      function get_rooms(err, response) {
        if (err) throw err;

        self.campfire_user = response.user;
        self.campfire_users[response.user.id] = response.user;

        // Reset token in case we used username/password
        if (response.user.api_auth_token) {
          options.token = response.user.api_auth_token;
          self.campfire = new Campfire(options);
        }

        self.campfire.rooms(this);
      },

      function find_room(err, rooms) {
        if (err) throw err;

        // .some() to emulate for loop with break
        var found_room = null;
        rooms.some(function(room) {
          if (room.id == room_name ||
              room.name.trim().toLowerCase() == room_name.toLowerCase()) {
            found_room = room;
            return true;
          }
          return false;
        });

        if (found_room) {
          self.campfire_room = found_room;
          self.campfire_room.join(this);
        } else {
          throw 302;
        }
      },

      function join_room(err, room) {
        if (err == 302) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'cancel', 'item-not-found', 'Room not found'));
          self.log('not found');
          return;
        } else if (err == 401) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'auth', 'not-authorized', 'Auth failed'));
          self.log('not authorized');
          return;
        } else if (err) {
          throw err;
        }

        self.present = true;

        self.refresh_room(this);
        // TODO parallelize next steps?
      },

      function finish_join(err) {
        if (err) throw err;

        // Send user self presence
        self.send_stanza(
          self.campfire_user.name, build_xmpp_presence(
            {status_codes: [110, 170, 210]}));

        self.campfire_room.messages(this);
      },

      function send_room_state(err, messages) {
        if (err) {
          self.log('messages', err);
        };

        if (messages) {
          // Filter types for history
          messages = messages.filter(function(msg) {
            return ['TextMessage', 'PasteMessage', 'TweetMessage',
                    'SystemMessage', 'TopicChangeMessage'
                   ].indexOf(msg.type) > -1;
          });

          // Obey (some) client history limits
          try {
            var history = stanza.getChild('x', MUC_NS).getChild('history');
          } catch (e) {}
          if (history && history.attrs.maxstanzas) {
            messages.splice(0, messages.length - history.attrs.maxstanzas);
          }

          // Send messages through normal handler
          messages.forEach(function(msg) {
            self.handle_campfire_message(msg, true);
          });
        }

        if (self.campfire_room.topic) {
          self.send_stanza(
            null, build_xmpp_message(
              self.campfire_room.topic, 'subject'));
        }

        self.campfire_start_listen();
      },

      function errors(err) {
        self.send_stanza(
          null, build_xmpp_error(
            stanza, 'wait', 'internal-server-error', err));
        self.log('join_error', err);
      }
    );
  },

  handle_exit: function(stanza, status_code) {
    this.campfire_stop_listen();

    if (this.present) {
      var self = this;
      this.campfire_room.leave(function() {
        self.send_stanza(
          self.campfire_user.name, build_xmpp_presence(
            {type: 'unavailable', status_codes: [status_code || 110]}));
      });
    }

    this.present = false;
  },

  handle_xmpp_message: function(stanza) {
    var body = stanza.getChildText('body');
    if (body) {
      var self = this;
      this.campfire_room.message(body, undefined, function(err) {
        if (err) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'wait', 'internal-server-error', err));
        }
      });
    }
    // TODO handle other messages (subject change)
  },

  handle_xmpp_presence: function(stanza) {
    if (this._probe_timer) {
      clearTimeout(this._probe_timer);
      this._probe_timer = undefined;
    }

    if (this.presence) {
      if (!stanza) {
        this.send_stanza(
          null, new xmpp.Element('presence', {type: 'probe'}));
      }

      this._probe_timer = setTimeout(this.handle_xmpp_presence.bind(this), 30000);
    }
  },

  handle_xmpp_vcard: function(stanza) {
    var name = stanza.attrs.to.split('/')[1];
    for (var user_id in this.campfire_users) {
      var user = this.campfire_users[user_id];
      if (user && user.name === name) {
        var vcard = new xmpp.Element('iq', {type: 'result', id: stanza.attrs.id})
            .c('vCard', {xmlns: 'vcard-temp'})
              .c('FN').t(name).up();

        if (user.email_address) {
          vcard.c('EMAIL').c('USERID').t(user.email_address);
        }

        if (user.avatar_url) {
          vcard.c('PHOTO').c('EXTVAL').t(user.avatar_url);
        }

        this.send_stanza(name, vcard);
        return;
      }
    }
    this.send_stanza(
      name, build_xmpp_error(
        stanza, 'cancel', 'item-not-found'));
  },

  refresh_room: function(callback) {
    var self = this;

    self.campfire.get(self.campfire_room.path, function(err, response) {
      if (!err) {
        try {
          // Build current roster
          var roster = response.room.users.map(function(user) {
            if (user.id != self.campfire_user.id) {
              self.send_stanza(
                user.name, build_xmpp_presence());
            }
            self.campfire_users[user.id] = user;
            return user.id;
          });

          // Send exit notifications
          self.campfire_roster.forEach(function(user_id) {
            if (roster.indexOf(user_id) < 0 && self.campfire_users[user_id]) {
              self.send_stanza(
                self.campfire_users[user_id].name, build_xmpp_presence(
                  {type: 'unavailable'}));
            }
          });

          self.campfire_roster = roster;

        } catch (e) {
          err = e;
        }
      }

      if (callback) {
        callback(err);
      } else if (err) {
        self.log('refresh_room:', err);
      }
    });
  },

  send_stanza: function(from_resource, stanza) {
    stanza = stanza.root();
    this._jid.setResource(from_resource);
    stanza.attrs.from = this._jid.toString();
    this.client.send_stanza(stanza);
    this.log('<<<', stanza.name);
  },

  campfire_start_listen: function() {
    this.campfire_stop_listen();

    if (this.present) {
      this._listen_req = this.campfire_room.listen(
        this.handle_campfire_message.bind(this));

      var self = this;
      this._listen_req.on('error', function(err) {
        self.log('listen', err);
        setTimeout(function() {
          self.campfire_start_listen();
        }, 1000);
      });
    }
  },

  campfire_stop_listen: function() {
    if (this._listen_req) {
      this._listen_req.abort();
    }
  },

  handle_campfire_message: function(msg, history) {
    var self = this;
    Step(
      function lookup_name() {
        if (msg.userId && self.campfire_users[msg.userId] === undefined) {
          self.campfire.user(msg.userId, this);
        } else {
          return null;
        }
      },

      function translate_message(err, response) {
        if (err == 404) {
          self.campfire_users[msg.userId] = null;
        } else if (err) {
          self.log('user', msg.userId, err);
        } else {
          // Cache user name if retrieved
          var _user = response && response.user;
          if (_user && _user.id) {
            self.campfire_users[_user.id] = _user;
          }
        }

        var user = self.campfire_users[msg.userId];

        var stanza = null;
        if (['TextMessage', 'PasteMessage', 'SystemMessage']
            .indexOf(msg.type) > -1) {
          stanza = build_xmpp_message(msg.body);

        } else if (msg.type === 'TweetMessage') {
          var body = msg.body;
          if (msg.tweet) {
            body = msg.tweet.message + ' -- @' + msg.tweet.author_username +
              ', ' + TWITTER_STATUS_PREFIX + msg.tweet.id;
          } else if (body.indexOf('---\n') === 0) {
            self.log('YAML tweet body without .tweet data');
          }
          stanza = build_xmpp_message(body);
        } else if (msg.type === 'TopicChangeMessage') {
          stanza = build_xmpp_message(msg.body, 'subject');

        } else if (msg.type === 'EnterMessage') {
          self.refresh_room();

        } else if (msg.type === 'LeaveMessage') {
          var i = self.campfire_roster.indexOf(msg.userId);
          if (i > -1 && user) {
            stanza = build_xmpp_presence({type: 'unavailable'});
          } else {
            self.refresh_room();
          }

        } else {
          self.log('unhandled campfire message', msg.type);
        }

        if (stanza) {
          if (history) {
            stanza.root()
              .c('delay', {xmlns: 'urn:xmpp:delay',
                           stamp: msg.createdAt.toISOString()});
          }

          self.send_stanza(
            user && user.name || 'User ' + msg.userId, stanza);
        }
      },

      function errors(err) {
        self.log('campfire_message error', err);
      }
    );
  },

  log: function() {
    Array.prototype.unshift.call(arguments, '-', this.jid.toString());
    this.client.log.apply(this.client, arguments);
  }
};

exports.XmppCampfireRoom = XmppCampfireRoom;

// XMPP Helpers

function build_xmpp_presence(options) {
  options = options || {};
  options.role = options.role || (
    options.type === 'unavailable' ? 'none' : 'participant');

  var stanza = new xmpp.Element('presence', {type: options.type})
        .c('x', {xmlns: MUC_USER_NS})
          .c('item', {role: options.role, affiliation: 'member'}).up();

  (options.status_codes || []).forEach(function(status_code) {
    stanza.c('status', {code: status_code});
  });

  return stanza;
}

function build_xmpp_message(body, body_type) {
  body_type = body_type || 'body';
  return new xmpp.Element('message', {type: 'groupchat'})
    .c(body_type)
      .t(body);
}

function build_xmpp_error(stanza, error_type, error, text) {
  if (typeof(stanza) === 'string') {
    stanza = {name: stanza, attrs: {}};
  }
  return new xmpp.Element(stanza.name, {type: 'error', id: stanza.attrs.id})
    .c('x', {xmlns: MUC_NS}).up()
    .c('error', {type: error_type})
      .c(error, {xmlns: ERROR_NS}).up()
      .c('text', {xmlns: ERROR_NS})
        .t(text || '');
}
<commit_msg>Add lock around initial room joining steps<commit_after>var Step     = require('step');
var xmpp     = require('node-xmpp');

var _campfire = require('../vendor/node-campfire/lib/campfire');
var Campfire  = _campfire.Campfire;
var Room      = _campfire.Room;

var ERROR_NS    = 'urn:ietf:params:xml:ns:xmpp-stanzas';
var MUC_NS      = 'http://jabber.org/protocol/muc';
var MUC_USER_NS = MUC_NS + '#user';

var TWITTER_STATUS_PREFIX = 'http://twitter.com/twitter/status/';

var XmppCampfireRoom = function(client, room_jid) {
  if (typeof(room_jid) === 'string') {
    room_jid = new xmpp.JID(room_jid);
  }

  this.client = client;
  this.jid = room_jid.bare();
  this.connecting = false;
  this.present = false;

  this.campfire = null;
  this.campfire_user = {};
  this.campfire_room = null;
  this.campfire_roster = [];
  this.campfire_users = {};

  // JID "buffer"
  this._jid = room_jid;
};

XmppCampfireRoom.prototype = {

  handle_stanza: function(stanza) {
    this.log('>>>', stanza.name);

    if (stanza.is('presence')) {
      if (stanza.attrs.type === 'unavailable') {
        if (this.present) {
          this.handle_exit(stanza);
        }
      } else if (stanza.children.length) {
        this.handle_enter(stanza);
      }

      if (stanza.attrs.type !== 'unsubscribed') {
        this.handle_xmpp_presence(stanza);
      }
    } else if (!this.present) {
      this.send_stanza(
        null, build_xmpp_error(
          stanza, 'modify', 'not-acceptable', 'Not in room'));
    } else if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
      this.handle_xmpp_message(stanza);
    } else if (stanza.is('iq')) {
      if (stanza.attrs.type === 'get' && stanza.getChild('vCard', 'vcard-temp')) {
        this.handle_xmpp_vcard(stanza);
      }
      this.send_stanza(
        stanza.attrs.from.split('/')[1], build_xmpp_error(
          stanza, 'cancel', 'service-unavailable'));
    } else {
      this.log('unhandled stanza:', stanza.toString());
    }
  },

  handle_enter: function(stanza) {
    var self = this;
    var options = {ssl: true};
    var room_name;

    Step(
      function auth() {
        if (self.present &&
            self._prev_id === stanza.attrs.id) {
          self.log('duplicate presence received');
          return;
        }
        self._prev_id = stanza.attrs.id;

        if (self.connecting) {
          self.log('already connecting');
          return;
        }
        self.connecting = true;

        var i = self.jid.user.indexOf('+');
        if (i > -1) {
          options.account = self.jid.user.substr(0, i).trim();
          room_name = self.jid.user.substr(i + 1).trim();
        } else {
          throw 302;
        }

        var x = stanza.getChild('x', MUC_NS);
        options.token = x ? x.getChildText('password') : null;
        if (!options.token) {
          throw 401;
        }

        self.campfire = new Campfire(options);

        if (options.token.length != 40) {
          var username = stanza.attrs.to.split('/')[1];
          self.campfire.authorization = 'Basic ' + new Buffer(
            username + ':' + options.token).toString('base64');
        }

        self.campfire.me(this);
      },

      function get_rooms(err, response) {
        if (err) throw err;

        self.campfire_user = response.user;
        self.campfire_users[response.user.id] = response.user;

        // Reset token in case we used username/password
        if (response.user.api_auth_token) {
          options.token = response.user.api_auth_token;
          self.campfire = new Campfire(options);
        }

        self.campfire.rooms(this);
      },

      function find_room(err, rooms) {
        if (err) throw err;

        // .some() to emulate for loop with break
        var found_room = null;
        rooms.some(function(room) {
          if (room.id == room_name ||
              room.name.trim().toLowerCase() == room_name.toLowerCase()) {
            found_room = room;
            return true;
          }
          return false;
        });

        if (found_room) {
          self.campfire_room = found_room;
          self.campfire_room.join(this);
        } else {
          throw 302;
        }
      },

      function join_room(err, room) {
        self.connecting = false;

        if (err == 302) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'cancel', 'item-not-found', 'Room not found'));
          self.log('not found');
          return;
        } else if (err == 401) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'auth', 'not-authorized', 'Auth failed'));
          self.log('not authorized');
          return;
        } else if (err) {
          throw err;
        }

        self.present = true;

        self.refresh_room(this);
        // TODO parallelize next steps?
      },

      function finish_join(err) {
        if (err) throw err;

        // Send user self presence
        self.send_stanza(
          self.campfire_user.name, build_xmpp_presence(
            {status_codes: [110, 170, 210]}));

        self.campfire_room.messages(this);
      },

      function send_room_state(err, messages) {
        if (err) {
          self.log('messages', err);
        };

        if (messages) {
          // Filter types for history
          messages = messages.filter(function(msg) {
            return ['TextMessage', 'PasteMessage', 'TweetMessage',
                    'SystemMessage', 'TopicChangeMessage'
                   ].indexOf(msg.type) > -1;
          });

          // Obey (some) client history limits
          try {
            var history = stanza.getChild('x', MUC_NS).getChild('history');
          } catch (e) {}
          if (history && history.attrs.maxstanzas) {
            messages.splice(0, messages.length - history.attrs.maxstanzas);
          }

          // Send messages through normal handler
          messages.forEach(function(msg) {
            self.handle_campfire_message(msg, true);
          });
        }

        if (self.campfire_room.topic) {
          self.send_stanza(
            null, build_xmpp_message(
              self.campfire_room.topic, 'subject'));
        }

        self.campfire_start_listen();
      },

      function errors(err) {
        self.send_stanza(
          null, build_xmpp_error(
            stanza, 'wait', 'internal-server-error', err));
        self.log('join_error', err);
      }
    );
  },

  handle_exit: function(stanza, status_code) {
    this.campfire_stop_listen();

    if (this.present) {
      var self = this;
      this.campfire_room.leave(function() {
        self.send_stanza(
          self.campfire_user.name, build_xmpp_presence(
            {type: 'unavailable', status_codes: [status_code || 110]}));
      });
    }

    this.present = false;
  },

  handle_xmpp_message: function(stanza) {
    var body = stanza.getChildText('body');
    if (body) {
      var self = this;
      this.campfire_room.message(body, undefined, function(err) {
        if (err) {
          self.send_stanza(
            null, build_xmpp_error(
              stanza, 'wait', 'internal-server-error', err));
        }
      });
    }
    // TODO handle other messages (subject change)
  },

  handle_xmpp_presence: function(stanza) {
    if (this._probe_timer) {
      clearTimeout(this._probe_timer);
      this._probe_timer = undefined;
    }

    if (this.presence) {
      if (!stanza) {
        this.send_stanza(
          null, new xmpp.Element('presence', {type: 'probe'}));
      }

      this._probe_timer = setTimeout(this.handle_xmpp_presence.bind(this), 30000);
    }
  },

  handle_xmpp_vcard: function(stanza) {
    var name = stanza.attrs.to.split('/')[1];
    for (var user_id in this.campfire_users) {
      var user = this.campfire_users[user_id];
      if (user && user.name === name) {
        var vcard = new xmpp.Element('iq', {type: 'result', id: stanza.attrs.id})
            .c('vCard', {xmlns: 'vcard-temp'})
              .c('FN').t(name).up();

        if (user.email_address) {
          vcard.c('EMAIL').c('USERID').t(user.email_address);
        }

        if (user.avatar_url) {
          vcard.c('PHOTO').c('EXTVAL').t(user.avatar_url);
        }

        this.send_stanza(name, vcard);
        return;
      }
    }
    this.send_stanza(
      name, build_xmpp_error(
        stanza, 'cancel', 'item-not-found'));
  },

  refresh_room: function(callback) {
    var self = this;

    self.campfire.get(self.campfire_room.path, function(err, response) {
      if (!err) {
        try {
          // Build current roster
          var roster = response.room.users.map(function(user) {
            if (user.id != self.campfire_user.id) {
              self.send_stanza(
                user.name, build_xmpp_presence());
            }
            self.campfire_users[user.id] = user;
            return user.id;
          });

          // Send exit notifications
          self.campfire_roster.forEach(function(user_id) {
            if (roster.indexOf(user_id) < 0 && self.campfire_users[user_id]) {
              self.send_stanza(
                self.campfire_users[user_id].name, build_xmpp_presence(
                  {type: 'unavailable'}));
            }
          });

          self.campfire_roster = roster;

        } catch (e) {
          err = e;
        }
      }

      if (callback) {
        callback(err);
      } else if (err) {
        self.log('refresh_room:', err);
      }
    });
  },

  send_stanza: function(from_resource, stanza) {
    stanza = stanza.root();
    this._jid.setResource(from_resource);
    stanza.attrs.from = this._jid.toString();
    this.client.send_stanza(stanza);
    this.log('<<<', stanza.name);
  },

  campfire_start_listen: function() {
    this.campfire_stop_listen();

    if (this.present) {
      this._listen_req = this.campfire_room.listen(
        this.handle_campfire_message.bind(this));

      var self = this;
      this._listen_req.on('error', function(err) {
        self.log('listen', err);
        setTimeout(function() {
          self.campfire_start_listen();
        }, 1000);
      });
    }
  },

  campfire_stop_listen: function() {
    if (this._listen_req) {
      this._listen_req.abort();
    }
  },

  handle_campfire_message: function(msg, history) {
    var self = this;
    Step(
      function lookup_name() {
        if (msg.userId && self.campfire_users[msg.userId] === undefined) {
          self.campfire.user(msg.userId, this);
        } else {
          return null;
        }
      },

      function translate_message(err, response) {
        if (err == 404) {
          self.campfire_users[msg.userId] = null;
        } else if (err) {
          self.log('user', msg.userId, err);
        } else {
          // Cache user name if retrieved
          var _user = response && response.user;
          if (_user && _user.id) {
            self.campfire_users[_user.id] = _user;
          }
        }

        var user = self.campfire_users[msg.userId];

        var stanza = null;
        if (['TextMessage', 'PasteMessage', 'SystemMessage']
            .indexOf(msg.type) > -1) {
          stanza = build_xmpp_message(msg.body);

        } else if (msg.type === 'TweetMessage') {
          var body = msg.body;
          if (msg.tweet) {
            body = msg.tweet.message + ' -- @' + msg.tweet.author_username +
              ', ' + TWITTER_STATUS_PREFIX + msg.tweet.id;
          } else if (body.indexOf('---\n') === 0) {
            self.log('YAML tweet body without .tweet data');
          }
          stanza = build_xmpp_message(body);
        } else if (msg.type === 'TopicChangeMessage') {
          stanza = build_xmpp_message(msg.body, 'subject');

        } else if (msg.type === 'EnterMessage') {
          self.refresh_room();

        } else if (msg.type === 'LeaveMessage') {
          var i = self.campfire_roster.indexOf(msg.userId);
          if (i > -1 && user) {
            stanza = build_xmpp_presence({type: 'unavailable'});
          } else {
            self.refresh_room();
          }

        } else {
          self.log('unhandled campfire message', msg.type);
        }

        if (stanza) {
          if (history) {
            stanza.root()
              .c('delay', {xmlns: 'urn:xmpp:delay',
                           stamp: msg.createdAt.toISOString()});
          }

          self.send_stanza(
            user && user.name || 'User ' + msg.userId, stanza);
        }
      },

      function errors(err) {
        self.log('campfire_message error', err);
      }
    );
  },

  log: function() {
    Array.prototype.unshift.call(arguments, '-', this.jid.toString());
    this.client.log.apply(this.client, arguments);
  }
};

exports.XmppCampfireRoom = XmppCampfireRoom;

// XMPP Helpers

function build_xmpp_presence(options) {
  options = options || {};
  options.role = options.role || (
    options.type === 'unavailable' ? 'none' : 'participant');

  var stanza = new xmpp.Element('presence', {type: options.type})
        .c('x', {xmlns: MUC_USER_NS})
          .c('item', {role: options.role, affiliation: 'member'}).up();

  (options.status_codes || []).forEach(function(status_code) {
    stanza.c('status', {code: status_code});
  });

  return stanza;
}

function build_xmpp_message(body, body_type) {
  body_type = body_type || 'body';
  return new xmpp.Element('message', {type: 'groupchat'})
    .c(body_type)
      .t(body);
}

function build_xmpp_error(stanza, error_type, error, text) {
  if (typeof(stanza) === 'string') {
    stanza = {name: stanza, attrs: {}};
  }
  return new xmpp.Element(stanza.name, {type: 'error', id: stanza.attrs.id})
    .c('x', {xmlns: MUC_NS}).up()
    .c('error', {type: error_type})
      .c(error, {xmlns: ERROR_NS}).up()
      .c('text', {xmlns: ERROR_NS})
        .t(text || '');
}
<|endoftext|>