Junction

XMPP middleware framework

client

lib/junction/client.js

Module dependencies.

var xmpp = require('node-xmpp')
  , uuid = require('node-uuid')
  , util = require('util')
  , assert = require('assert')
  , NullStream = require('./nullstream');

Initialize a new Client.

  • param: Object options

function Client(options) {
  options = options || {}
  // WORKAROUND: Disable socket streams in underlying node-xmpp, useful in cases
  //             where one is not necessary (such as mounted connections or
  //             tests).
  this._disableStream = options.disableStream || false;
  
  xmpp.Client.call(this, options);
  this._stack = [];
  this._filters = [];
  this.addListener('stanza', this.handle);
}

Inherit from xmpp.Client.

util.inherits(Client, xmpp.Client);

Utilize the given middleware handle.

  • param: Function handle

  • return: Connection for chaining

Client.prototype.use = function(ns, handle) {
  // TODO: Allow ns to be set as an XPath expression, in order to limit the
  //       scope of the handler.
  // TODO: Alternatively, let the qualifier be the JID to which the stanza is
  //       addressed.  This would be beneficial for components that handle a
  //       domain, but want to be addressed using node@domain.  I'm leaning in
  //       favor of this approach, rather than the XPath one.
  if ('string' != typeof ns) {
    handle = ns;
    ns = null;
  }
  
  // wrap sub-apps
  // TODO: Implement test cases for this functionality
  if ('function' == typeof handle.handle) {
    var connection = handle;
    connection.send = this.send.bind(this);
    connection.generateID = this.generateID.bind(this);
    handle = function(stanza, next){
      connection.handle(stanza, next);
    };
  }
  
  this._stack.push({ ns: ns, handle: handle });
  return this;
}

Utilize the given filter handle.

  • param: Function handle

  • return: Connection for chaining

Client.prototype.filter = function(handle) {
  this._filters.push({ handle: handle });
  return this;
}

Send stanza, running it through the filter chain.

  • param: Element | XMLElement stanza

Client.prototype.send = function(stanza) {
  if (stanza.toXML) {
    stanza = stanza.toXML();
  }
  
  if (stanza.root) {
      console.log('SEND: ' + stanza.root().toString() + '\n');
  } else {
      console.log('SEND: ' + stanza + '\n');
  }
  
  var filters = this._filters;
  var idx = 0;
  function next() {
    var filter = filters[idx++];
    
    // all done
    if (!filter) { return; }
    
    // TODO: Implement error handling
    try {
      filter.handle(stanza, next);
    } catch (e) {
      if (e instanceof assert.AssertionError) {
        console.error(e.stack + '\n');
        next();
      } else {
        console.error(e.stack + '\n');
        next();
      }
    }
  }
  if (stanza instanceof xmpp.Element) { next(); }
  
  xmpp.Client.prototype.send.call(this, stanza);
}

Generate a unique identifier, for use in a stanza's id attribute.

  • return: String

Client.prototype.generateID = function() {
  return uuid();
}

Expose Client.

module.exports = Client;

component

lib/junction/component.js

Module dependencies.

var xmpp = require('node-xmpp')
  , util = require('util')
  , Client = require('./client');

Initialize a new Component.

  • param: Object options

  • api: public

function Component(options) {
  options = options || {}
  // WORKAROUND: Disable socket streams in underlying node-xmpp, useful in cases
  //             where one is not necessary.  See Client.prototype.setupStream
  //             for implementation.
  this._disableStream = options.disableStream || false;
  
  xmpp.Component.call(this, options);
  this._stack = [];
  this._filters = [];
  this.addListener('stanza', this.handle);
}

Inherit from xmpp.Component.

util.inherits(Component, xmpp.Component);

Mixin Client methods.

Object.keys(Client.prototype).forEach(function(method){
  Component.prototype[method] = Client.prototype[method];
});

Expose Component.

module.exports = Component;

element

lib/junction/elements/element.js

Module dependencies.

var util = require('util')
  , XMLElement = require('node-xmpp').Element;

Initialize a new Element.

  • param: String name

  • param: String xmlns

  • api: public

function Element(name, xmlns) {
  if (!name) { throw new Error('Element requires a name'); }
  
  this.parent = null;
  this.name = name;
  this.xmlns = xmlns;
  this.children = [];
}

Add a child element.

  • param: Element | XMLElement child

  • param: Object attrs (optional)

  • api: public

Element.prototype.c = function(child, attrs) {
  if (attrs) {
    child = new XMLElement(child, attrs);
  }
  this.children.push(child);
  child.parent = this;
  return child;
}

Set the text value.

  • param: String text

  • api: public

Element.prototype.t = function(text) {
  this.children.push(text);
  return this;
};

Return the parent element, or this if root.

  • api: public

Element.prototype.up = function() {
  if (this.parent) {
    return this.parent;
  } else {
    return this;
  }
};

Serialize to XML.

  • api: public

Element.prototype.toXML = function() {
  var attrs = this.xmlAttributes();
  attrs['xmlns'] = this.xmlns;
  var xml = new XMLElement(this.name, attrs);
  this.children.forEach(function(child) {
    if (child.toXML) { // instances of junction.Element
      xml.children.push(child.toXML());
    } else if (child.toString) { // instances of ltx.Element
      xml.children.push(child);
    } else if (typeof child === 'string') {
      xml.children.push(child);
    }
  });
  return xml;
}

