view script2.js @ 5:03ef53b969bd

Add XMPP support.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 31 Jan 2012 01:09:41 +0100
parents
children 24aa8dccb170
line wrap: on
line source

'use strict';

/** Copyright (c) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
  * deal in the Software without restriction, including without limitation the
  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  * sell copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  * IN THE SOFTWARE.
  */

var conn = new Lightstring.Connection(SERVICE);
var roster = {};
var documents = {};

Lightstring.NS.sxe = 'urn:xmpp:sxe:0';
Lightstring.NS.jingle = {
  main: 'urn:xmpp:jingle:1',
  transports: {
    sxe: 'urn:xmpp:jingle:transports:sxe'
  },
  apps: {
    xhtml: 'urn:xmpp:jingle:apps:xhtml'
  }
};

conn.on('iq/' + Lightstring.NS['disco#info'] + ':query', function(stanza) {
  if (stanza.DOM.getAttributeNS(null, 'type') !== 'get')
    return;

  var query = stanza.DOM.firstChild;
  if (query.getAttributeNS(null, 'node')) {
    var response = "<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
                      " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
                      " type='error'/>"; //TODO: precise the error.
    conn.send(response);
    return;
  }

  var features = [Lightstring.NS.sxe, Lightstring.NS.jingle.transports.sxe]; //TODO: put that elsewhere.

  var response = "<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
                    " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
                    " type='result'>" +
                   "<query xmlns='" + Lightstring.NS['disco#info'] + "'>" +
                     "<identity category='client' type='browser'/>";
  features.forEach(function(f) {
    response += "<feature var='" + f + "'/>";
  });
  response += "</query>" +
            "</iq>";

  conn.send(response);
});

conn.on('presence', function(stanza) {
  var from = new Lightstring.JID(stanza.DOM.getAttributeNS(null, 'from'));
  if (!from.equals(conn.jid)) {
    var type = stanza.DOM.getAttributeNS(null, 'type');
    if (!type)
      Lightstring.discoInfo(conn, from, undefined, function(aData) {
        roster[from.full] = aData;
      });
    else if (type === 'unavailable')
      delete roster[from.full];
  }
});

var host = function(sid, name) {
  if (!sid)
    sid = String(Math.random());

  documents[sid] = new Document(conn.jid.full, name, conn.jid.full, 'document');

  for (var jid in roster) {
    var contact = roster[jid];
    if ((contact.features.indexOf(Lightstring.NS.sxe) !== -1) && (contact.features.indexOf(Lightstring.NS.jingle.transports.sxe) !== -1))
      initiate(jid, sid);
  }
}

var initiate = function(jid, sid) {
  var initiate = "<iq to='" + jid + "' type='set'>" +
                   "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
                          " action='session-initiate'" +
                          " initiator='" + documents[sid].initiator + "'" +
                          " sid='" + sid + "'>" +
                     "<content creator='initiator' name='" + documents[sid].name + "'>" +
                       "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
                       "<transport xmlns='" + Lightstring.NS.jingle.transports.sxe + "'>" +
                         "<host>" + documents[sid].host + "</host>" +
                       "</transport>" +
                     "</content>" +
                   "</jingle>" +
                 "</iq>";
  conn.send(initiate);
};

var accept = function(sid) {
  var accept = "<iq to='" + documents[sid].initiator + "' type='set'>" +
                 "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
                        " action='session-accept'" +
                        " initiator='" + documents[sid].initiator + "'" +
                        " sid='" + sid + "'>" +
                   "<content creator='initiator' name='" + documents[sid].name + "'>" +
                     "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
                     "<transport xmlns='" + Lightstring.NS.jingle.transports.sxe + "'>" +
                       "<host>" + documents[sid].host + "</host>" +
                     "</transport>" +
                   "</content>" +
                 "</jingle>" +
               "</iq>";
  conn.send(accept, function(stanza) {
    if (stanza.DOM.getAttributeNS(null, 'type') === 'result')
      connect(sid);
    else
      terminate('TODO'); //XXX
  });
};

var terminate = function(sid, reason) {
  if (!(sid in documents))
    return console.log('BIG WARNING!!!');

  var terminate = "<iq to='" + documents[sid].initiator + "' type='set'>" +
                    "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
                           " action='session-terminate'" +
                           " host='" + documents[sid].host + "'" +
                           " initiator='" + documents[sid].initiator + "'" +
                           " sid='" + sid + "'>" +
                      "<reason>" +
                        "<" + reason + "/>" +
                      "</reason>" +
                    "</jingle>" +
                  "</iq>";
  delete documents[sid];
  conn.send(terminate);
};

