Mercurial > eldonilo > lightstring
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 }; |