Build XML attributes.

This function is intended to be overrriden by subclasses, in order to serialize attributes.

  • api: protected

Element.prototype.xmlAttributes = function() {
  return {};
}

Expose Element.

exports = module.exports = Element;

iq

lib/junction/elements/iq.js

Module dependencies.

var util = require('util')
  , Element = require('./element');

Initialize a new IQ element.

  • param: String to

  • param: String from

  • param: String type

  • api: public

function IQ(to, from, type) {
  if ('string' != typeof type) {
    type = from;
    from = null;
  }
  type = type || 'get';
  
  Element.call(this, 'iq');
  this.id = null;
  this.to = to;
  this.from = from;
  this.type = type;
}

Inherit from Element.

util.inherits(IQ, Element);

Expose IQ.

exports = module.exports = IQ;

message

lib/junction/elements/message.js

Module dependencies.

var util = require('util')
  , Element = require('./element');

Initialize a new Message element.

  • param: String to

  • param: String from

  • param: String type

  • api: public

function Message(to, from, type) {
  if ('string' != typeof type) {
    type = from;
    from = null;
  }
  
  Element.call(this, 'message');
  this.to = to || null;
  this.from = from || null;
  this.type = type || null;
}

Inherit from Element.

util.inherits(Message, Element);

Expose Message.

exports = module.exports = Message;

presence

lib/junction/elements/presence.js

Module dependencies.

var util = require('util')
  , Element = require('./element');

Initialize a new Presence element.

  • param: String to

  • param: String from

  • param: String type

  • api: public

function Presence(to, from, type) {
  if ('string' != typeof type) {
    type = from;
    from = null;
  }
  
  Element.call(this, 'presence');
  this.to = to || null;
  this.from = from || null;
  this.type = type || null;
}

Inherit from Element.

util.inherits(Presence, Element);

Expose Presence.

exports = module.exports = Presence;

pending

lib/junction/filters/pending.js

Module dependencies.

var util = require('util');

Save pending data to pending store.

This filter processes outgoing stanzas, saving any data attached to the pending property to the pending store. Pending data is typically set by applying additional filter()'s to the connection. Such filters process outgoing stanzas and record any data necessary to interpret the eventual response.

When used in conjunction with the pending middleware, the data saved in the pending store will be loaded when the corresponding response stanza arrives.

This allows a stateless, shared-nothing architecture to be utilized in XMPP-based systems. This is particularly advantageous in systems employing XMPP component connections with round-robin load balancing strategies. In such a scenario, requests can be sent via one component instance, while the result can be received and processed by an entirely separate component instance.

Options

  • store pending store instance

Examples

 var store = new junction.pending.MemoryStore();

 connection.filter(disco.filters.infoQuery());
 connection.filter(junction.filters.pending({ store: store }));

 connection.use(junction.pending({ store: store }));
 connection.use(function(stanza, next) {
   if (stanza.inResponseTo) {
     console.log('response received!');
     return;
   }
   next();
 });

  • param: Object options

  • return: Function

  • api: public

module.exports = function pending(options) {
  options = options || {};
  var store = options.store;
  
  if (!store) throw new Error('pending filter requires a store');
  
  return function pending(stanza, next) {
    if (!stanza.attrs.id || !stanza.pending) {
      return next();
    }
    
    var key = stanza.attrs.to + ':' + stanza.attrs.id;
    store.set(key, stanza.pending, function(err) {
      next(err);
    });
  }
}

index

lib/junction/index.js

Module dependencies.

var fs = require('fs')
  , xmpp = require('node-xmpp')
  , Client = require('./client')
  , Component = require('./component')
  , StanzaError = require('./stanzaerror');

Framework version.

exports.version = '0.1.0';

Create a new Junction connection.

Options

  • jid JID
  • password Password, for authentication
  • host
  • port
  • type Type of connection, see below for types
  • disableStream Disable underlying stream, defaults to false

Connection Types

  • client XMPP client connection
  • component XMPP component connection

Examples

var client = junction.createConnection({
  type: 'client',
  jid: 'user@example.com',
  password: 'secret',
  host: 'example.com',
  port: 5222
});

  • param: Object options

  • return: Connection

  • api: public

exports.createConnection = function (options) {
  if (options.type == 'component') {
    return new Component(options);
  }
  return new Client(options);
};

Expose constructors.

exports.Client = Client;
exports.Component = Component;
exports.JID = xmpp.JID;
exports.XMLElement = xmpp.Element;
exports.StanzaError = StanzaError;

Expose element constructors.

exports.elements = {};
exports.elements.Element = require('./elements/element');
exports.elements.IQ = require('./elements/iq');
exports.elements.Message = require('./elements/message');
exports.elements.Presence = require('./elements/presence');

Expose bundled filters.

exports.filters = {};
exports.filters.pending = require('./filters/pending');

Auto-load bundled middleware.

exports.middleware = {};

fs.readdirSync(__dirname + '/middleware').forEach(function(filename) {
  if (/\.js$/.test(filename)) {
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function(){
      return require('./middleware/' + name);
    });
  }
});

Expose middleware as first-class exports.

exports.__proto__ = exports.middleware;

attentionParser