conn.on('iq/' + Lightstring.NS.jingle.main + ':jingle', function(stanza) {
  conn.send("<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
               " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
               " type='result'/>");

  var jingle = stanza.DOM.firstChild;
  var action = jingle.getAttributeNS(null, 'action');
  var initiator = jingle.getAttributeNS(null, 'initiator');
  var sid = jingle.getAttributeNS(null, 'sid');

  var payload = jingle.firstChild; //TODO: what if they are multiple?

  //TODO: verify that the payload is really what we want.
  //TODO: use the reason even with content as payload.
  if (action === 'session-initiate') {
    var content = payload;
    var creator = content.getAttributeNS(null, 'creator');
    var name = content.getAttributeNS(null, 'name');

    if (documents[sid])
      return terminate(sid, 'alternative-session'); //TODO: The XEP: “and wishes to use that [previous] session instead”

    var description = content.getElementsByTagName('description')[0]; //TODO: supporte multiple applications.
    if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
      return terminate(sid, 'unsupported-applications');

    var transport = content.getElementsByTagName('transport')[0]; //TODO: supporte multiple transports.
    if (transport.namespaceURI !== Lightstring.NS.jingle.transports.sxe)
      return terminate(sid, 'unsupported-transports');

    var host = transport.textContent; //TODO: verify the presence of the host element.

    documents[sid] = new Document(initiator, name, host, 'document');

    if (confirm('Do you accept?'))
      accept(sid);
    else
      terminate(sid, 'decline');

  } else if (action === 'session-accept') {
    var content = payload;
    var creator = content.getAttributeNS(null, 'creator');
    var name = content.getAttributeNS(null, 'name');

    alert('Accepted! \\o/');

    var description = content.getElementsByTagName('description')[0]; //TODO: supporte multiple applications.
    if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
      return terminate(sid, 'unsupported-applications');

    var transport = content.getElementsByTagName('transport')[0]; //TODO: supporte multiple transports.
    if (transport.namespaceURI !== Lightstring.NS.jingle.transports.sxe)
      return terminate(sid, 'unsupported-transports');

    var host = transport.textContent; //TODO: verify the presence of the host element.

    var doc = documents[sid];
    if (doc.initiator !== initiator ||
        doc.name !== name ||
        doc.host !== host)
      return terminate(sid, 'unknown-error'); //XXX

  } else if (action === 'session-terminate') {
    var reason = payload;

    //TODO: verify they are what the should be.
    var element = reason.firstChild;
    var text = element.nextSibling;

    alert('Terminated, reason: ' + element.localName + (text? ' (' + text.textContent + ')': ''));

    delete documents[sid];
  }
});

var connect = function(sid) {
  var host = documents[sid].host;
  var identities = roster[host].identities;
  var type = 'chat';
  identities.forEach(function(identity) {
    if (identity.category === 'conference')
      type = 'groupchat';
  });
  var message = "<message to='" + host + "'" +
                        " type='" + type + "'>" +
                  "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
                      " id='" + Lightstring.newId('sxe') + "'" +
                      " session='" + sid + "'>" +
                    "<connect/>" +
                  "</sxe>" +
                "</message>";
  conn.send(message);
};

conn.on('message/' + Lightstring.NS.sxe + ':sxe', function(stanza) {
  var from = stanza.DOM.getAttributeNS(null, 'from');
  var type = 'chat'; //TODO: always?
  var sxe = stanza.DOM.firstChild; //TODO: there can be multiple payloads.
  var id = sxe.getAttributeNS(null, 'id');
  var sid = sxe.getAttributeNS(null, 'session');

  var doc = documents[sid];
  if (!doc) {
    conn.send("<message to='" + from + "' type='error'/>"); //XXX
    return;
  }

  var payload = sxe.firstChild; //TODO: really?
  switch (payload.localName) {
    case 'connect':
      var message = "<message to='" + from + "'" +
                            " type='" + type + "'>" +
                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
                          " id='" + Lightstring.newId('sxe') + "'" +
                          " session='" + sid + "'>" +
                        "<state-offer>" +
                          "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
                        "</state-offer>" +
                      "</sxe>" +
                    "</message>";
      conn.send(message);
      break;
    case 'state-offer':
      var description = payload.firstChild;
      if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
        return terminate(sid, 'unsupported-applications');

      var accept = false;
      if (from === doc.host || from === doc.initiator)
        accept = true;

      //TODO: refuse if proposed multiple times.

      var message = "<message to='" + from + "'" +
                            " type='" + type + "'>" +
                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
                          " id='" + Lightstring.newId('sxe') + "'" +
                          " session='" + sid + "'>" +
                        "<" + (accept? 'accept': 'refuse') + "-state/>" +
                      "</sxe>" +
                    "</message>";
      conn.send(message);
      break;
    case 'accept-state':
      var message = "<message to='" + from + "'" +
                            " type='" + type + "'>" +
                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
                          " id='" + Lightstring.newId('sxe') + "'" +
                          " session='" + sid + "'>" +
                        "<state>" +
                          "<document-begin prolog='" + doc.prolog + "'/>" +
                          //TODO: support non-empty documents.
                          "<document-end last-sender='' last-id=''/>" +
                        "</state>" +
                      "</sxe>" +
                    "</message>";
      conn.send(message);
      break;
    case 'refuse-state':
      terminate(sid, 'decline');
      break;
    case 'state':
      var elements = payload.children;
      doc.processState(from, elements);
      break;
    default:
      var elements = sxe.children;
      doc.processState(from, elements);
  }
});

(function() {
  var events = ['connected', 'conn-error', 'connecting', 'disconnected', 'disconnecting'];
  var state = document.getElementById('state');
  events.forEach(function(e) {
    conn.on(e, function() {
      state.innerHTML = e;
    });
  });
})();

conn.on('error', function(a) {
  alert(a);
});

document.getElementById('host').addEventListener('click', function() {
  host(Lightstring.newId('sess'), 'My First XHTML Document');
}, false);

conn.on('connected', function() {
  conn.on('output', function(stanza) {
    console.log('out:', stanza.DOM);
  });
  conn.on('input', function(stanza) {
    console.log('in:', stanza.DOM);
  });

  Lightstring.presence(conn);
});

conn.connect(JID + '/' + prompt('Resource?'), PASSWORD);

register_feature_not_implemented.call(conn);