comparison lightstring.js @ 108:5cb4733c5189

many api changes
author Sonny Piers <sonny@fastmail.net>
date Fri, 13 Jul 2012 15:26:18 +0200
parents c06ec02217ee
children
comparison
equal deleted inserted replaced
107:704ce44c1a22 108:5cb4733c5189
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */ 17 */
18 18
19 19 (function() {
20 var Lightstring = { 20 define(['./jid', './stanza', './transports/websocket.js', './transports/bosh.js'], function(JID, Stanza, WebSocketTransport, BOSHTransport) {
21 /** 21 Lightstring.JID = JID;
22 * @namespace Holds XMPP namespaces. 22 Lightstring.Stanza = Stanza.stanza;
23 * @description http://xmpp.org/xmpp-protocols/protocol-namespaces 23 Lightstring.IQ = Stanza.iq;
24 */ 24 Lightstring.doc = Stanza.doc;
25 ns: { 25 Lightstring.Presence = Stanza.presence;
26 streams: 'http://etherx.jabber.org/streams', 26 Lightstring.Message = Stanza.message;
27 jabber_client: 'jabber:client', 27 Lightstring.BOSHTransport = BOSHTransport;
28 xmpp_stanzas: 'urn:ietf:params:xml:ns:xmpp-stanzas' 28 Lightstring.WebSocketTransport = WebSocketTransport;
29 }, 29 return Lightstring;
30 /** 30 });
31 * @namespace Holds XMPP stanza builders. 31
32 */ 32 var Lightstring = {
33 stanzas: { 33 /*
34 stream: { 34 * @namespace Holds XMPP namespaces.
35 open: function(aService) { 35 * @description http://xmpp.org/xmpp-protocols/protocol-namespaces
36 return "<stream:stream to='" + aService + "'" + 36 */
37 " xmlns='" + Lightstring.ns['jabber_client'] + "'" + 37 ns: {
38 " xmlns:stream='" + Lightstring.ns['streams'] + "'" + 38 streams: 'http://etherx.jabber.org/streams',
39 " version='1.0'>"; 39 jabber_client: 'jabber:client',
40 xmpp_stanzas: 'urn:ietf:params:xml:ns:xmpp-stanzas'
41 },
42 /**
43 * @namespace Holds XMPP stanza builders.
44 */
45 stanzas: {
46 stream: {
47 open: function(aService) {
48 return "<stream:stream to='" + aService + "'" +
49 " xmlns='" + Lightstring.ns['jabber_client'] + "'" +
50 " xmlns:stream='" + Lightstring.ns['streams'] + "'" +
51 " version='1.0'>";
52 },
53 close: function() {
54 return "</stream:stream>";
55 }
40 }, 56 },
41 close: function() { 57 errors: {
42 return "</stream:stream>"; 58 iq: function(from, id, type, error) {
59 return "<iq to='" + from + "'" +
60 " id='" + id + "'" +
61 " type='error'>" +
62 "<error type='" + type + "'>" +
63 "<" + error + " xmlns='" + Lightstring.ns['xmpp_stanzas'] + "'/>" + //TODO: allow text content.
64 //TODO: allow text and payload.
65 "</error>" +
66 "</iq>";
67 }
43 } 68 }
44 }, 69 },
45 errors: { 70 /**
46 iq: function(from, id, type, error) { 71 * @namespace Holds Lightstring plugins
47 return "<iq to='" + from + "'" + 72 */
48 " id='" + id + "'" + 73 plugins: {},
49 " type='error'>" + 74 /**
50 "<error type='" + type + "'>" + 75 * @private Holds the connections
51 "<" + error + " xmlns='" + Lightstring.ns['xmpp_stanzas'] + "'/>" + //TODO: allow text content. 76 */
52 //TODO: allow text and payload. 77 connections: [],
53 "</error>" + 78 /**
54 "</iq>"; 79 * @function Returns a new unique identifier.
55 } 80 * @param {String} [aPrefix] Prefix to put before the identifier.
56 } 81 * @return {String} Identifier.
57 }, 82 */
58 /** 83 id: function(aPrefix) {
59 * @namespace Holds Lightstring plugins 84 return (aPrefix || '') + Date.now();
60 */ 85 }
61 plugins: {}, 86 };
62 /** 87
63 * @private 88 /**
64 */ 89 * @constructor Creates a new Lightstring connection
65 parser: new DOMParser(), 90 * @param {String} [aService] The connection manager URL.
66 /** 91 * @memberOf Lightstring
67 * @private 92 */
68 */ 93 Lightstring.Connection = function(aService) {
69 serializer: new XMLSerializer(), 94 if (aService)
70 /** 95 this.service = aService;
71 * @function Transforms a XML string to a DOM object. 96 /**
72 * @param {String} aString XML string. 97 * @namespace Holds connection events handlers
73 * @return {Object} Domified XML. 98 */
74 */ 99 this.handlers = {};
75 parse: function(aString) { 100 /**
76 var el = null; 101 * @namespace Holds connection iq callbacks
77 //FIXME webkit doesn't throw an error when the parsing fails 102 */
78 try { 103 this.callbacks = {};
79 el = this.parser.parseFromString(aString, 'text/xml').documentElement; 104
80 } 105 Lightstring.connections.push(this);
81 catch (e) { 106 };
82 //TODO: error 107 Lightstring.Connection.prototype = new EventEmitter();
83 } 108 Lightstring.Connection.prototype.onTransportLoaded = function() {
84 finally { 109 this.transport.open();
85 return el; 110
86 }; 111 var that = this;
87 }, 112
88 /** 113 this.transport.once('open', function() {
89 * @function Transforms a DOM object to a XML string. 114 that.emit('open');
90 * @param {Object} aString DOM object. 115 });
91 * @return {String} Stringified DOM. 116 this.transport.on('out', function(stanza) {
92 */ 117 setTimeout(function() {
93 serialize: function(aElement) { 118 that.emit('out', stanza);
94 var string = null; 119 }, 0);
95 try { 120 });
96 string = this.serializer.serializeToString(aElement); 121 this.transport.on('in', function(stanza) {
97 } 122 //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong!
98 catch (e) { 123 that.emit('stanza', stanza);
99 //TODO: error 124
100 } 125 if (!stanza.el)
101 finally { 126 return;
102 return string; 127
103 }; 128 var el = stanza.el;
104 }, 129
105 /** 130 //Authentication
106 * @function Get an unique identifier. 131 //FIXME SASL mechanisms and XMPP features can be both in a stream:features
107 * @param {String} [aString] Prefix to put before the identifier. 132 if (el.localName === 'features') {
108 * @return {String} Identifier. 133 var children = el.childNodes;
109 */ 134 for (var i = 0, length = children.length; i < length; i++) {
110 newId: (function() { 135 //SASL mechanisms
111 var id = 1024; 136 if(children[i].localName === 'mechanisms') {
112 return function(prefix) { 137 stanza.mechanisms = [];
113 if (typeof prefix === 'string') 138 var nodes = el.getElementsByTagName('mechanism');
114 return prefix + id++; 139 for (var i = 0; i < nodes.length; i++)
115 return '' + id++; 140 stanza.mechanisms.push(nodes[i].textContent);
116 }; 141 that.emit('mechanisms', stanza);
117 })() 142 return;
118 }; 143 }
119 144 }
120 /** 145 //XMPP features
121 * @constructor Creates a new Lightstring connection 146 // else {
122 * @param {String} [aService] The connection manager URL. 147 //TODO: stanza.features
123 * @memberOf Lightstring 148 that.emit('features', stanza);
124 */ 149 // }
125 Lightstring.Connection = function(aService) { 150 }
126 var that = this; 151 else if (el.localName === 'challenge') {
127 window.addEventListener('message', function(e) { 152 that.emit('challenge', stanza);
128 that.send(e.data.send) 153 }
129 }); 154 else if (el.localName === 'failure') {
130 if (aService) 155 that.emit('failure', stanza);
131 this.service = aService; 156 }
132 /** 157 else if (el.localName === 'success') {
133 * @namespace Holds connection events handlers 158 that.emit('success', stanza);
134 */ 159 }
135 this.handlers = {}; 160
136 /** 161 //Iq callbacks
137 * @namespace Holds connection iq callbacks 162 else if (el.localName === 'iq') {
138 */ 163 var payload = el.firstChild;
139 this.callbacks = {}; 164 if (payload)
140 }; 165 that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza);
141 Lightstring.Connection.prototype = new EventEmitter(); 166
142 /** 167 var id = el.getAttribute('id');
143 * @function Create and open a websocket then go though the XMPP authentification process. 168 if (!(id && id in that.callbacks))
144 * @param {String} [aJid] The JID (Jabber id) to use. 169 return;
145 * @param {String} [aPassword] The associated password. 170
146 */ 171 var type = el.getAttribute('type');
147 Lightstring.Connection.prototype.connect = function(aJid, aPassword) { 172 if (type !== 'result' && type !== 'error')
148 this.emit('connecting'); 173 return; //TODO: warning
149 this.jid = new Lightstring.JID(aJid); 174
150 if (aPassword) 175 var callback = that.callbacks[id];
151 this.password = aPassword; 176 if (type === 'result' && callback.success)
152 177 callback.success.call(that, stanza);
153 if (!this.jid.bare) 178 else if (type === 'error' && callback.error)
154 return; //TODO: error 179 callback.error.call(that, stanza);
155 if (!this.service) 180
156 return; //TODO: error 181 delete that.callbacks[id];
157 182 }
158 function getProtocol(aURL) { 183
159 var a = document.createElement('a'); 184 else if (el.localName === 'presence' || el.localName === 'message') {
160 a.href = aURL; 185 that.emit(name, stanza);
161 return a.protocol.replace(':', ''); 186 }
162 } 187 });
163 var protocol = getProtocol(this.service); 188 };
164 189 /**
165 if (protocol.match('http')) 190 * @function Create and open a websocket then go though the XMPP authentification process.
166 this.connection = new Lightstring.BOSHConnection(this.service, this.jid); 191 * @param {String} [aJid] The JID (Jabber id) to use.
167 else if (protocol.match('ws')) 192 * @param {String} [aPassword] The associated password.
168 this.connection = new Lightstring.WebSocketConnection(this.service, this.jid); 193 */
169 194 Lightstring.Connection.prototype.connect = function(aJid, aPassword) {
170 this.connection.open(); 195 this.emit('connecting');
171 196 this.jid = new Lightstring.JID(aJid);
172 var that = this; 197 if (aPassword)
173 198 this.password = aPassword;
174 this.connection.once('open', function() { 199
175 that.emit('open'); 200 if (!this.jid.bare)
176 }); 201 return; //TODO: error
177 this.connection.on('out', function(stanza) { 202 if (!this.service)
178 that.emit('out', stanza); 203 return; //TODO: error
179 }); 204
180 this.connection.on('in', function(stanza) { 205 function getProtocol(aURL) {
181 //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong! 206 var a = document.createElement('a');
182 that.emit('stanza', stanza); 207 a.href = aURL;
183 208 return a.protocol.replace(':', '');
184 if (!stanza.el) 209 }
210 var protocol = getProtocol(this.service);
211
212 if (protocol.match('http'))
213 this.transport = new Lightstring.BOSHTransport(this.service, this.jid);
214 else if (protocol.match('ws'))
215 this.transport = new Lightstring.WebSocketTransport(this.service, this.jid);
216
217 this.onTransportLoaded();
218 };
219 /**
220 * @function Send a message.
221 * @param {String|Object} aStanza The message to send.
222 * @param {Function} [aCallback] Executed on answer. (stanza must be iq)
223 */
224 Lightstring.Connection.prototype.send = function(aStanza, aOnSuccess, aOnError) {
225 if (!(aStanza instanceof Lightstring.Stanza))
226 var stanza = new Lightstring.Stanza(aStanza);
227 else
228 var stanza = aStanza;
229
230 if (!stanza)
185 return; 231 return;
186 232
187 var el = stanza.el; 233 if (stanza.name === 'iq') {
188 234 var type = stanza.type;
189 //Authentication 235 if (type !== 'get' || type !== 'set')
190 //FIXME SASL mechanisms and XMPP features can be both in a stream:features 236 ; //TODO: error
191 if (el.localName === 'features') { 237
192 var children = el.childNodes; 238 var callback = {success: aOnSuccess, error: aOnError};
193 for (var i = 0, length = children.length; i < length; i++) { 239
194 //SASL mechanisms 240 var id = stanza.id;
195 if(children[i].localName === 'mechanisms') { 241 if (!id)
196 stanza.mechanisms = []; 242 stanza.id = Lightstring.id();
197 var nodes = el.getElementsByTagName('mechanism'); 243
198 for (var i = 0; i < nodes.length; i++) 244 this.callbacks[stanza.id] = callback;
199 stanza.mechanisms.push(nodes[i].textContent); 245 }
200 that.emit('mechanisms', stanza); 246 else if (aOnSuccess || aOnError)
201 return; 247 ; //TODO: warning (no callback without iq)
202 } 248
203 } 249 this.transport.send(stanza.toString());
204 //XMPP features 250 };
205 // else { 251 /**
206 //TODO: stanza.features 252 * @function Closes the XMPP stream and the socket.
207 that.emit('features', stanza); 253 */
208 // } 254 Lightstring.Connection.prototype.disconnect = function() {
209 } 255 this.emit('disconnecting');
210 else if (el.localName === 'challenge') { 256 var stream = Lightstring.stanzas.stream.close();
211 that.emit('challenge', stanza); 257 this.transport.send(stream);
212 } 258 this.emit('out', stream);
213 else if (el.localName === 'failure') { 259 this.transport.close();
214 that.emit('failure', stanza); 260 };
215 } 261 Lightstring.Connection.prototype.load = function() {
216 else if (el.localName === 'success') { 262 for (var i = 0; i < arguments.length; i++) {
217 that.emit('success', stanza); 263 var name = arguments[i];
218 } 264 if (!(name in Lightstring.plugins))
219 265 continue; //TODO: error
220 //Iq callbacks 266
221 else if (el.localName === 'iq') { 267 var plugin = Lightstring.plugins[name];
222 var payload = el.firstChild; 268
223 if (payload) 269 //Namespaces
224 that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza); 270 for (var ns in plugin.namespaces)
225 271 Lightstring.ns[ns] = plugin.namespaces[ns];
226 var id = el.getAttribute('id'); 272
227 if (!(id && id in that.callbacks)) 273 //Stanzas
228 return; 274 Lightstring.stanzas[name] = {};
229 275 for (var stanza in plugin.stanzas)
230 var type = el.getAttribute('type'); 276 Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza];
231 if (type !== 'result' && type !== 'error') 277
232 return; //TODO: warning 278 //Handlers
233 279 for (var handler in plugin.handlers)
234 var callback = that.callbacks[id]; 280 this.on(handler, plugin.handlers[handler]);
235 if (type === 'result' && callback.success) 281
236 callback.success.call(that, stanza); 282 //Methods
237 else if (type === 'error' && callback.error) 283 this[name] = {};
238 callback.error.call(that, stanza); 284 for (var method in plugin.methods)
239 285 this[name][method] = plugin.methods[method].bind(this);
240 delete that.callbacks[id]; 286
241 } 287 if (plugin.init)
242 288 plugin.init.apply(this);
243 else if (el.localName === 'presence' || el.localName === 'message') { 289 }
244 that.emit(name, stanza); 290 };
245 } 291 })();
246 });
247 };
248 /**
249 * @function Send a message.
250 * @param {String|Object} aStanza The message to send.
251 * @param {Function} [aCallback] Executed on answer. (stanza must be iq)
252 */
253 Lightstring.Connection.prototype.send = function(aStanza, aSuccess, aError) {
254 if (!(aStanza instanceof Lightstring.Stanza))
255 var stanza = new Lightstring.Stanza(aStanza);
256 else
257 var stanza = aStanza;
258
259 if (!stanza)
260 return;
261
262 if (stanza.el.tagName === 'iq') {
263 var type = stanza.el.getAttribute('type');
264 if (type !== 'get' || type !== 'set')
265 ; //TODO: error
266
267 var callback = {success: aSuccess, error: aError};
268
269 var id = stanza.el.getAttribute('id');
270 if (!id) {
271 var id = Lightstring.newId('sendiq:');
272 stanza.el.setAttribute('id', id);
273 }
274
275 this.callbacks[id] = callback;
276
277 }
278 else if (aSuccess || aError)
279 ; //TODO: warning (no callback without iq)
280
281 this.connection.send(stanza.toString());
282 };
283 /**
284 * @function Closes the XMPP stream and the socket.
285 */
286 Lightstring.Connection.prototype.disconnect = function() {
287 this.emit('disconnecting');
288 var stream = Lightstring.stanzas.stream.close();
289 this.socket.send(stream);
290 this.emit('out', stream);
291 this.socket.close();
292 };
293 Lightstring.Connection.prototype.load = function() {
294 for (var i = 0; i < arguments.length; i++) {
295 var name = arguments[i];
296 if (!(name in Lightstring.plugins))
297 continue; //TODO: error
298
299 var plugin = Lightstring.plugins[name];
300
301 //Namespaces
302 for (var ns in plugin.namespaces)
303 Lightstring.ns[ns] = plugin.namespaces[ns];
304
305 //Stanzas
306 Lightstring.stanzas[name] = {};
307 for (var stanza in plugin.stanzas)
308 Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza];
309
310 //Handlers
311 for (var handler in plugin.handlers)
312 this.on(handler, plugin.handlers[handler]);
313
314 //Methods
315 this[name] = {};
316 for (var method in plugin.methods)
317 this[name][method] = plugin.methods[method].bind(this);
318
319 if (plugin.init)
320 plugin.init.apply(this);
321 }
322 };