lib/junction/middleware/attentionParser.js

Parse elements intended to get the attention of a user.

This middleware parses elements within the urn:xmpp:attention:0 namespace. If attention is requested, stanza.attention will be set to true.

Examples

 connection.use(junction.attentionParser());

References

  • return: Function

  • api: public

module.exports = function attentionParser() {
  
  return function attentionParser(stanza, next) {
    if (!stanza.is('message')) { return next(); }
    var attention = stanza.getChild('attention', 'urn:xmpp:attention:0');
    if (!attention) { return next(); }
    
    stanza.attention = true;
    next();
  }
}

capabilitiesParser

lib/junction/middleware/capabilitiesParser.js

Parse entity capabilities broadcast in presence stanzas.

This middleware parses entity capabilities present in presence stanzas. stanza.capabilities.node will be set to a URI that uniquely identifies a software application. stanza.capabilities.verification will be set to a string used to verify the identity and supported features of the entity. stanza.capabilities.hash will indicate the hashing algorithm used to generate the verification string

Examples

 connection.use(junction.capabilitiesParser());

References

  • return: Function

  • api: public

module.exports = function capabilitiesParser() {
  
  return function capabilitiesParser(stanza, next) {
    if (!stanza.is('presence')) { return next(); }
    var c = stanza.getChild('c', 'http://jabber.org/protocol/caps');
    if (!c) { return next(); }
    
    stanza.capabilities = {};
    stanza.capabilities.node = c.attrs.node;
    stanza.capabilities.verification = c.attrs.ver;
    stanza.capabilities.hash = c.attrs.hash;
    next();
  }
}

delayParser

lib/junction/middleware/delayParser.js

Module dependencies.

var JID = require('node-xmpp').JID;

Parse information indicating an XMPP stanza has been delivered with a delay.

This middleware parses delay information contained within message and presence stanzas. stanza.delayedBy indicates the Jabber ID of the entity that delayed the delivery of the stana. stanza.originallySentAt indicates the time the stanza was originally sent.

Examples

 connection.use(junction.delayParser());

References

  • return: Function

  • api: public

module.exports = function delayParser() {
  
  return function delayParser(stanza, next) {
    if (!(stanza.is('message') || stanza.is('presence'))) { return next(); }
    var delay = stanza.getChild('delay', 'urn:xmpp:delay');
    if (!delay) { return next(); }
    
    stanza.delayedBy = new JID(delay.attrs.from);
    stanza.originallySentAt = new Date(delay.attrs.stamp);
    next();
  }
}

errorHandler

lib/junction/middleware/errorHandler.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Flexible error handler, providing error responses containing a message, application-specific error conditions, and optional stack traces.

Options

  • includeStanza include the original stanza in the error response. Defaults to false (not implemented)
  • showStack respond with both the error message and stack trace. Defaults to false
  • dumpExceptions dump exceptions to stderr (without terminating the process). Defaults to false

Examples

 connection.use(junction.errorHandler());

 connection.use(
   junction.errorHandler({ showStack: true, dumpExceptions: true })
 );

Middleware can then next() with an error:

 next(new Error("You're doing it wrong!"));

 next(new StanzaError("Administrator privileges required.", "auth", "forbidden"));

 var err = new StanzaError('', 'modify', 'bad-request');
 err.specificCondition = { name: 'invalid-jid', xmlns: 'http://jabber.org/protocol/pubsub#errors' };
 next(err);

  • param: Object options

  • return: Function

  • api: public

