diff 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 diff
new file mode 100644
--- /dev/null
+++ b/script2.js
@@ -0,0 +1,354 @@
+'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);