Mercurial > eldonilo > barbecue
changeset 5:03ef53b969bd
Add XMPP support.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Tue, 31 Jan 2012 01:09:41 +0100 |
parents | 95c6fa5f4715 |
children | 24aa8dccb170 |
files | config.js index.xhtml record.js script2.js sxe-document.js |
diffstat | 5 files changed, 620 insertions(+), 2 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/config.js @@ -0,0 +1,5 @@ +'use strict'; + +const SERVICE = 'ws://plugsbee.com:5280/'; +const JID = 'test@linkmauve.fr'; +const PASSWORD = 'test';
--- a/index.xhtml +++ b/index.xhtml @@ -1,10 +1,20 @@ <!DOCTYPE html> -<html xmlns="http://www.w3.org/1999/xhtml"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <meta charset="UTF-8" /> <title> barbecue </title> <link type="text/css" href="style.css" rel="stylesheet" /> <script type="text/javascript" src="script.js"></script> + <script type="text/javascript" src="lightstring/md5.js"></script> + <script type="text/javascript" src="lightstring/lightstring.js"></script> + <script type="text/javascript" src="lightstring/stanza.js"></script> + <script type="text/javascript" src="lightstring/jid.js"></script> + <script type="text/javascript" src="lightstring/plugins/presence.js"></script> + <script type="text/javascript" src="lightstring/plugins/disco.js"></script> + <script type="text/javascript" src="lightstring/plugins/feature-not-implemented.js"></script> + <script type="text/javascript" src="record.js"></script> + <script type="text/javascript" src="sxe-document.js"></script> + <script type="text/javascript" src="config.js"></script> </head> <body> <form id="toolbar"> @@ -92,7 +102,7 @@ <ol><li> and now a list </li><li> with only </li><li> three items </li></ol> </section> - <footer contenteditable="true"> + <footer> <p> This HTML content editor is not intended to compete with <a href="http://ckeditor.com/">CKEditor</a> or <a @@ -112,5 +122,6 @@ <a href="http://www.browserscope.org/richtext2/test">Browserscope.org</a>. </p> </footer> + <script type="text/javascript" src="script2.js"></script> </body> </html>
new file mode 100644 --- /dev/null +++ b/record.js @@ -0,0 +1,91 @@ +'use strict'; + +var Record = function(jid, child) { + this.creator = jid; + this.creationDate = new Date; //FIXME: non-standard? + this.update(jid, child); +} + +Record.prototype = { + update: function(jid, child) { + this.rid = child.getAttributeNS(null, 'rid') || this.rid; + this.type = child.getAttributeNS(null, 'type') || this.type; + this.version = child.getAttributeNS(null, 'version') || this.version || 0; + this.parent = child.getAttributeNS(null, 'parent') || this.parent; + this.primaryWeight = child.getAttributeNS(null, 'primary-weight') || this.primaryWeight || 0; + + this.lastModifiedDate = new Date; //FIXME: non-standard? + this.lastModifiedBy = jid; // only in 10.5.1 + + if (this.type === 'element' || this.type === 'attr') { + this.ns = child.getAttributeNS(null, 'ns') || this.ns; + this.name = child.getAttributeNS(null, 'name') || this.name; + } + + if (this.type === 'text' || this.type === 'attr' || this.type === 'comment') + this.chdata = child.getAttributeNS(null, 'chdata') || this.chdata; + + if (this.type === 'processinginstruction') { + this.pitarget = child.getAttributeNS(null, 'pitarget') || this.pitarget; + this.pidata = child.getAttributeNS(null, 'pidata') || this.pidata; + } + }, + toDOM: function(records, dom) { + var element; + if (this.parent) + dom = records[this.parent].dom; + + switch (this.type) { + case 'processinginstruction': + element = document.createProcessingInstruction(this.pitarget, this.pidata); + break; + case 'element': + if (this.ns) + element = document.createElementNS(this.ns, this.name); + else + element = document.createElement(this.name); + break; + case 'attr': + if (this.ns === 'xml') //FIXME: it’s ugly. + this.dom = document.createAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:' + this.name); + else if (this.ns) + this.dom = document.createAttributeNS(this.ns, this.name); + else + this.dom = document.createAttribute(this.name); + this.dom.value = this.chdata; + dom.setAttributeNodeNS(this.dom); + return; + case 'text': + element = document.createTextNode(this.chdata); + break; + case 'comment': + element = document.createComment(this.chdata); + break; + } + + dom.appendChild(element); + this.dom = element; + }, + remove: function(records) { + if (this.parent) + var parentNode = records[this.parent].dom; + + switch (this.type) { + case 'processinginstruction': + //TODO + break; + case 'element': + if (this.dom.children.length) + return; //TODO: use a better error reporting system. + parentNode.removeChild(this.dom); + break; + case 'attr': + parentNode.removeAttributeNS(this.ns, this.name); + break; + case 'text': + case 'comment': + parentNode.removeChild(this.node); + break; + } + } +};
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);
new file mode 100644 --- /dev/null +++ b/sxe-document.js @@ -0,0 +1,157 @@ +'use strict'; + +var Document = function(initiator, name, host, domId, prolog) { + this.initiator = initiator; + this.name = name; + this.host = host; + + this.prolog = prolog || 'data:application/xhtml+xml,%3C%3Fxml%20version%3D%271.0%27%3F%3E%0A%3C%21DOCTYPE%20html%3E%0A'; + this.state = 'not-started'; + this.records = {}; + this.dom = document.getElementById(domId); +}; + +Document.prototype = { + add: function(jid, child) { + var record = new Record(jid, child); + + if (record.rid in this.records) + console.log('duplicate new'); + + this.records[record.rid] = record; + record.toDOM(this.records, this.dom); + }, + update: function(jid, child) { + var target = child.getAttributeNS(null, 'target'); + + if (!(target in this.records)) + return; // Ignore it. + + var record = this.records[target]; + console.log(record, child); + child.setAttributeNS(null, 'rid', target); + + var version = +child.getAttributeNS(null, 'version'); + if (this.state === 'getting-state') + record.version = version; + else + record.version++; + + if (record.version === version) { + var type = child.getAttributeNS(null, 'type'); + if (type === 'text' || type === 'attr' || type === 'comment') { + var chdata = child.getAttributeNS(null, 'chdata'); + var replacefrom = +child.getAttributeNS(null, 'replacefrom'); + var replacen = +child.getAttributeNS(null, 'replacen'); + if (chdata && replacefrom && replacen) { + var string = record.chdata.substr(0, replacefrom); + string += chdata; + string += record.chdata.substr(replacefrom + replacen); + child.removeAttributeNS(null, 'replacefrom'); + child.removeAttributeNS(null, 'replacen'); + child.setAttributeNS(null, 'chdata', string); + } + } + record.update(jid, child); + record.toDOM(this.records, this.dom); + } else + ; // Not sure I understand correctly. + }, + remove: function(jid, child) { + var rid = child.getAttributeNS(null, 'target'); + this.records[rid].remove(this.records); + delete this.records[rid]; + }, + processState: function(jid, elements) { + var i = 0; + var first = elements[0]; + if (first.localName === 'document-begin') { + if (this.state !== 'not-started') + return; //TODO: the session has already started. + i = 1; + this.prolog = first.getAttributeNS(null, 'prolog'); + this.state = 'getting-session'; + } + + //TODO: if the session isn’t started, should ignore changes? + + for (; i < elements.length; i++) { + var child = elements[i]; + var change = child.localName; + + switch (change) { + case 'new': + doc.add(jid, child); + break; + case 'set': + doc.update(jid, child); + break; + case 'remove': + doc.remove(jid, child); + break; + case 'document-end': + this.state = 'started'; + break; + } + } + }, + createState: function(root, parent, state) { + var children = root.childNodes; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var element = document.createElementNS(Lightstring.NS.sxe, 'new'); + var rid = Lightstring.newId('GUID'); + element.setAttributeNS(null, 'rid', rid); + + if (parent) + element.setAttributeNS(null, 'parent', parent); + + switch (child.nodeType) { + case 1: + element.setAttributeNS(null, 'type', 'element'); + element.setAttributeNS(null, 'ns', child.namespaceURI); + element.setAttributeNS(null, 'name', child.localName); + state.push(element); + + //TODO: move that elsewhere, or make it prettier. + var convertAttr = function(attr) { + var element = document.createElementNS(Lightstring.NS.sxe, 'new'); + var arid = Lightstring.newId('GUID'); + element.setAttributeNS(null, 'rid', arid); + element.setAttributeNS(null, 'parent', rid); + if (attr.namespaceURI) + element.setAttributeNS(null, 'ns', attr.namespaceURI); + element.setAttributeNS(null, 'name', attr.localName); + element.setAttributeNS(null, 'chdata', attr.textContent); + state.push(element); + }; + + for (var j = 0; j < child.attributes.length; j++) + convertAttr(child.attributes[j]); + + state = this.createState(child, rid, state); + break; + + case 3: + element.setAttributeNS(null, 'type', 'text'); + element.setAttributeNS(null, 'chdata', child.textContent); + state.push(element); + break; + + case 7: + element.setAttributeNS(null, 'type', 'processinginstruction'); + element.setAttributeNS(null, 'pitarget', child.target); + element.setAttributeNS(null, 'pidata', child.data); + state.push(element); + break; + + case 8: + element.setAttributeNS(null, 'type', 'comment'); + element.setAttributeNS(null, 'chdata', child.textContent); + state.push(element); + break; + } + } + return state; + } +};