module.exports = function errorHandler(options) {
  options = options || {};
  
  var includeStanza = options.includeStanza || false;
  var showStack = options.showStack || false;
  var dumpExceptions = options.dumpExceptions || false;

  return function errorHandler(err, req, res, next) {
    if (dumpExceptions) { console.error(err.stack); }
    
    // An error was encountered, but no response mechanism is available to
    // return information to the requesting entity.
    if (!res) { return next(err); }
    
    var type = err.type || 'wait';
    var condition = err.condition || 'internal-server-error';
    var message = err.message || '';
    if (showStack) {
      message += '\n';
      message += err.stack;
    }
    
    res.attrs.type = 'error';
    if (includeStanza) {
      // TODO: Implement support for including XML stanza that triggered the
      //       error.
    }
    var errorEl = res.c('error', { type: type }).c(condition, { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' }).up();
    if (message && message.length) {
      errorEl.c('text', { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' }).t(message)
    }
    if (err.specificCondition && err.specificCondition.name) {
      errorEl.c(err.specificCondition.name, { xmlns: err.specificCondition.xmlns })
    }
    res.send();
  }
}

lastActivity

lib/junction/middleware/lastActivity.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Handle requests for the last activity associated with an XMPP entity.

This middleware handles IQ-get requests within the jabber:iq:last XML namespace. By default, the middleware responds with the uptime of the entity, measured in seconds.

Examples

 connection.use(junction.lastActivity());

 connection.use(
   junction.lastActivity(function() {
     return 31556926;
   })
 );

References

  • param: Function callback

  • return: Function

  • api: public

module.exports = function lastActivity(callback) {
  callback = callback || uptime;
  
  var start = Date.now();
  function uptime() {
    return Math.round((Date.now() - start) / 1000);
  }
  
  return function lastActivity(req, res, next) {
    if (!req.is('iq')) { return next(); }
    if (req.type == 'result' || req.type == 'error') { return next(); }
    var query = req.getChild('query', 'jabber:iq:last');
    if (!query) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Query must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    
    var now = Date.now();
    var q = res.c('query', { xmlns: 'jabber:iq:last', seconds: callback() });
    res.send();
  }
}

lastActivityResultParser

lib/junction/middleware/lastActivityResultParser.js

Parse information about the last activity associated with an XMPP entity.

This middleware parses last activity information contained within IQ-result stanzas. stanza.lastActivity indicates the time of last activity of the entity, in seconds. stanza.lastStatus indicates last status of the entity.

Examples

 connection.use(junction.lastActivityResultParser());

References

  • return: Function

  • api: public

module.exports = function lastActivityResultParser() {
  
  return function lastActivityResultParser(stanza, next) {
    if (!stanza.is('iq')) { return next(); }
    if (stanza.type != 'result') { return next(); }
    var query = stanza.getChild('query', 'jabber:iq:last');
    if (!query) { return next(); }
    
    stanza.lastActivity = query.attrs.seconds;
    stanza.lastStatus = query.getText();
    next();
  }
}

legacyDelayParser

lib/junction/middleware/legacyDelayParser.js

Module dependencies.

var JID = require('node-xmpp').JID;

Parse legacy information indicating an XMPP stanza has been delivered with a delay.

This middleware parses legacy delay information contained within message and presence stanzas. stanza.delayedBy indicates the Jabber ID of the entity that delayed the delivery of the stana. stanza.originallySentAt indicates the time the stanza was originally sent.

Examples

 connection.use(junction.legacyDelayParser());

References

  • return: Function

  • api: public

module.exports = function legacyDelayParser() {
  
  return function legacyDelayParser(stanza, next) {
    if (!(stanza.is('message') || stanza.is('presence'))) { return next(); }
    var delay = stanza.getChild('x', 'jabber:x:delay');
    if (!delay) { return next(); }
    
    stanza.delayedBy = new JID(delay.attrs.from);
    var match = /(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(delay.attrs.stamp);
    if (match) {
      stanza.originallySentAt = new Date(Date.UTC(match[1], match[2] - 1, match[3], match[4], match[5], match[6]));
    }
    
    next();
  }
}

legacyTime

lib/junction/middleware/legacyTime.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Handle legacy requests for the local time of an XMPP entity.

This middleware handles IQ-get requests within the jabber:iq:time XML namespace. The middleware responds with the UTC time of the entity, and optionally the time zone and a human-readable display string.

Options

  • timezone a string containing the time zone, typically a three-letter acronym
  • display a string containing the time in a human-readable format

Examples

 connection.use(junction.legacyTime());

 connection.use(
   junction.legacyTime({ timezone: 'MDT', display: 'Tue Sep 10 12:58:35 2002' })
 );

References

  • param: Object options

  • return: Function

  • api: public

module.exports = function legacyTime(options) {
  options = options || {};
  
  return function legacyTime(req, res, next) {
    if (!req.is('iq')) { return next(); }
    if (req.type == 'result' || req.type == 'error') { return next(); }
    var query = req.getChild('query', 'jabber:iq:time');
    if (!query) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Query must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    
    var now = options.date || new Date();
    var t = res.c('query', { xmlns: 'jabber:iq:time' });
    t.c('utc').t(LegacyXMPPDateTimeString(now));
    if (options.timezone) { t.c('tz').t(options.timezone); }
    if (options.display) { t.c('display').t(options.display); }
    res.send();
  }
}


function LegacyXMPPDateTimeString(d) {
  function pad(n) { return n < 10 ? '0' + n : n.toString() }
  return d.getUTCFullYear()
    + pad(d.getUTCMonth() + 1)
    + pad(d.getUTCDate()) + 'T'
    + pad(d.getUTCHours()) + ':'
    + pad(d.getUTCMinutes()) + ':'
    + pad(d.getUTCSeconds())
}

legacyTimeResultParser

lib/junction/middleware/legacyTimeResultParser.js

Parse legacy information about the local time of an XMPP entity.

This middleware parses legacy local time information contained within IQ-result stanzas. stanza.utcDate indicates the UTC time according to the responding entity. stanza.timezone indicates the time zone in which the responding entity is located. stanza.display contains the time in a human-readable format.

Examples

 connection.use(junction.legacyTimeResultParser());

References

  • return: Function

  • api: public

module.exports = function legacyTimeResultParser() {
  
  return function legacyTimeResultParser(stanza, next) {
    if (!stanza.is('iq')) { return next(); }
    if (stanza.type != 'result') { return next(); }
    var query = stanza.getChild('query', 'jabber:iq:time');
    if (!query) { return next(); }
    
    var utcEl = query.getChild('utc');
    if (utcEl) {
      var match = /(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(utcEl.getText());
      if (match) {
        stanza.utcDate = new Date(Date.UTC(match[1], match[2] - 1, match[3], match[4], match[5], match[6]));
      }
    }
    var tzEl = query.getChild('tz');
    if (tzEl) {
      stanza.timezone = tzEl.getText();
    }
    var displayEl = query.getChild('display');
    if (displayEl) {
      stanza.display = displayEl.getText();
    }
    next();
  }
}

logger

lib/junction/middleware/logger.js

module.exports = function logger(options) { options = options || {};

var stream = options.stream || process.stdout;

// @todo: Determine if there is a standard format for XMPP stanza logging. return function logger(stanza, next) { stream.write('RECV: ' + stanza.toString() + '\n'); next(); } }

message

lib/junction/middleware/message.js

Module dependencies.

var events = require('events');
var util = require('util');
require('../../node-xmpp/element_ext');

Handle message stanzas.

This middleware allows applications to handle message stanzas. Applications provide a callback(handler) which the middleware calls with an instance of EventEmitter. Listeners can be attached to handler in order to process presence stanza.

Events

  • chat the message is sent in the context of a one-to-one chat session
  • groupchat the message is sent in the context of a multi-user chat environment
  • normal the message is a standalone message that is sent outside the context of a one-to-one conversation or multi-user chat environment, and to which it is expected that the recipient will reply
  • headline the message provides an alert, a notification, or other transient information to which no reply is expected
  • err an error has occurred regarding processing of a previously sent message stanza

Examples

 connection.use(
   junction.message(function(handler) {
     handler.on('chat', function(stanza) {
       console.log('someone is chatting!');
     });
     handler.on('groupchat', function(stanza) {
       console.log('someone is group chatting!');
     });
   })
 );

References

  • param: Function fn

  • return: Function

  • api: public

module.exports = function message(fn) {
  if (!fn) throw new Error('message middleware requires a callback function');
  
  var handler = new Handler();
  fn.call(this, handler);
  
  return function message(stanza, next) {
    if (!stanza.isMessage()) { return next(); }
    handler._handle(stanza);
    next();
  }
}

Inherit from EventEmitter.

util.inherits(Handler, events.EventEmitter);

messageParser

lib/junction/middleware/messageParser.js

Parse message stanzas.

This middleware parses the standard elements contained in message stanzas. stanza.subject indicates the topic of the message. stanza.body contains the contents of the message. stanza.thread is used to identify a conversation thread. stanza.parentThread is used to identify another thread of which the current thread is an offshoot.

Examples

 connection.use(junction.messageParser());

References

  • return: Function

  • api: public

module.exports = function messageParser() {
  
  return function messageParser(stanza, next) {
    if (!stanza.is('message')) { return next(); }
    
    var subject = stanza.getChild('subject');
    if (subject) { stanza.subject = subject.getText(); }
    
    var body = stanza.getChild('body');
    if (body) { stanza.body = body.getText(); }
    
    var thread = stanza.getChild('thread');
    if (thread) {
      stanza.thread = thread.getText();
      stanza.parentThread = thread.attrs.parent;
    }
    
    next();
  }
}

nicknameParser

lib/junction/middleware/nicknameParser.js

Parse user nicknames.

This middleware parses user nicknames contained within presence subscription requests and messages. stanza.nickname indicates the nickname, asserted by the sending XMPP user.

Examples

 connection.use(junction.nicknameParser());

References

  • return: Function

  • api: public

module.exports = function nicknameParser() {
  
  return function nicknameParser(stanza, next) {
    if (!(stanza.is('message') || stanza.is('presence'))) { return next(); }
    var nick = stanza.getChild('nick', 'http://jabber.org/protocol/nick');
    if (!nick) { return next(); }
    
    stanza.nickname = nick.getText();
    next();
  }
}

memorystore

lib/junction/middleware/pending/memorystore.js

Module dependencies.

var util = require('util')
  , Store = require('./store');

Initialize a new MemoryStore.

Options

  • timeout expire keys in the store after timeout milliseconds

  • param: Object options

function MemoryStore(options) {
  options = options || {};
  this._hash = {};
  this._timeout = options.timeout;
}

Inherit from Store.

util.inherits(MemoryStore, Store);

Fetch data with the given key from the pending store.

  • param: String key

  • param: Function fn

  • api: public

MemoryStore.prototype.get = function(key, fn) {
  var self = this;
  process.nextTick(function(){
    var data = self._hash[key];
    if (data) {
      data = JSON.parse(data);
      fn(null, data);
    } else {
      fn();
    }
  });
}

Commit data associated with the given key to the pending store.

  • param: String key

  • param: Object data

  • param: Function fn

  • api: public

MemoryStore.prototype.set = function(key, data, fn) {
  var self = this;
  process.nextTick(function(){
    self._hash[key] = JSON.stringify(data);
    fn && fn();
  });
  if (this._timeout) {
    setTimeout(function() {
      self.remove(key);
    }, this._timeout);
  }
};

Remove data associated with the given key from the pending store.

  • param: String key

  • param: Function fn

  • api: public

MemoryStore.prototype.remove = function(key, fn) {
  var self = this;
  process.nextTick(function(){
    delete self._hash[key];
    fn && fn();
  });
};

Expose MemoryStore.

module.exports = MemoryStore;

store

lib/junction/middleware/pending/store.js

Initialize abstract Store.

  • api: public

function Store() {
}

Get data from the pending store.

This function must be overridden by subclasses. In abstract form, it always throws an exception.

  • param: String key

  • param: Function callback

  • api: protected

Store.prototype.get = function(key, callback) {
  throw new Error('Store#get must be overridden by subclass');
}

Set data to the pending store.

This function must be overridden by subclasses. In abstract form, it always throws an exception.

  • param: String key

  • param: Object data

  • param: Function callback

  • api: protected

Store.prototype.set = function(key, data, callback) {
  throw new Error('Store#set must be overridden by subclass');
}

Remove data from the pending store.

This function must be overridden by subclasses. In abstract form, it always throws an exception.

  • param: String key

  • param: Function callback

  • api: protected

Store.prototype.remove = function(key, callback) {
  throw new Error('Store#remove must be overridden by subclass');
}

Expose Store.

module.exports = Store;

pending

lib/junction/middleware/pending.js

Module dependencies.

var util = require('util')
  , Store = require('./pending/store')
  , MemoryStore = require('./pending/memorystore');

Setup pending store with the given options.

This middleware processes incoming response stanzas, restoring any pending data set when the corresponding request was sent. Pending data is typically set by applying filter()'s to the connection. The pending data can be accessed via the stanza.irt property, also aliased to stanza.inReplyTo, stanza.inResponseTo and stanza.regarding. Data is (generally) serialized as JSON by the store.

This allows a stateless, shared-nothing architecture to be utilized in XMPP-based systems. This is particularly advantageous in systems employing XMPP component connections with round-robin load balancing strategies. In such a scenario, requests can be sent via one component instance, while the result can be received and processed by an entirely separate component instance.

Options

  • store pending store instance
  • autoRemove automatically remove data when the response is received. Defaults to true

Examples

 connection.use(junction.pending({ store: new junction.pending.MemoryStore() }));


 var store = new junction.pending.MemoryStore();

 connection.filter(disco.filters.infoQuery());
 connection.filter(junction.filters.pending({ store: store }));

 connection.use(junction.pending({ store: store }));
 connection.use(function(stanza, next) {
   if (stanza.inResponseTo) {
     console.log('response received!');
     return;
   }
   next();
 });

  • param: Object options

  • return: Function

  • api: public

function pending(options) {
  options = options || {};
  var store = options.store;
  var autoRemove = (typeof options.autoRemove === 'undefined') ? true : options.autoRemove;
  
  if (!store) throw new Error('pending middleware requires a store');
  
  return function pending(stanza, next) {
    if (!stanza.id || !(stanza.type == 'result' || stanza.type == 'error')) {
      return next();
    }
    
    var key = stanza.from + ':' + stanza.id;
    store.get(key, function(err, data) {
      if (err) {
        next(err);
      } else if (!data) {
        next();
      } else {
        stanza.irt =
        stanza.inReplyTo = 
        stanza.inResponseTo =
        stanza.regarding = data;
        if (autoRemove) { store.remove(key); }
        next();
      }
    });
  }
}

Expose the middleware.

exports = module.exports = pending;

Expose constructors.

exports.Store = Store;
exports.MemoryStore = MemoryStore;

ping

lib/junction/middleware/ping.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Handle application-level pings sent over XMPP stanzas.

Examples

 connection.use(junction.ping());

References

  • return: Function

  • api: public

module.exports = function ping() {
  
  return function ping(req, res, next) {
    if (!req.is('iq')) { return next(); }
    var ping = req.getChild('ping', 'urn:xmpp:ping');
    if (!ping) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Ping must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    
    // Send ping response.
    res.send();
  }
}

presence

lib/junction/middleware/presence.js

Module dependencies.

var events = require('events');
var util = require('util');
require('../../node-xmpp/element_ext');

Handle presence stanzas.

This middleware allows applications to handle presence stanzas. Applications provide a callback(handler) which the middleware calls with an instance of EventEmitter. Listeners can be attached to handler in order to process presence stanza.

Events

  • available the sending entity is available for communication
  • unavailable the sending entity is no longer available for communication
  • probe the sending entity requests the recipient's current presence
  • subscribe the sending entity wishes to subscribe to the recipient's presence
  • subscribed the sending entity has allowed the recipient to receive their presence
  • unsubscribe the sending entity is unsubscribing from the recipient's presence
  • unsubscribed the sending entity has denied a subscription request or has canceled a previously granted subscription
  • err an error has occurred regarding processing of a previously sent presence stanza

Examples

 connection.use(
   junction.presence(function(handler) {
     handler.on('available', function(stanza) {
       console.log('someone is available!');
     });
     handler.on('unavailable', function(stanza) {
       console.log('someone is unavailable.');
     });
   })
 );

References

  • param: Function fn

  • return: Function

  • api: public

module.exports = function presence(fn) {
  if (!fn) throw new Error('presence middleware requires a callback function');
  
  var handler = new Handler();
  fn.call(this, handler);
  
  return function presence(stanza, next) {
    if (!stanza.isPresence()) { return next(); }
    handler._handle(stanza);
    next();
  }
}

Inherit from EventEmitter.

util.inherits(Handler, events.EventEmitter);

presenceParser

lib/junction/middleware/presenceParser.js

Parse presence stanzas.

This middleware parses the standard elements contained in presence stanzas. stanza.show indicates the availability sub-state of an entity. stanza.status contains a human-readable description of an entity's availability. stanza.priority specifies the priority level of the resource.

Examples

 connection.use(junction.presenceParser());

References

  • return: Function

  • api: public

module.exports = function presenceParser() {
  
  return function presenceParser(stanza, next) {
    if (!stanza.is('presence')) { return next(); }
    
    var show = stanza.getChild('show');
    if (show) {
      stanza.show = show.getText();
    } else if (!stanza.attrs.type) {
      stanza.show = 'online';
    }
    
    var status = stanza.getChild('status');
    if (status) { stanza.status = status.getText(); }
    
    var priority = stanza.getChild('priority');
    if (priority) {
      stanza.priority = parseInt(priority.getText());
    } else {
      stanza.priority = 0;
    }
    
    next();
  }
}

serviceDiscovery

lib/junction/middleware/serviceDiscovery.js

Module dependencies.

var util = require('util');
var StanzaError = require('../stanzaerror');

Handle requests for discovering information about an XMPP entity

This middleware handles IQ-get requests for information about the identity and capabilities of an XMPP entity. These requests are contained within the http://jabber.org/protocol/disco#info XML namespace.

This middleware is intentionally simple, providing only the most basic, yet widely used, features of the service discovery protocol. Specifically, it has no support for item discovery or for querying nodes. For a feature-complete implementation of service discovery, Junction/Disco should be used.

Examples

 connection.use(
   junction.serviceDiscovery([ { category: 'conference', type: 'text', name: 'Play-Specific Chatrooms' },
                               { category: 'directory', type: 'chatroom' } ],
                             [ 'http://jabber.org/protocol/disco#info', 
                               'http://jabber.org/protocol/muc' ])
 );

 connection.use(
   junction.serviceDiscovery( { category: 'client', type: 'pc' },
                              'http://jabber.org/protocol/disco#info' )
 );

References

  • param: Array | String identities

  • param: Array | String features

  • return: Function

  • api: public

module.exports = function serviceDiscovery(identities, features) {
  identities = identities || [];
  features = features || [];
  if (!Array.isArray(identities)) { identities = [ identities ]; }
  if (!Array.isArray(features)) { features = [ features ]; }
  
  return function serviceDiscovery(req, res, next) {
    if (!req.is('iq')) { return next(); }
    if (req.type == 'result' || req.type == 'error') { return next(); }
    var query = req.getChild('query', 'http://jabber.org/protocol/disco#info');
    if (!query) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Query must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    if (query.attrs.node) {
      return next(new StanzaError("", 'cancel', 'item-not-found'));
    }
    
    var info = res.c('query', { xmlns: 'http://jabber.org/protocol/disco#info' });
    identities.forEach(function(identity) {
      var ide = info.c('identity', { category: identity.category,
                                     type: identity.type });
      if (identity.name) { ide.attrs.name = identity.name };
    });
    features.forEach(function(feature) {
      info.c('feature', { var: feature });
    });
    res.send();
  }
}

serviceUnavailable

lib/junction/middleware/serviceUnavailable.js

Module dependencies.

var StanzaError = require('../stanzaerror');
require('../../node-xmpp/element_ext');

Respond with service-unavailable error to IQ request stanzas.

This middleware responds with a service-unavailable stanza error to any IQ-get or IQ-set stanza. This error is used to indicate that an entity does not provide the requested service.

This middleware should be use()-ed at the lowest priority level. This allows higher-priority middleware an opportunity to process the stanza. If all higher-priority middleware pass on that opportunity, the stanza will be handled by this middleware and a service-unavailable error will be sent to the requesting entity.

Examples

 connection.use(junction.ping());
 // TODO: Use other application-specific middleware here.
 connection.use(junction.serviceUnavailable());

References

  • return: Function

  • api: public

module.exports = function serviceUnavailable() {
  return function serviceUnavailable(req, res, next) {
    if (!req.isIQ()) { return next(); }
    res.attrs.type = 'error';
    res.c('error', { type: 'cancel' }).c('service-unavailable', { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' });
    res.send();
  }
}

softwareVersion

lib/junction/middleware/softwareVersion.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Handle requests for information about the software application associated with an XMPP entity.

This middleware handles IQ-get requests within the jabber:iq:version XML namespace. The middleware responds with the application name, version, and operating system.

Examples

 connection.use(junction.softwareVersion('IMster', '1.0', 'Linux'));

References

  • param: String name

  • param: String version

  • param: String os

  • return: Function

  • api: public

module.exports = function softwareVersion(name, version, os) {
  
  return function softwareVersion(req, res, next) {
    if (!req.is('iq')) { return next(); }
    if (req.type == 'result' || req.type == 'error') { return next(); }
    var query = req.getChild('query', 'jabber:iq:version');
    if (!query) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Query must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    
    var ver = res.c('query', { xmlns: 'jabber:iq:version' });
    if (name) { ver.c('name').t(name); }
    if (version) { ver.c('version').t(version); }
    if (os) { ver.c('os').t(os); }
    res.send();
  }
}

softwareVersionResultParser

lib/junction/middleware/softwareVersionResultParser.js

Parse information about the software application associated with an XMPP entity.

This middleware parses software application information contained within IQ-result stanzas. stanza.application indicates the name of the software application. stanza.version indicates the specific version of the application. stanza.os indicates the operating system on which the application is executing.

Examples

 connection.use(junction.softwareVersionResultParser());

References

  • return: Function

  • api: public

module.exports = function softwareVersionResultParser() {
  
  return function softwareVersionResultParser(stanza, next) {
    if (!stanza.is('iq')) { return next(); }
    if (stanza.type != 'result') { return next(); }
    var query = stanza.getChild('query', 'jabber:iq:version');
    if (!query) { return next(); }
    
    var nameEl = query.getChild('name');
    if (nameEl) {
      stanza.application = nameEl.getText();
    }
    var versionEl = query.getChild('version');
    if (versionEl) {
      stanza.version = versionEl.getText();
    }
    var osEl = query.getChild('os');
    if (osEl) {
      stanza.os = osEl.getText();
    }
    next();
  }
}

time

lib/junction/middleware/time.js

Module dependencies.

var StanzaError = require('../stanzaerror');

Handle requests for the local time of an XMPP entity.

This middleware handles IQ-get requests within the urn:xmpp:time XML namespace. The middleware responds with the UTC time of the entity, as well as the offset from UTC.

Examples

 connection.use(junction.time());

References

  • param: Object options

  • return: Function

  • api: public

module.exports = function time(options) {
  options = options || {};
  
  return function time(req, res, next) {
    if (!req.is('iq')) { return next(); }
    if (req.type == 'result' || req.type == 'error') { return next(); }
    var te = req.getChild('time', 'urn:xmpp:time');
    if (!te) { return next(); }
    
    if (req.type != 'get') {
      return next(new StanzaError("Time must be an IQ-get stanza.", 'modify', 'bad-request'));
    }
    
    var now = options.date || new Date();
    var tzo = (typeof options.timezoneOffset !== 'undefined') ? options.timezoneOffset : now.getTimezoneOffset();
    var t = res.c('time', { xmlns: 'urn:xmpp:time' });
    t.c('utc').t(XSDDateTimeString(now));
    t.c('tzo').t(XSDTimeZoneString(tzo));
    res.send();
  }
}


// CREDIT: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date
function XSDDateTimeString(d) {
  function pad(n) { return n < 10 ? '0' + n : n }
  return d.getUTCFullYear() + '-'
    + pad(d.getUTCMonth() + 1) + '-'
    + pad(d.getUTCDate()) + 'T'
    + pad(d.getUTCHours()) + ':'
    + pad(d.getUTCMinutes()) + ':'
    + pad(d.getUTCSeconds()) + 'Z'
}

function XSDTimeZoneString(tzo) {
  function sign(n) { return n > 0 ? '+' : '-' }
  function pad(n) { return n < 10 ? '0' + n : n.toString() }
  return sign(0 - tzo)
    + pad(Math.floor(Math.abs(tzo) / 60)) + ':'
    + pad(Math.abs(tzo) % 60);
}

timeResultParser

lib/junction/middleware/timeResultParser.js

Parse information about the local time of an XMPP entity.

This middleware parses local time information contained within IQ-result stanzas. stanza.utcDate indicates the UTC time according to the responding entity. stanza.timezoneOffset indicates the timezone offset from UTC, in minutes, for the responding entity.

The time-zone offset is the difference, in minutes, between UTC and local time. Note that this means that the offset is positive if the local timezone is behind UTC and negative if it is ahead. For example, if your time zone is UTC+10 (Australian Eastern Standard Time), -600 will be returned. This matches the semantics of getTimezoneOffset() provided by JavaScript's built-in Date class.

Examples

 connection.use(junction.timeResultParser());

References

  • return: Function

  • api: public

module.exports = function timeResultParser() {
  
  return function timeResultParser(stanza, next) {
    if (!stanza.is('iq')) { return next(); }
    if (stanza.type != 'result') { return next(); }
    var time = stanza.getChild('time', 'urn:xmpp:time');
    if (!time) { return next(); }
    
    var utcEl = time.getChild('utc');
    if (utcEl) {
      stanza.utcDate = new Date(utcEl.getText());
    }
    var tzoEl = time.getChild('tzo');
    if (tzoEl) {
      stanza.timezoneOffset = timezoneOffsetFromUTC(tzoEl.getText());
    }
    next();
  }
}


function timezoneOffsetFromUTC(tzo) {
  function parse(s) {
    var match = /([+\-])([0-9]{2}):(\d{2})/.exec(s);
    if (match) {
      var min = (parseInt(match[2], 10) * 60) + parseInt(match[3], 10);
      return match[1] == '+' ? min : 0 - min;
    }
    return 0;
  }
  return 0 - parse(tzo);
}

nullstream

lib/junction/nullstream.js

Module dependencies.

var util = require('util')
  , EventEmitter = require('events').EventEmitter;

Inherit from EventEmitter.

util.inherits(NullStream, EventEmitter);

Expose NullStream.

module.exports = NullStream;

stanzaerror

lib/junction/stanzaerror.js

Module dependencies.

var util = require('util');

Initialize a new StanzaError.

  • param: String message

  • param: String type

  • param: String condition

  • api: public

function StanzaError(message, type, condition) {
  Error.apply(this, arguments);
  Error.captureStackTrace(this, arguments.callee);
  this.name = 'StanzaError';
  this.message = message || null;
  this.type = type || 'wait';
  this.condition = condition || 'internal-server-error';
};

Inherit from Error.

util.inherits(StanzaError, Error);

Expose StanzaError.

exports = module.exports = StanzaError;