├── .gitignore ├── .idea ├── libraries │ └── Dart_SDK.xml ├── modules.xml └── workspace.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── app │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── flutter │ │ └── plugins │ │ └── GeneratedPluginRegistrant.java └── local.properties ├── lib ├── src │ ├── bosh.dart │ ├── core.dart │ ├── enums.dart │ ├── md5.dart │ ├── plugins │ │ ├── administration.dart │ │ ├── bookmark.dart │ │ ├── caps.dart │ │ ├── chat-notifications.dart │ │ ├── disco.dart │ │ ├── last-activity.dart │ │ ├── muc.dart │ │ ├── pep.dart │ │ ├── plugins.dart │ │ ├── privacy.dart │ │ ├── private-storage.dart │ │ ├── pubsub.dart │ │ ├── register.dart │ │ ├── roster.dart │ │ └── vcard-temp.dart │ ├── sessionstorage.dart │ ├── sha1.dart │ ├── utils.dart │ └── websocket.dart └── strophe.dart ├── pubspec.lock ├── pubspec.yaml ├── strophe.iml └── test └── strophe_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | ios/.generated/ 9 | ios/Flutter/Generated.xcconfig 10 | ios/Runner/GeneratedPluginRegistrant.* 11 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - TODO: Add release date. 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Strophe-dart is a pure Dart library for speaking XMPP via BOSH (XEP 124 and XEP 206) and WebSockets (RFC 7395). 2 | 3 | Its primary purpose is to enable mobile-based(flutter),desktop and web-based, real-time XMPP applications. 4 | 5 | The book Professional XMPP Programming with JavaScript and jQuery covers Strophe.Js in detail in the context of web applications. 6 | 7 | Caveats 8 | 9 | Bosh and all authenticate mechanisms don't work for the moment . 10 | 11 | License 12 | It is licensed under the MIT license, except for the files sha1.js, base64.js and md5.js, which are licensed as public domain and BSD (see these files for details). 13 | 14 | Author & History 15 | Strophe.js was originally created by Jack Moffitt. It was originally developed for Chesspark, an online chess community based on XMPP technology. It has been cared for and improved over the years and is currently maintained by many people in the community. -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import io.flutter.plugin.common.PluginRegistry; 4 | 5 | /** 6 | * Generated file. Do not edit. 7 | */ 8 | public final class GeneratedPluginRegistrant { 9 | public static void registerWith(PluginRegistry registry) { 10 | if (alreadyRegisteredWith(registry)) { 11 | return; 12 | } 13 | } 14 | 15 | private static boolean alreadyRegisteredWith(PluginRegistry registry) { 16 | final String key = GeneratedPluginRegistrant.class.getCanonicalName(); 17 | if (registry.hasPlugin(key)) { 18 | return true; 19 | } 20 | registry.registrarFor(key); 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/home/andre/MyInstalled/android-sdk-linux 2 | flutter.sdk=/home/andre/MyInstalled/flutter 3 | flutter.versionName=0.0.5 -------------------------------------------------------------------------------- /lib/src/bosh.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:math'; 4 | import 'dart:io' show HttpClient; 5 | import 'package:strophe/src/core.dart'; 6 | import 'package:strophe/src/enums.dart'; 7 | import 'package:strophe/src/sessionstorage.dart'; 8 | import 'package:xml/xml.dart' as xml; 9 | 10 | class StropheBosh extends ServiceType { 11 | StropheConnection _conn; 12 | 13 | int rid; 14 | 15 | String sid; 16 | 17 | int hold; 18 | 19 | int wait; 20 | 21 | int window; 22 | 23 | int errors; 24 | 25 | int inactivity; 26 | 27 | Map lastResponseHeaders; 28 | 29 | List _requests; 30 | 31 | bool disconnecting; 32 | 33 | StropheBosh(StropheConnection connection) { 34 | this._conn = connection; 35 | /* request id for body tags */ 36 | this.rid = new Random().nextInt(4294967295); 37 | /* The current session ID. */ 38 | this.sid = null; 39 | 40 | // default BOSH values 41 | this.hold = 1; 42 | this.wait = 60; 43 | this.window = 5; 44 | this.errors = 0; 45 | this.inactivity = null; 46 | 47 | this.lastResponseHeaders = null; 48 | 49 | this._requests = []; 50 | } 51 | @override 52 | StropheConnection get conn => null; 53 | /** Variable: strip 54 | * 55 | * BOSH-Connections will have all stanzas wrapped in a tag when 56 | * passed to or . 57 | * To strip this tag, User code can set to "body": 58 | * 59 | * > Strophe.Bosh.prototype.strip = "body"; 60 | * 61 | * This will enable stripping of the body tag in both 62 | * and . 63 | */ 64 | String strip; 65 | 66 | /** PrivateFunction: _buildBody 67 | * _Private_ helper function to generate the wrapper for BOSH. 68 | * 69 | * Returns: 70 | * A Strophe.Builder with a element. 71 | */ 72 | StanzaBuilder _buildBody() { 73 | StanzaBuilder bodyWrap = Strophe.$build( 74 | 'body', {'rid': this.rid++, 'xmlns': Strophe.NS['HTTPBIND']}); 75 | if (this.sid != null) { 76 | bodyWrap = bodyWrap.attrs({sid: this.sid}); 77 | } 78 | if (this._conn.options['keepalive'] && 79 | this._conn.sessionCachingSupported()) { 80 | this._cacheSession(); 81 | } 82 | return bodyWrap; 83 | } 84 | 85 | /** PrivateFunction: _reset 86 | * Reset the connection. 87 | * 88 | * This function is called by the reset function of the Strophe Connection 89 | */ 90 | reset() { 91 | this._reset(); 92 | } 93 | 94 | _reset() { 95 | this.rid = new Random().nextInt(4294967295); 96 | this.sid = null; 97 | this.errors = 0; 98 | if (this._conn.sessionCachingSupported()) { 99 | SessionStorage.removeItem('strophe-bosh-session'); 100 | } 101 | 102 | this._conn.nextValidRid(this.rid); 103 | } 104 | 105 | /** PrivateFunction: _connect 106 | * _Private_ function that initializes the BOSH connection. 107 | * 108 | * Creates and sends the Request that initializes the BOSH connection. 109 | */ 110 | connect([int wait, int hold, String route]) { 111 | _connect(wait, hold, route); 112 | } 113 | 114 | _connect([int wait, int hold, String route]) { 115 | this.wait = wait ?? this.wait; 116 | this.hold = hold ?? this.hold; 117 | this.errors = 0; 118 | 119 | // build the body tag 120 | var body = this._buildBody().attrs({ 121 | 'to': this._conn.domain, 122 | "xml:lang": "en", 123 | 'wait': this.wait, 124 | 'hold': this.hold, 125 | 'content': "text/xml; charset=utf-8", 126 | 'ver': "1.6", 127 | "xmpp:version": "1.0", 128 | "xmlns:xmpp": Strophe.NS['BOSH'] 129 | }); 130 | 131 | if (route != null && route.isNotEmpty) { 132 | body.attrs({route: route}); 133 | } 134 | StropheRequest req = 135 | new StropheRequest(body.tree(), null, body.tree().getAttribute("rid")); 136 | req.func = this._onRequestStateChange(this._conn.connectCb, req); 137 | req.origFunc = req.func; 138 | 139 | this._requests.add(req); 140 | this._throttledRequestHandler(); 141 | } 142 | 143 | /** PrivateFunction: _attach 144 | * Attach to an already created and authenticated BOSH session. 145 | * 146 | * This function is provided to allow Strophe to attach to BOSH 147 | * sessions which have been created externally, perhaps by a Web 148 | * application. This is often used to support auto-login type features 149 | * without putting user credentials into the page. 150 | * 151 | * Parameters: 152 | * (String) jid - The full JID that is bound by the session. 153 | * (String) sid - The SID of the BOSH session. 154 | * (String) rid - The current RID of the BOSH session. This RID 155 | * will be used by the next request. 156 | * (Function) callback The connect callback function. 157 | * (Integer) wait - The optional HTTPBIND wait value. This is the 158 | * time the server will wait before returning an empty result for 159 | * a request. The default setting of 60 seconds is recommended. 160 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 161 | * (Integer) hold - The optional HTTPBIND hold value. This is the 162 | * number of connections the server will hold at one time. This 163 | * should almost always be set to 1 (the default). 164 | * (Integer) wind - The optional HTTBIND window value. This is the 165 | * allowed range of request ids that are valid. The default is 5. 166 | */ 167 | void attach(String jid, String sid, int rid, Function callback, int wait, 168 | int hold, int wind) { 169 | this._attach(jid, sid, rid, callback, wait, hold, wind); 170 | } 171 | 172 | _attach(String jid, String sid, int rid, Function callback, int wait, 173 | int hold, int wind) { 174 | this._conn.jid = jid; 175 | this.sid = sid; 176 | this.rid = rid; 177 | 178 | this._conn.connectCallback = callback; 179 | 180 | this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); 181 | 182 | this._conn.authenticated = true; 183 | this._conn.connected = true; 184 | 185 | this.wait = wait ?? this.wait; 186 | this.hold = hold ?? this.hold; 187 | this.window = wind ?? this.window; 188 | 189 | this._conn.changeConnectStatus(Strophe.Status['ATTACHED'], null); 190 | } 191 | 192 | /** PrivateFunction: _restore 193 | * Attempt to restore a cached BOSH session 194 | * 195 | * Parameters: 196 | * (String) jid - The full JID that is bound by the session. 197 | * This parameter is optional but recommended, specifically in cases 198 | * where prebinded BOSH sessions are used where it's important to know 199 | * that the right session is being restored. 200 | * (Function) callback The connect callback function. 201 | * (Integer) wait - The optional HTTPBIND wait value. This is the 202 | * time the server will wait before returning an empty result for 203 | * a request. The default setting of 60 seconds is recommended. 204 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 205 | * (Integer) hold - The optional HTTPBIND hold value. This is the 206 | * number of connections the server will hold at one time. This 207 | * should almost always be set to 1 (the default). 208 | * (Integer) wind - The optional HTTBIND window value. This is the 209 | * allowed range of request ids that are valid. The default is 5. 210 | */ 211 | void restore(String jid, Function callback, int wait, int hold, int wind) { 212 | this._restore(jid, callback, wait, hold, wind); 213 | } 214 | 215 | _restore(String jid, Function callback, int wait, int hold, int wind) { 216 | var session = 217 | JsonCodec().decode(SessionStorage.getItem('strophe-bosh-session')); 218 | if (session != null && 219 | session.rid && 220 | session.sid && 221 | session.jid && 222 | (jid == null || 223 | Strophe.getBareJidFromJid(session.jid) == 224 | Strophe.getBareJidFromJid(jid) || 225 | // If authcid is null, then it's an anonymous login, so 226 | // we compare only the domains: 227 | ((Strophe.getNodeFromJid(jid) == null) && 228 | (Strophe.getDomainFromJid(session.jid) == jid)))) { 229 | this._conn.restored = true; 230 | this._attach( 231 | session.jid, session.sid, session.rid, callback, wait, hold, wind); 232 | } else { 233 | throw { 234 | 'name': "StropheSessionError", 235 | 'message': "_restore: no restoreable session." 236 | }; 237 | } 238 | } 239 | 240 | /** PrivateFunction: _cacheSession 241 | * _Private_ handler for the beforeunload event. 242 | * 243 | * This handler is used to process the Bosh-part of the initial request. 244 | * Parameters: 245 | * (Request) bodyWrap - The received stanza. 246 | */ 247 | void _cacheSession() { 248 | if (this._conn.authenticated) { 249 | if (this._conn.jid != null && 250 | this._conn.jid.isNotEmpty && 251 | this.rid != null && 252 | this.rid != 0 && 253 | this.sid != null && 254 | this.sid.isNotEmpty) { 255 | SessionStorage.setItem( 256 | 'strophe-bosh-session', 257 | JsonCodec().encode( 258 | {'jid': this._conn.jid, 'rid': this.rid, 'sid': this.sid})); 259 | } 260 | } else { 261 | SessionStorage.removeItem('strophe-bosh-session'); 262 | } 263 | } 264 | 265 | /** PrivateFunction: _connect_cb 266 | * _Private_ handler for initial connection request. 267 | * 268 | * This handler is used to process the Bosh-part of the initial request. 269 | * Parameters: 270 | * (Request) bodyWrap - The received stanza. 271 | */ 272 | connectCb(xml.XmlElement bodyWrap) { 273 | this._connectCb(bodyWrap); 274 | } 275 | 276 | _connectCb(xml.XmlElement bodyWrap) { 277 | String typ = bodyWrap.getAttribute("type"); 278 | String cond; 279 | List conflict; 280 | if (typ != null && typ == "terminate") { 281 | // an error occurred 282 | cond = bodyWrap.getAttribute("condition"); 283 | Strophe.error("BOSH-Connection failed: " + cond); 284 | conflict = bodyWrap.findAllElements("conflict"); 285 | if (cond != null) { 286 | if (cond == "remote-stream-error" && conflict.length > 0) { 287 | cond = "conflict"; 288 | } 289 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], cond); 290 | } else { 291 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], "unknown"); 292 | } 293 | this._conn.doDisconnect(cond); 294 | return Strophe.Status['CONNFAIL']; 295 | } 296 | 297 | // check to make sure we don't overwrite these if _connect_cb is 298 | // called multiple times in the case of missing stream:features 299 | if (this.sid == null || this.sid.isEmpty) { 300 | this.sid = bodyWrap.getAttribute("sid"); 301 | } 302 | String wind = bodyWrap.getAttribute('requests'); 303 | if (wind != null) { 304 | this.window = int.parse(wind); 305 | } 306 | String hold = bodyWrap.getAttribute('hold'); 307 | if (hold != null) { 308 | this.hold = int.parse(hold); 309 | } 310 | String wait = bodyWrap.getAttribute('wait'); 311 | if (wait != null) { 312 | this.wait = int.parse(wait); 313 | } 314 | String inactivity = bodyWrap.getAttribute('inactivity'); 315 | if (inactivity != null) { 316 | this.inactivity = int.parse(inactivity); 317 | } 318 | } 319 | 320 | /** PrivateFunction: _disconnect 321 | * _Private_ part of Connection.disconnect for Bosh 322 | * 323 | * Parameters: 324 | * (Request) pres - This stanza will be sent before disconnecting. 325 | */ 326 | disconnect(xml.XmlElement pres) { 327 | this._disconnect(pres); 328 | } 329 | 330 | _disconnect(xml.XmlElement pres) { 331 | this._sendTerminate(pres); 332 | } 333 | 334 | /** PrivateFunction: _doDisconnect 335 | * _Private_ function to disconnect. 336 | * 337 | * Resets the SID and RID. 338 | */ 339 | doDisconnect() { 340 | this._doDisconnect(); 341 | } 342 | 343 | _doDisconnect() { 344 | this.sid = null; 345 | this.rid = new Random().nextInt(4294967295); 346 | if (this._conn.sessionCachingSupported()) { 347 | SessionStorage.removeItem('strophe-bosh-session'); 348 | } 349 | 350 | this._conn.nextValidRid(this.rid); 351 | } 352 | 353 | /** PrivateFunction: _emptyQueue 354 | * _Private_ function to check if the Request queue is empty. 355 | * 356 | * Returns: 357 | * True, if there are no Requests queued, False otherwise. 358 | */ 359 | bool emptyQueue() { 360 | return this._emptyQueue(); 361 | } 362 | 363 | bool _emptyQueue() { 364 | return this._requests.length == 0; 365 | } 366 | 367 | /** PrivateFunction: _callProtocolErrorHandlers 368 | * _Private_ function to call error handlers registered for HTTP errors. 369 | * 370 | * Parameters: 371 | * (Request) req - The request that is changing readyState. 372 | */ 373 | _callProtocolErrorHandlers(StropheRequest req) { 374 | int reqStatus = this._getRequestStatus(req); 375 | Function err_callback = this._conn.protocolErrorHandlers['HTTP'][reqStatus]; 376 | if (err_callback != null) { 377 | err_callback.call(this, reqStatus); 378 | } 379 | } 380 | 381 | /** PrivateFunction: _hitError 382 | * _Private_ function to handle the error count. 383 | * 384 | * Requests are resent automatically until their error count reaches 385 | * 5. Each time an error is encountered, this function is called to 386 | * increment the count and disconnect if the count is too high. 387 | * 388 | * Parameters: 389 | * (Integer) reqStatus - The request status. 390 | */ 391 | void _hitError(int reqStatus) { 392 | this.errors++; 393 | Strophe.warn("request errored, status: " + 394 | reqStatus.toString() + 395 | ", number of errors: " + 396 | this.errors.toString()); 397 | if (this.errors > 4) { 398 | this._conn.onDisconnectTimeout(); 399 | } 400 | } 401 | 402 | /** PrivateFunction: _onDisconnectTimeout 403 | * _Private_ timeout handler for handling non-graceful disconnection. 404 | * 405 | * Cancels all remaining Requests and clears the queue. 406 | */ 407 | onDisconnectTimeout() { 408 | this._onDisconnectTimeout(); 409 | } 410 | 411 | _onDisconnectTimeout() { 412 | this._abortAllRequests(); 413 | } 414 | 415 | /** PrivateFunction: _abortAllRequests 416 | * _Private_ helper function that makes sure all pending requests are aborted. 417 | */ 418 | abortAllRequests() { 419 | this._abortAllRequests(); 420 | } 421 | 422 | _abortAllRequests() { 423 | StropheRequest req; 424 | while (this._requests.length > 0) { 425 | req = this._requests.removeLast(); 426 | req.abort = true; 427 | req.xhr.close(); 428 | } 429 | } 430 | 431 | /** PrivateFunction: _onIdle 432 | * _Private_ handler called by Strophe.Connection._onIdle 433 | * 434 | * Sends all queued Requests or polls with empty Request if there are none. 435 | */ 436 | onIdle() { 437 | this._onIdle(); 438 | } 439 | 440 | _onIdle() { 441 | var data = this._conn.data; 442 | // if no requests are in progress, poll 443 | if (this._conn.authenticated && 444 | this._requests.length == 0 && 445 | data.length == 0 && 446 | !this._conn.disconnecting) { 447 | Strophe.info("no requests during idle cycle, sending " + "blank request"); 448 | data.add(null); 449 | } 450 | 451 | if (this._conn.paused) { 452 | return; 453 | } 454 | 455 | if (this._requests.length < 2 && data.length > 0) { 456 | StanzaBuilder body = this._buildBody(); 457 | for (int i = 0; i < data.length; i++) { 458 | if (data[i] != null) { 459 | if (data[i] == "restart") { 460 | body.attrs({ 461 | 'to': this._conn.domain, 462 | "xml:lang": "en", 463 | "xmpp:restart": "true", 464 | "xmlns:xmpp": Strophe.NS['BOSH'] 465 | }); 466 | } else { 467 | body.cnode(data[i]).up(); 468 | } 469 | } 470 | } 471 | this._conn.data = []; 472 | StropheRequest req = new StropheRequest( 473 | body.tree(), null, body.tree().getAttribute("rid")); 474 | req.func = this._onRequestStateChange(this._conn.dataRecv, req); 475 | req.origFunc = req.func; 476 | this._requests.add(req); 477 | this._throttledRequestHandler(); 478 | } 479 | 480 | if (this._requests.length > 0) { 481 | var time_elapsed = this._requests[0].age(); 482 | if (this._requests[0].dead != null) { 483 | if (this._requests[0].timeDead() > 484 | (Strophe.SECONDARY_TIMEOUT * this.wait).floor()) { 485 | this._throttledRequestHandler(); 486 | } 487 | } 488 | 489 | if (time_elapsed > (Strophe.TIMEOUT * this.wait).floor()) { 490 | Strophe.warn("Request " + 491 | this._requests[0].id.toString() + 492 | " timed out, over " + 493 | (Strophe.TIMEOUT * this.wait).floor().toString() + 494 | " seconds since last activity"); 495 | this._throttledRequestHandler(); 496 | } 497 | } 498 | } 499 | 500 | /** PrivateFunction: _getRequestStatus 501 | * 502 | * Returns the HTTP status code from a Request 503 | * 504 | * Parameters: 505 | * (Request) req - The Request instance. 506 | * (Integer) def - The default value that should be returned if no 507 | * status value was found. 508 | */ 509 | int _getRequestStatus(StropheRequest req, [num def]) { 510 | int reqStatus; 511 | if (req.response != null) { 512 | try { 513 | reqStatus = req.response.statusCode; 514 | } catch (e) { 515 | Strophe.error("Caught an error while retrieving a request's status, " + 516 | "reqStatus: " + 517 | reqStatus.toString()); 518 | } 519 | } 520 | if (reqStatus == null) { 521 | reqStatus = def ?? 0; 522 | } 523 | return reqStatus; 524 | } 525 | 526 | /** PrivateFunction: _onRequestStateChange 527 | * _Private_ handler for Request state changes. 528 | * 529 | * This function is called when the XMLHttpRequest readyState changes. 530 | * It contains a lot of error handling logic for the many ways that 531 | * requests can fail, and calls the request callback when requests 532 | * succeed. 533 | * 534 | * Parameters: 535 | * (Function) func - The handler for the request. 536 | * (Request) req - The request that is changing readyState. 537 | */ 538 | _onRequestStateChange(Function func, StropheRequest req) { 539 | Strophe.debug("request id " + 540 | req.id.toString() + 541 | "." + 542 | req.sends.toString() + 543 | " state changed to " + 544 | (req.response != null ? req.response.statusCode : "0")); 545 | if (req.abort) { 546 | req.abort = false; 547 | return; 548 | } 549 | if (req.response != null && 550 | req.response.statusCode != 200 && 551 | req.response.statusCode != 304) { 552 | // The request is not yet complete 553 | return; 554 | } 555 | int reqStatus = this._getRequestStatus(req); 556 | this.lastResponseHeaders = req.response.headers; 557 | if (this.disconnecting && reqStatus >= 400) { 558 | this._hitError(reqStatus); 559 | this._callProtocolErrorHandlers(req); 560 | return; 561 | } 562 | 563 | bool valid_request = reqStatus > 0 && reqStatus < 500; 564 | bool too_many_retries = req.sends > this._conn.maxRetries; 565 | if (valid_request || too_many_retries) { 566 | // remove from internal queue 567 | this._removeRequest(req); 568 | Strophe.debug( 569 | "request id " + req.id.toString() + " should now be removed"); 570 | } 571 | 572 | if (reqStatus == 200) { 573 | // request succeeded 574 | bool reqIs0 = (this._requests[0] == req); 575 | bool reqIs1 = (this._requests[1] == req); 576 | // if request 1 finished, or request 0 finished and request 577 | // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to 578 | // restart the other - both will be in the first spot, as the 579 | // completed request has been removed from the queue already 580 | if (reqIs1 || 581 | (reqIs0 && 582 | this._requests.length > 0 && 583 | this._requests[0].age() > 584 | (Strophe.SECONDARY_TIMEOUT * this.wait).floor())) { 585 | this._restartRequest(0); 586 | } 587 | this._conn.nextValidRid(int.parse(req.rid) + 1); 588 | Strophe.debug("request id " + 589 | req.id.toString() + 590 | "." + 591 | req.sends.toString() + 592 | " got 200"); 593 | func(req); // call handler 594 | this.errors = 0; 595 | } else if (reqStatus == 0 || 596 | (reqStatus >= 400 && reqStatus < 600) || 597 | reqStatus >= 12000) { 598 | // request failed 599 | Strophe.error("request id " + 600 | req.id.toString() + 601 | "." + 602 | req.sends.toString() + 603 | " error " + 604 | reqStatus.toString() + 605 | " happened"); 606 | this._hitError(reqStatus); 607 | this._callProtocolErrorHandlers(req); 608 | if (reqStatus >= 400 && reqStatus < 500) { 609 | this._conn.changeConnectStatus(Strophe.Status['DISCONNECTING'], null); 610 | this._conn.doDisconnect(); 611 | } 612 | } else { 613 | Strophe.error("request id " + 614 | req.id.toString() + 615 | "." + 616 | req.sends.toString() + 617 | " error " + 618 | reqStatus.toString() + 619 | " happened"); 620 | } 621 | 622 | if (!valid_request && !too_many_retries) { 623 | this._throttledRequestHandler(); 624 | } else if (too_many_retries && !this._conn.connected) { 625 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], "giving-up"); 626 | } 627 | } 628 | 629 | /** PrivateFunction: _processRequest 630 | * _Private_ function to process a request in the queue. 631 | * 632 | * This function takes requests off the queue and sends them and 633 | * restarts dead requests. 634 | * 635 | * Parameters: 636 | * (Integer) i - The index of the request in the queue. 637 | */ 638 | _processRequest(int i) { 639 | StropheRequest req = this._requests[i]; 640 | int reqStatus = this._getRequestStatus(req, -1); 641 | 642 | // make sure we limit the number of retries 643 | if (req.sends > this._conn.maxRetries) { 644 | this._conn.onDisconnectTimeout(); 645 | return; 646 | } 647 | 648 | var time_elapsed = req.age(); 649 | var primaryTimeout = (time_elapsed is num && 650 | time_elapsed > (Strophe.TIMEOUT * this.wait).floor()); 651 | var secondaryTimeout = (req.dead != null && 652 | req.timeDead() > (Strophe.SECONDARY_TIMEOUT * this.wait).floor()); 653 | var requestCompletedWithServerError = 654 | (req.response != null && (reqStatus < 1 || reqStatus >= 500)); 655 | if (primaryTimeout || secondaryTimeout || requestCompletedWithServerError) { 656 | if (secondaryTimeout) { 657 | Strophe.error("Request " + 658 | this._requests[i].id.toString() + 659 | " timed out (secondary), restarting"); 660 | } 661 | req.abort = true; 662 | req.xhr.close(); 663 | this._requests[i] = 664 | new StropheRequest(req.xmlData, req.origFunc, req.rid, req.sends); 665 | req = this._requests[i]; 666 | } 667 | 668 | if (req.response == null) { 669 | Strophe.debug("request id " + 670 | req.id.toString() + 671 | "." + 672 | req.sends.toString() + 673 | " posting"); 674 | 675 | // Fires the XHR request -- may be invoked immediately 676 | // or on a gradually expanding retry window for reconnects 677 | 678 | // Implement progressive backoff for reconnects -- 679 | // First retry (send == 1) should also be instantaneous 680 | if (req.sends > 1) { 681 | // Using a cube of the retry number creates a nicely 682 | // expanding retry window 683 | num backoff = 684 | min((Strophe.TIMEOUT * this.wait).floor(), pow(req.sends, 3)) * 685 | 1000; 686 | new Timer(new Duration(milliseconds: backoff), () { 687 | // XXX: setTimeout should be called only with function expressions (23974bc1) 688 | this._sendFunc(req); 689 | }); 690 | } else { 691 | this._sendFunc(req); 692 | } 693 | 694 | req.sends++; 695 | 696 | //if (this._conn.xmlOutput != Strophe.Connection.xmlOutput) { 697 | if (req.xmlData.name == this.strip && req.xmlData.children.length > 0) { 698 | this._conn.xmlOutput(req.xmlData.firstChild); 699 | } else { 700 | this._conn.xmlOutput(req.xmlData); 701 | } 702 | //} 703 | //if (this._conn.rawOutput != Strophe.Connection.rawOutput) { 704 | this._conn.rawOutput(req.data); 705 | //} 706 | } else { 707 | Strophe.debug("_processRequest: " + 708 | (i == 0 ? "first" : "second") + 709 | " request has readyState of " + 710 | (req.response != null ? req.response.reasonPhrase : "0")); 711 | } 712 | } 713 | 714 | _sendFunc(StropheRequest req) { 715 | String contentType; 716 | Map map; 717 | var request; 718 | HttpClient httpClient = HttpClient(); 719 | try { 720 | contentType = 721 | this._conn.options['contentType'] ?? "text/xml; charset=utf-8"; 722 | request = httpClient.getUrl(Uri.parse(this._conn.service)); 723 | request.persistentConnection = this._conn.options['sync'] ? false : true; 724 | map = {"Content-Type": contentType}; 725 | if (this._conn.options['withCredentials']) { 726 | map['withCredentials'] = true; 727 | } 728 | } catch (e2) { 729 | Strophe.error("XHR open failed: " + e2.toString()); 730 | if (!this._conn.connected) { 731 | this 732 | ._conn 733 | .changeConnectStatus(Strophe.Status['CONNFAIL'], "bad-service"); 734 | } 735 | this._conn.disconnect(); 736 | return; 737 | } 738 | req.date = new DateTime.now().millisecondsSinceEpoch; 739 | if (this._conn.options['customHeaders']) { 740 | var headers = this._conn.options['customHeaders']; 741 | for (var header in headers) { 742 | map[header] = headers[header]; 743 | } 744 | } 745 | 746 | request.bodyFields = map; 747 | /* req.xhr.send(request).then((http.StreamedResponse response) { 748 | req.response = response as http.Response; 749 | }).catchError(() {}); */ 750 | } 751 | 752 | /** PrivateFunction: _removeRequest 753 | * _Private_ function to remove a request from the queue. 754 | * 755 | * Parameters: 756 | * (Request) req - The request to remove. 757 | */ 758 | void _removeRequest(StropheRequest req) { 759 | Strophe.debug("removing request"); 760 | for (int i = this._requests.length - 1; i >= 0; i--) { 761 | if (req == this._requests[i]) { 762 | this._requests.removeAt(i); 763 | } 764 | } 765 | this._throttledRequestHandler(); 766 | } 767 | 768 | /** PrivateFunction: _restartRequest 769 | * _Private_ function to restart a request that is presumed dead. 770 | * 771 | * Parameters: 772 | * (Integer) i - The index of the request in the queue. 773 | */ 774 | _restartRequest(i) { 775 | var req = this._requests[i]; 776 | if (req.dead == null) { 777 | req.dead = new DateTime.now().millisecondsSinceEpoch; 778 | } 779 | 780 | this._processRequest(i); 781 | } 782 | 783 | /** PrivateFunction: _reqToData 784 | * _Private_ function to get a stanza out of a request. 785 | * 786 | * Tries to extract a stanza out of a Request Object. 787 | * When this fails the current connection will be disconnected. 788 | * 789 | * Parameters: 790 | * (Object) req - The Request. 791 | * 792 | * Returns: 793 | * The stanza that was passed. 794 | */ 795 | xml.XmlElement reqToData(dynamic req) { 796 | req = req as StropheRequest; 797 | return this._reqToData(req); 798 | } 799 | 800 | xml.XmlElement _reqToData(StropheRequest req) { 801 | try { 802 | return req.getResponse(); 803 | } catch (e) { 804 | if (e != "parsererror") { 805 | throw e; 806 | } 807 | this._conn.disconnect("strophe-parsererror"); 808 | return null; 809 | } 810 | } 811 | 812 | /** PrivateFunction: _sendTerminate 813 | * _Private_ function to send initial disconnect sequence. 814 | * 815 | * This is the first step in a graceful disconnect. It sends 816 | * the BOSH server a terminate body and includes an unavailable 817 | * presence if authentication has completed. 818 | */ 819 | _sendTerminate(pres) { 820 | Strophe.info("_sendTerminate was called"); 821 | StanzaBuilder body = this._buildBody().attrs({'type': "terminate"}); 822 | if (pres) { 823 | body.cnode(pres.tree()); 824 | } 825 | StropheRequest req = 826 | new StropheRequest(body.tree(), null, body.tree().getAttribute("rid")); 827 | req.func = this._onRequestStateChange(this._conn.dataRecv, req); 828 | req.origFunc = req.func; 829 | this._requests.add(req); 830 | this._throttledRequestHandler(); 831 | } 832 | 833 | /** PrivateFunction: _send 834 | * _Private_ part of the Connection.send function for BOSH 835 | * 836 | * Just triggers the RequestHandler to send the messages that are in the queue 837 | */ 838 | send() { 839 | this._send(); 840 | } 841 | 842 | _send() { 843 | if (this._conn.idleTimeout != null) this._conn.idleTimeout.cancel(); 844 | this._throttledRequestHandler(); 845 | 846 | // XXX: setTimeout should be called only with function expressions (23974bc1) 847 | this._conn.idleTimeout = new Timer(new Duration(milliseconds: 100), () { 848 | this._onIdle(); 849 | }); 850 | } 851 | 852 | /** PrivateFunction: _sendRestart 853 | * 854 | * Send an xmpp:restart stanza. 855 | */ 856 | sendRestart() { 857 | this._sendRestart(); 858 | } 859 | 860 | _sendRestart() { 861 | this._throttledRequestHandler(); 862 | if (this._conn.idleTimeout != null) this._conn.idleTimeout.cancel(); 863 | } 864 | 865 | /** PrivateFunction: _throttledRequestHandler 866 | * _Private_ function to throttle requests to the connection window. 867 | * 868 | * This function makes sure we don't send requests so fast that the 869 | * request ids overflow the connection window in the case that one 870 | * request died. 871 | */ 872 | _throttledRequestHandler() { 873 | if (this._requests == null) { 874 | Strophe.debug( 875 | "_throttledRequestHandler called with " + "undefined requests"); 876 | } else { 877 | Strophe.debug("_throttledRequestHandler called with " + 878 | this._requests.length.toString() + 879 | " requests"); 880 | } 881 | 882 | if (this._requests == null || this._requests.length == 0) { 883 | return; 884 | } 885 | 886 | if (this._requests.length > 0) { 887 | this._processRequest(0); 888 | } 889 | if (this._requests.length > 1 && 890 | (int.parse(this._requests[0].rid) - int.parse(this._requests[1].rid)) 891 | .abs() < 892 | this.window) { 893 | this._processRequest(1); 894 | } 895 | } 896 | } 897 | /** PrivateClass: Request 898 | * _Private_ helper class that provides a cross implementation abstraction 899 | * for a BOSH related XMLHttpRequest. 900 | * 901 | * The Request class is used internally to encapsulate BOSH request 902 | * information. It is not meant to be used from user's code. 903 | */ 904 | 905 | /** PrivateConstructor: Request 906 | * Create and initialize a new Request object. 907 | * 908 | * Parameters: 909 | * (XMLElement) elem - The XML data to be sent in the request. 910 | * (Function) func - The function that will be called when the 911 | * XMLHttpRequest readyState changes. 912 | * (Integer) rid - The BOSH rid attribute associated with this request. 913 | * (Integer) sends - The number of times this same request has been sent. 914 | */ 915 | class StropheRequest { 916 | int id; 917 | 918 | xml.XmlElement xmlData; 919 | 920 | String data; 921 | 922 | Function origFunc; 923 | 924 | Function func; 925 | 926 | num date; 927 | 928 | String rid; 929 | 930 | int sends; 931 | 932 | bool abort; 933 | 934 | int dead; 935 | 936 | //http.Client 937 | var xhr; 938 | //http.Response 939 | var response; 940 | 941 | StropheRequest(xml.XmlElement elem, Function func, String rid, [int sends]) { 942 | this.id = ++Strophe.requestId; 943 | this.xmlData = elem; 944 | this.data = Strophe.serialize(elem); 945 | // save original function in case we need to make a new request 946 | // from this one. 947 | this.origFunc = func; 948 | this.func = func; 949 | this.rid = rid; 950 | this.date = null; 951 | this.sends = sends ?? 0; 952 | this.abort = false; 953 | this.dead = null; 954 | this.age(); 955 | this.xhr = this._newXHR(); 956 | } 957 | num timeDead() { 958 | if (this.dead == null) { 959 | return 0; 960 | } 961 | int now = new DateTime.now().millisecondsSinceEpoch; 962 | return (now - this.dead) / 1000; 963 | } 964 | 965 | num age() { 966 | if (this.date == null || this.date == 0) { 967 | return 0; 968 | } 969 | int now = new DateTime.now().millisecondsSinceEpoch; 970 | return (now - this.date) / 1000; 971 | } 972 | 973 | /** PrivateFunction: getResponse 974 | * Get a response from the underlying XMLHttpRequest. 975 | * 976 | * This function attempts to get a response from the request and checks 977 | * for errors. 978 | * 979 | * Throws: 980 | * "parsererror" - A parser error occured. 981 | * "badformat" - The entity has sent XML that cannot be processed. 982 | * 983 | * Returns: 984 | * The DOM element tree of the response. 985 | */ 986 | xml.XmlElement getResponse() { 987 | String body = response.body; 988 | xml.XmlElement node = null; 989 | try { 990 | node = xml.parse(body).rootElement; 991 | Strophe.error("responseXML: " + Strophe.serialize(node)); 992 | if (node == null) { 993 | throw {'message': 'Parsing produced null node'}; 994 | } 995 | } catch (e) { 996 | // if (node.name == "parsererror") { 997 | Strophe.error("invalid response received" + e.toString()); 998 | Strophe.error("responseText: " + body); 999 | throw "parsererror"; 1000 | //} 1001 | } 1002 | return node; 1003 | } 1004 | 1005 | /** PrivateFunction: _newXHR 1006 | * _Private_ helper function to create XMLHttpRequests. 1007 | * 1008 | * This function creates XMLHttpRequests across all implementations. 1009 | * 1010 | * Returns: 1011 | * A new XMLHttpRequest. 1012 | */ 1013 | _newXHR() { 1014 | //return new http.Client(); 1015 | } 1016 | } 1017 | -------------------------------------------------------------------------------- /lib/src/core.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/bosh.dart'; 2 | import 'package:strophe/src/plugins/plugins.dart'; 3 | import 'package:strophe/src/websocket.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | import 'package:strophe/src/enums.dart'; 6 | 7 | class Strophe { 8 | static const String VERSION = '0.0.1'; 9 | /** PrivateConstants: Timeout Values 10 | * Timeout values for error states. These values are in seconds. 11 | * These should not be changed unless you know exactly what you are 12 | * doing. 13 | * 14 | * TIMEOUT - Timeout multiplier. A waiting request will be considered 15 | * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. 16 | * This defaults to 1.1, and with default wait, 66 seconds. 17 | * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where 18 | * Strophe can detect early failure, it will consider the request 19 | * failed if it doesn't return after 20 | * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. 21 | * This defaults to 0.1, and with default wait, 6 seconds. 22 | */ 23 | static const num TIMEOUT = 1.1; 24 | static const num SECONDARY_TIMEOUT = 0.1; 25 | static Map Status = ConnexionStatus; 26 | static Map NS = NAMESPACE; 27 | static const Map ErrorCondition = ERRORSCONDITIONS; 28 | static const Map LogLevel = LOGLEVEL; 29 | static const Map ElementType = ELEMENTTYPE; 30 | static Map XHTML = { 31 | "tags": [ 32 | 'a', 33 | 'blockquote', 34 | 'br', 35 | 'cite', 36 | 'em', 37 | 'img', 38 | 'li', 39 | 'ol', 40 | 'p', 41 | 'span', 42 | 'strong', 43 | 'ul', 44 | 'body' 45 | ], 46 | 'attributes': { 47 | 'a': ['href'], 48 | 'blockquote': ['style'], 49 | 'br': [], 50 | 'cite': ['style'], 51 | 'em': [], 52 | 'img': ['src', 'alt', 'style', 'height', 'width'], 53 | 'li': ['style'], 54 | 'ol': ['style'], 55 | 'p': ['style'], 56 | 'span': ['style'], 57 | 'strong': [], 58 | 'ul': ['style'], 59 | 'body': [] 60 | }, 61 | 'css': [ 62 | 'background-color', 63 | 'color', 64 | 'font-family', 65 | 'font-size', 66 | 'font-style', 67 | 'font-weight', 68 | 'margin-left', 69 | 'margin-right', 70 | 'text-align', 71 | 'text-decoration' 72 | ], 73 | /** Function: XHTML.validTag 74 | * 75 | * Utility method to determine whether a tag is allowed 76 | * in the XHTML_IM namespace. 77 | * 78 | * XHTML tag names are case sensitive and must be lower case. 79 | */ 80 | 'validTag': (String tag) { 81 | for (int i = 0; i < Strophe.XHTML['tags'].length; i++) { 82 | if (tag == Strophe.XHTML['tags'][i]) { 83 | return true; 84 | } 85 | } 86 | return false; 87 | }, 88 | /** Function: XHTML.validAttribute 89 | * 90 | * Utility method to determine whether an attribute is allowed 91 | * as recommended per XEP-0071 92 | * 93 | * XHTML attribute names are case sensitive and must be lower case. 94 | */ 95 | 'validAttribute': (String tag, String attribute) { 96 | if (Strophe.XHTML['attributes'][tag] != null && 97 | Strophe.XHTML['attributes'][tag].length > 0) { 98 | for (int i = 0; i < Strophe.XHTML['attributes'][tag].length; i++) { 99 | if (attribute == Strophe.XHTML['attributes'][tag][i]) { 100 | return true; 101 | } 102 | } 103 | } 104 | return false; 105 | }, 106 | 'validCSS': (style) { 107 | for (int i = 0; i < Strophe.XHTML['css'].length; i++) { 108 | if (style == Strophe.XHTML['css'][i]) { 109 | return true; 110 | } 111 | } 112 | return false; 113 | } 114 | }; 115 | 116 | /** Function: addNamespace 117 | * This function is used to extend the current namespaces in 118 | * Strophe.NS. It takes a key and a value with the key being the 119 | * name of the new namespace, with its actual value. 120 | * For example: 121 | * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); 122 | * 123 | * Parameters: 124 | * (String) name - The name under which the namespace will be 125 | * referenced under Strophe.NS 126 | * (String) value - The actual namespace. 127 | */ 128 | static void addNamespace(String name, String value) { 129 | Strophe.NS[name] = value; 130 | } 131 | 132 | /** Function: forEachChild 133 | * Map a function over some or all child elements of a given element. 134 | * 135 | * This is a small convenience function for mapping a function over 136 | * some or all of the children of an element. If elemName is null, all 137 | * children will be passed to the function, otherwise only children 138 | * whose tag names match elemName will be passed. 139 | * 140 | * Parameters: 141 | * (XMLElement) elem - The element to operate on. 142 | * (String) elemName - The child element tag name filter. 143 | * (Function) func - The function to apply to each child. This 144 | * function should take a single argument, a DOM element. 145 | */ 146 | static void forEachChild(elem, String elemName, Function func) { 147 | if (elem == null) return; 148 | var childNode; 149 | for (int i = 0; i < elem.children.length; i++) { 150 | try { 151 | if (elem.children.elementAt(i) is xml.XmlElement) 152 | childNode = elem.children.elementAt(i); 153 | else if (elem.children.elementAt(i) is xml.XmlDocument) 154 | childNode = elem.rootElement.children.elementAt(i); 155 | } catch (e) { 156 | childNode = null; 157 | } 158 | if (childNode == null) continue; 159 | if (childNode.nodeType == xml.XmlNodeType.ELEMENT && 160 | (elemName == null || isTagEqual(childNode, elemName))) { 161 | func(childNode); 162 | } 163 | } 164 | } 165 | 166 | /** Function: isTagEqual 167 | * Compare an element's tag name with a string. 168 | * 169 | * This function is case sensitive. 170 | * 171 | * Parameters: 172 | * (XMLElement) el - A DOM element. 173 | * (String) name - The element name. 174 | * 175 | * Returns: 176 | * true if the element's tag name matches _el_, and false 177 | * otherwise. 178 | */ 179 | static bool isTagEqual(xml.XmlElement el, String name) { 180 | return el.name.qualified == name; 181 | } 182 | 183 | /** PrivateVariable: _xmlGenerator 184 | * _Private_ variable that caches a DOM document to 185 | * generate elements. 186 | */ 187 | static xml.XmlBuilder _xmlGenerator; 188 | 189 | /** PrivateFunction: _makeGenerator 190 | * _Private_ function that creates a dummy XML DOM document to serve as 191 | * an element and text node generator. 192 | */ 193 | static xml.XmlBuilder _makeGenerator() { 194 | xml.XmlBuilder builder = new xml.XmlBuilder(); 195 | //builder.element('strophe', namespace: 'jabber:client'); 196 | return builder; 197 | } 198 | 199 | /** Function: xmlGenerator 200 | * Get the DOM document to generate elements. 201 | * 202 | * Returns: 203 | * The currently used DOM document. 204 | */ 205 | static xml.XmlBuilder xmlGenerator() { 206 | //if (Strophe._xmlGenerator == null) { 207 | Strophe._xmlGenerator = Strophe._makeGenerator(); 208 | //} 209 | return Strophe._xmlGenerator; 210 | } 211 | 212 | /** Function: xmlElement 213 | * Create an XML DOM element. 214 | * 215 | * This function creates an XML DOM element correctly across all 216 | * implementations. Note that these are not HTML DOM elements, which 217 | * aren't appropriate for XMPP stanzas. 218 | * 219 | * Parameters: 220 | * (String) name - The name for the element. 221 | * (Array|Object) attrs - An optional array or object containing 222 | * key/value pairs to use as element attributes. The object should 223 | * be in the format {'key': 'value'} or {key: 'value'}. The array 224 | * should have the format [['key1', 'value1'], ['key2', 'value2']]. 225 | * (String) text - The text child data for the element. 226 | * 227 | * Returns: 228 | * A new XML DOM element. 229 | */ 230 | static xml.XmlNode xmlElement(String name, {dynamic attrs, String text}) { 231 | if (name == null || name.isEmpty || name.trim().length == 0) { 232 | return null; 233 | } 234 | if (attrs != null && 235 | (attrs is! List>) && 236 | (attrs is! Map)) { 237 | return null; 238 | } 239 | Map attributes = {}; 240 | if (attrs != null) { 241 | if (attrs is List>) { 242 | for (int i = 0; i < attrs.length; i++) { 243 | List attr = attrs[i]; 244 | if (attr.length == 2 && attr[1] != null && attr.isNotEmpty) { 245 | attributes[attr[0]] = attr[1].toString(); 246 | } 247 | } 248 | } else if (attrs is Map) { 249 | List keys = attrs.keys.toList(); 250 | for (int i = 0, len = keys.length; i < len; i++) { 251 | String key = keys[i]; 252 | if (key != null && key.isNotEmpty && attrs[key] != null) { 253 | attributes[key] = attrs[key].toString(); 254 | } 255 | } 256 | } 257 | } 258 | xml.XmlBuilder builder = Strophe.xmlGenerator(); 259 | builder.element(name, attributes: attributes, nest: text); 260 | return builder.build(); 261 | } 262 | /* Function: xmlescape 263 | * Excapes invalid xml characters. 264 | * 265 | * Parameters: 266 | * (String) text - text to escape. 267 | * 268 | * Returns: 269 | * Escaped text. 270 | */ 271 | 272 | static String xmlescape(String text) { 273 | text = text.replaceAll(new RegExp(r'&'), "&"); 274 | text = text.replaceAll(new RegExp(r'<'), "<"); 275 | text = text.replaceAll(new RegExp(r'>'), ">"); 276 | text = text.replaceAll(new RegExp(r"'"), "'"); 277 | text = text.replaceAll(new RegExp(r'"'), """); 278 | return text; 279 | } 280 | 281 | /* Function: xmlunescape 282 | * Unexcapes invalid xml characters. 283 | * 284 | * Parameters: 285 | * (String) text - text to unescape. 286 | * 287 | * Returns: 288 | * Unescaped text. 289 | */ 290 | static String xmlunescape(String text) { 291 | text = text.replaceAll(new RegExp(r'&'), "&"); 292 | text = text.replaceAll(new RegExp(r'<'), "<"); 293 | text = text.replaceAll(new RegExp(r'>'), ">"); 294 | text = text.replaceAll(new RegExp(r"'"), "'"); 295 | text = text.replaceAll(new RegExp(r'"'), '"'); 296 | return text; 297 | } 298 | 299 | /** Function: xmlTextNode 300 | * Creates an XML DOM text node. 301 | * 302 | * 303 | * Parameters: 304 | * (String) text - The content of the text node. 305 | * 306 | * Returns: 307 | * A new XML DOM text node. 308 | */ 309 | static xml.XmlNode xmlTextNode(String text) { 310 | xml.XmlBuilder builder = Strophe.xmlGenerator(); 311 | builder.element('strophe', nest: text); 312 | return builder.build(); 313 | } 314 | 315 | /** Function: xmlHtmlNode 316 | * Creates an XML DOM html node. 317 | * 318 | * Parameters: 319 | * (String) html - The content of the html node. 320 | * 321 | * Returns: 322 | * A new XML DOM text node. 323 | */ 324 | static xml.XmlNode xmlHtmlNode(String html) { 325 | xml.XmlNode parsed; 326 | try { 327 | parsed = xml.parse(html); 328 | } catch (e) { 329 | parsed = null; 330 | } 331 | return parsed; 332 | } 333 | 334 | /** Function: getText 335 | * Get the concatenation of all text children of an element. 336 | * 337 | * Parameters: 338 | * (XMLElement) elem - A DOM element. 339 | * 340 | * Returns: 341 | * A String with the concatenated text of all text element children. 342 | */ 343 | static String getText(xml.XmlNode elem) { 344 | if (elem == null) { 345 | return null; 346 | } 347 | String str = ""; 348 | if (elem.children.length == 0 && elem.nodeType == xml.XmlNodeType.TEXT) { 349 | str += elem.toString(); 350 | } 351 | 352 | for (int i = 0; i < elem.children.length; i++) { 353 | if (elem.children[i].nodeType == xml.XmlNodeType.TEXT) { 354 | str += elem.children[i].toString(); 355 | } 356 | } 357 | return Strophe.xmlescape(str); 358 | } 359 | 360 | /** Function: copyElement 361 | * Copy an XML DOM element. 362 | * 363 | * This function copies a DOM element and all its descendants and returns 364 | * the new copy. 365 | * 366 | * Parameters: 367 | * (XMLElement) elem - A DOM element. 368 | * 369 | * Returns: 370 | * A new, copied DOM element tree. 371 | */ 372 | static xml.XmlNode copyElement(xml.XmlNode elem) { 373 | var el = elem; 374 | if (elem.nodeType == xml.XmlNodeType.ELEMENT) { 375 | el = elem.copy(); 376 | } else if (elem.nodeType == xml.XmlNodeType.TEXT) { 377 | el = elem; 378 | } else if (elem.nodeType == xml.XmlNodeType.DOCUMENT) { 379 | el = elem.document.rootElement; 380 | } 381 | return el; 382 | } 383 | 384 | /** Function: createHtml 385 | * Copy an HTML DOM element into an XML DOM. 386 | * 387 | * This function copies a DOM element and all its descendants and returns 388 | * the new copy. 389 | * 390 | * Parameters: 391 | * (HTMLElement) elem - A DOM element. 392 | * 393 | * Returns: 394 | * A new, copied DOM element tree. 395 | */ 396 | static xml.XmlNode createHtml(xml.XmlNode elem) { 397 | xml.XmlNode el; 398 | String tag; 399 | if (elem.nodeType == xml.XmlNodeType.ELEMENT) { 400 | // XHTML tags must be lower case. 401 | //tag = elem. 402 | if (Strophe.XHTML['validTag'](tag)) { 403 | try { 404 | el = Strophe.copyElement(elem); 405 | } catch (e) { 406 | // invalid elements 407 | el = Strophe.xmlTextNode(''); 408 | } 409 | } else { 410 | el = Strophe.copyElement(elem); 411 | } 412 | } else if (elem.nodeType == xml.XmlNodeType.DOCUMENT_FRAGMENT) { 413 | el = Strophe.copyElement(elem); 414 | } else if (elem.nodeType == xml.XmlNodeType.TEXT) { 415 | el = Strophe.xmlTextNode(elem.toString()); 416 | } 417 | return el; 418 | } 419 | 420 | /** Function: escapeNode 421 | * Escape the node part (also called local part) of a JID. 422 | * 423 | * Parameters: 424 | * (String) node - A node (or local part). 425 | * 426 | * Returns: 427 | * An escaped node (or local part). 428 | */ 429 | static String escapeNode(String node) { 430 | return node 431 | .replaceAll(new RegExp(r"^\s+|\s+$"), '') 432 | .replaceAll(new RegExp(r"\\"), "\\5c") 433 | .replaceAll(new RegExp(r" "), "\\20") 434 | .replaceAll(new RegExp(r'"'), "\\22") 435 | .replaceAll(new RegExp(r'&'), "\\26") 436 | .replaceAll(new RegExp(r"'"), "\\27") 437 | .replaceAll(new RegExp(r'/'), "\\2f") 438 | .replaceAll(new RegExp(r':'), "\\3a") 439 | .replaceAll(new RegExp(r'<'), "\\3c") 440 | .replaceAll(new RegExp(r'>'), "\\3e") 441 | .replaceAll(new RegExp(r'@'), "\\40"); 442 | } 443 | 444 | /** Function: unescapeNode 445 | * Unescape a node part (also called local part) of a JID. 446 | * 447 | * Parameters: 448 | * (String) node - A node (or local part). 449 | * 450 | * Returns: 451 | * An unescaped node (or local part). 452 | */ 453 | static String unescapeNode(String node) { 454 | return node 455 | .replaceAll(new RegExp(r"\\5c"), "\\") 456 | .replaceAll(new RegExp(r"\\20"), " ") 457 | .replaceAll(new RegExp(r'\\22'), '"') 458 | .replaceAll(new RegExp(r'\\26'), "&") 459 | .replaceAll(new RegExp(r"\\27"), "'") 460 | .replaceAll(new RegExp(r'\\2f'), "/") 461 | .replaceAll(new RegExp(r'\\3a'), ":") 462 | .replaceAll(new RegExp(r'\\3c'), "<") 463 | .replaceAll(new RegExp(r'\\3e'), ">") 464 | .replaceAll(new RegExp(r'\\40'), "@"); 465 | } 466 | 467 | /** Function: getNodeFromJid 468 | * Get the node portion of a JID String. 469 | * 470 | * Parameters: 471 | * (String) jid - A JID. 472 | * 473 | * Returns: 474 | * A String containing the node. 475 | */ 476 | static String getNodeFromJid(String jid) { 477 | if (jid.indexOf("@") < 0) { 478 | return null; 479 | } 480 | return jid.split("@")[0]; 481 | } 482 | 483 | /** Function: getDomainFromJid 484 | * Get the domain portion of a JID String. 485 | * 486 | * Parameters: 487 | * (String) jid - A JID. 488 | * 489 | * Returns: 490 | * A String containing the domain. 491 | */ 492 | static String getDomainFromJid(String jid) { 493 | String bare = Strophe.getBareJidFromJid(jid); 494 | if (bare.indexOf("@") < 0) { 495 | return bare; 496 | } else { 497 | List parts = bare.split("@"); 498 | parts.removeAt(0); 499 | return parts.join('@'); 500 | } 501 | } 502 | 503 | /** Function: getResourceFromJid 504 | * Get the resource portion of a JID String. 505 | * 506 | * Parameters: 507 | * (String) jid - A JID. 508 | * 509 | * Returns: 510 | * A String containing the resource. 511 | */ 512 | static String getResourceFromJid(String jid) { 513 | List s = jid.split("/"); 514 | if (s.length < 2) { 515 | return null; 516 | } 517 | s.removeAt(0); 518 | return s.join('/'); 519 | } 520 | 521 | /** Function: getBareJidFromJid 522 | * Get the bare JID from a JID String. 523 | * 524 | * Parameters: 525 | * (String) jid - A JID. 526 | * 527 | * Returns: 528 | * A String containing the bare JID. 529 | */ 530 | static String getBareJidFromJid(String jid) { 531 | return jid != null && jid.isNotEmpty ? jid.split("/")[0] : null; 532 | } 533 | 534 | /** PrivateFunction: _handleError 535 | * _Private_ function that properly logs an error to the console 536 | */ 537 | static handleError(Error e) { 538 | if (e.stackTrace != null) { 539 | Strophe.fatal(e.stackTrace.toString()); 540 | } 541 | if (e.toString() != null) { 542 | Strophe.fatal("error: " + 543 | e.hashCode.toString() + 544 | " - " + 545 | e.runtimeType.toString() + 546 | ": " + 547 | e.toString()); 548 | } 549 | } 550 | 551 | /** Function: log 552 | * User overrideable logging function. 553 | * 554 | * This function is called whenever the Strophe library calls any 555 | * of the logging functions. The default implementation of this 556 | * function logs only fatal errors. If client code wishes to handle the logging 557 | * messages, it should override this with 558 | * > Strophe.log = function (level, msg) { 559 | * > (user code here) 560 | * > }; 561 | * 562 | * Please note that data sent and received over the wire is logged 563 | * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput(). 564 | * 565 | * The different levels and their meanings are 566 | * 567 | * DEBUG - Messages useful for debugging purposes. 568 | * INFO - Informational messages. This is mostly information like 569 | * 'disconnect was called' or 'SASL auth succeeded'. 570 | * WARN - Warnings about potential problems. This is mostly used 571 | * to report transient connection errors like request timeouts. 572 | * ERROR - Some error occurred. 573 | * FATAL - A non-recoverable fatal error occurred. 574 | * 575 | * Parameters: 576 | * (Integer) level - The log level of the log message. This will 577 | * be one of the values in Strophe.LogLevel. 578 | * (String) msg - The log message. 579 | */ 580 | static log(int level, String msg) { 581 | if (level != Strophe.LogLevel['FATAL']) { 582 | print(msg); 583 | } 584 | } 585 | 586 | /** Function: debug 587 | * Log a message at the Strophe.LogLevel.DEBUG level. 588 | * 589 | * Parameters: 590 | * (String) msg - The log message. 591 | */ 592 | static debug(String msg) { 593 | Strophe.log(Strophe.LogLevel['DEBUG'], msg); 594 | } 595 | 596 | /** Function: info 597 | * Log a message at the Strophe.LogLevel.INFO level. 598 | * 599 | * Parameters: 600 | * (String) msg - The log message. 601 | */ 602 | static info(String msg) { 603 | Strophe.log(Strophe.LogLevel['INFO'], msg); 604 | } 605 | 606 | /** Function: warn 607 | * Log a message at the Strophe.LogLevel.WARN level. 608 | * 609 | * Parameters: 610 | * (String) msg - The log message. 611 | */ 612 | static warn(String msg) { 613 | Strophe.log(Strophe.LogLevel['WARN'], msg); 614 | } 615 | 616 | /** Function: error 617 | * Log a message at the Strophe.LogLevel.ERROR level. 618 | * 619 | * Parameters: 620 | * (String) msg - The log message. 621 | */ 622 | static error(String msg) { 623 | Strophe.log(Strophe.LogLevel['ERROR'], msg); 624 | } 625 | 626 | /** Function: fatal 627 | * Log a message at the Strophe.LogLevel.FATAL level. 628 | * 629 | * Parameters: 630 | * (String) msg - The log message. 631 | */ 632 | static fatal(String msg) { 633 | Strophe.log(Strophe.LogLevel['FATAL'], msg); 634 | } 635 | 636 | /** Function: serialize 637 | * Render a DOM element and all descendants to a String. 638 | * 639 | * Parameters: 640 | * (XMLElement) elem - A DOM element. 641 | * 642 | * Returns: 643 | * The serialized element tree as a String. 644 | */ 645 | static String serialize(xml.XmlNode elem) { 646 | if (elem == null) { 647 | return null; 648 | } 649 | 650 | return elem.toXmlString(); 651 | } 652 | 653 | /** PrivateVariable: _requestId 654 | * _Private_ variable that keeps track of the request ids for 655 | * connections. 656 | */ 657 | static int requestId = 0; 658 | 659 | /** PrivateVariable: Strophe.connectionPlugins 660 | * _Private_ variable Used to store plugin names that need 661 | * initialization on Strophe.Connection construction. 662 | */ 663 | static Map _connectionPlugins = {}; 664 | static Map get connectionPlugins { 665 | return _connectionPlugins; 666 | } 667 | 668 | /** Function: addConnectionPlugin 669 | * Extends the Strophe.Connection object with the given plugin. 670 | * 671 | * Parameters: 672 | * (String) name - The name of the extension. 673 | * (Object) ptype - The plugin's prototype. 674 | */ 675 | static void addConnectionPlugin(String name, ptype) { 676 | Strophe._connectionPlugins[name] = ptype; 677 | } 678 | /** Class: Strophe.Builder 679 | * XML DOM builder. 680 | * 681 | * This object provides an interface similar to JQuery but for building 682 | * DOM elements easily and rapidly. All the functions except for toString() 683 | * and tree() return the object, so calls can be chained. Here's an 684 | * example using the $iq() builder helper. 685 | * > $iq({to: 'you', from: 'me', type: 'get', id: '1'}) 686 | * > .c('query', {xmlns: 'strophe:example'}) 687 | * > .c('example') 688 | * > .toString() 689 | * 690 | * The above generates this XML fragment 691 | * > 692 | * > 693 | * > 694 | * > 695 | * > 696 | * The corresponding DOM manipulations to get a similar fragment would be 697 | * a lot more tedious and probably involve several helper variables. 698 | * 699 | * Since adding children makes new operations operate on the child, up() 700 | * is provided to traverse up the tree. To add two children, do 701 | * > builder.c('child1', ...).up().c('child2', ...) 702 | * The next operation on the Builder will be relative to the second child. 703 | */ 704 | 705 | /** Constructor: Strophe.Builder 706 | * Create a Strophe.Builder object. 707 | * 708 | * The attributes should be passed in object notation. For example 709 | * > var b = new Builder('message', {to: 'you', from: 'me'}); 710 | * or 711 | * > var b = new Builder('messsage', {'xml:lang': 'en'}); 712 | * 713 | * Parameters: 714 | * (String) name - The name of the root element. 715 | * (Object) attrs - The attributes for the root element in object notation. 716 | * 717 | * Returns: 718 | * A new Strophe.Builder. 719 | */ 720 | static StanzaBuilder Builder(String name, {Map attrs}) { 721 | return new StanzaBuilder(name, attrs); 722 | } 723 | /** PrivateClass: Strophe.Handler 724 | * _Private_ helper class for managing stanza handlers. 725 | * 726 | * A Strophe.Handler encapsulates a user provided callback function to be 727 | * executed when matching stanzas are received by the connection. 728 | * Handlers can be either one-off or persistant depending on their 729 | * return value. Returning true will cause a Handler to remain active, and 730 | * returning false will remove the Handler. 731 | * 732 | * Users will not use Strophe.Handler objects directly, but instead they 733 | * will use Strophe.Connection.addHandler() and 734 | * Strophe.Connection.deleteHandler(). 735 | */ 736 | 737 | /** PrivateConstructor: Strophe.Handler 738 | * Create and initialize a new Strophe.Handler. 739 | * 740 | * Parameters: 741 | * (Function) handler - A function to be executed when the handler is run. 742 | * (String) ns - The namespace to match. 743 | * (String) name - The element name to match. 744 | * (String) type - The element type to match. 745 | * (String) id - The element id attribute to match. 746 | * (String) from - The element from attribute to match. 747 | * (Object) options - Handler options 748 | * 749 | * Returns: 750 | * A new Strophe.Handler object. 751 | */ 752 | static StanzaHandler Handler(Function handler, String ns, String name, 753 | [type, String id, String from, Map options]) { 754 | if (options != null) { 755 | options.putIfAbsent('matchBareFromJid', () => false); 756 | options.putIfAbsent('ignoreNamespaceFragment', () => false); 757 | } else 758 | options = {'matchBareFromJid': false, 'ignoreNamespaceFragment': false}; 759 | StanzaHandler stanzaHandler = 760 | new StanzaHandler(handler, ns, name, type, id, options); 761 | // BBB: Maintain backward compatibility with old `matchBare` option 762 | if (stanzaHandler.options['matchBare'] != null) { 763 | Strophe.warn( 764 | 'The "matchBare" option is deprecated, use "matchBareFromJid" instead.'); 765 | stanzaHandler.options['matchBareFromJid'] = 766 | stanzaHandler.options['matchBare']; 767 | stanzaHandler.options.remove('matchBare'); 768 | } 769 | 770 | if (stanzaHandler.options['matchBareFromJid'] != null && 771 | stanzaHandler.options['matchBareFromJid'] == true) { 772 | stanzaHandler.from = (from != null && from.isNotEmpty) 773 | ? Strophe.getBareJidFromJid(from) 774 | : null; 775 | } else { 776 | stanzaHandler.from = from; 777 | } 778 | // whether the handler is a user handler or a system handler 779 | stanzaHandler.user = true; 780 | return stanzaHandler; 781 | } 782 | /** PrivateClass: Strophe.TimedHandler 783 | * _Private_ helper class for managing timed handlers. 784 | * 785 | * A Strophe.TimedHandler encapsulates a user provided callback that 786 | * should be called after a certain period of time or at regular 787 | * intervals. The return value of the callback determines whether the 788 | * Strophe.TimedHandler will continue to fire. 789 | * 790 | * Users will not use Strophe.TimedHandler objects directly, but instead 791 | * they will use Strophe.Connection.addTimedHandler() and 792 | * Strophe.Connection.deleteTimedHandler(). 793 | */ 794 | 795 | /** PrivateConstructor: Strophe.TimedHandler 796 | * Create and initialize a new Strophe.TimedHandler object. 797 | * 798 | * Parameters: 799 | * (Integer) period - The number of milliseconds to wait before the 800 | * handler is called. 801 | * (Function) handler - The callback to run when the handler fires. This 802 | * function should take no arguments. 803 | * 804 | * Returns: 805 | * A new Strophe.TimedHandler object. 806 | */ 807 | static StanzaTimedHandler TimedHandler(int period, Function handler) { 808 | StanzaTimedHandler stanzaTimedHandler = 809 | new StanzaTimedHandler(period, handler); 810 | return stanzaTimedHandler; 811 | } 812 | 813 | static StanzaBuilder $build(String name, Map attrs) { 814 | return Strophe.Builder(name, attrs: attrs); 815 | } 816 | 817 | static StanzaBuilder $msg([Map attrs]) { 818 | return Strophe.Builder("message", attrs: attrs); 819 | } 820 | 821 | static StanzaBuilder $iq([Map attrs]) { 822 | return Strophe.Builder("iq", attrs: attrs); 823 | } 824 | 825 | static StanzaBuilder $pres([Map attrs]) { 826 | return Strophe.Builder("presence", attrs: attrs); 827 | } 828 | /** Class: Strophe.Connection 829 | * XMPP Connection manager. 830 | * 831 | * This class is the main part of Strophe. It manages a BOSH or websocket 832 | * connection to an XMPP server and dispatches events to the user callbacks 833 | * as data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, SASL SCRAM-SHA1 834 | * and legacy authentication. 835 | * 836 | * After creating a Strophe.Connection object, the user will typically 837 | * call connect() with a user supplied callback to handle connection level 838 | * events like authentication failure, disconnection, or connection 839 | * complete. 840 | * 841 | * The user will also have several event handlers defined by using 842 | * addHandler() and addTimedHandler(). These will allow the user code to 843 | * respond to interesting stanzas or do something periodically with the 844 | * connection. These handlers will be active once authentication is 845 | * finished. 846 | * 847 | * To send data to the connection, use send(). 848 | */ 849 | 850 | /** Constructor: Strophe.Connection 851 | * Create and initialize a Strophe.Connection object. 852 | * 853 | * The transport-protocol for this connection will be chosen automatically 854 | * based on the given service parameter. URLs starting with "ws://" or 855 | * "wss://" will use WebSockets, URLs starting with "http://", "https://" 856 | * or without a protocol will use BOSH. 857 | * 858 | * To make Strophe connect to the current host you can leave out the protocol 859 | * and host part and just pass the path, e.g. 860 | * 861 | * > var conn = new Strophe.Connection("/http-bind/"); 862 | * 863 | * Options common to both Websocket and BOSH: 864 | * ------------------------------------------ 865 | * 866 | * cookies: 867 | * 868 | * The *cookies* option allows you to pass in cookies to be added to the 869 | * document. These cookies will then be included in the BOSH XMLHttpRequest 870 | * or in the websocket connection. 871 | * 872 | * The passed in value must be a map of cookie names and string values. 873 | * 874 | * > { "myCookie": { 875 | * > "value": "1234", 876 | * > "domain": ".example.org", 877 | * > "path": "/", 878 | * > "expires": expirationDate 879 | * > } 880 | * > } 881 | * 882 | * Note that cookies can't be set in this way for other domains (i.e. cross-domain). 883 | * Those cookies need to be set under those domains, for example they can be 884 | * set server-side by making a XHR call to that domain to ask it to set any 885 | * necessary cookies. 886 | * 887 | * mechanisms: 888 | * 889 | * The *mechanisms* option allows you to specify the SASL mechanisms that this 890 | * instance of Strophe.Connection (and therefore your XMPP client) will 891 | * support. 892 | * 893 | * The value must be an array of objects with Strophe.SASLMechanism 894 | * prototypes. 895 | * 896 | * If nothing is specified, then the following mechanisms (and their 897 | * priorities) are registered: 898 | * 899 | * SCRAM-SHA1 - 70 900 | * DIGEST-MD5 - 60 901 | * PLAIN - 50 902 | * OAUTH-BEARER - 40 903 | * OAUTH-2 - 30 904 | * ANONYMOUS - 20 905 | * EXTERNAL - 10 906 | * 907 | * WebSocket options: 908 | * ------------------ 909 | * 910 | * If you want to connect to the current host with a WebSocket connection you 911 | * can tell Strophe to use WebSockets through a "protocol" attribute in the 912 | * optional options parameter. Valid values are "ws" for WebSocket and "wss" 913 | * for Secure WebSocket. 914 | * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call 915 | * 916 | * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); 917 | * 918 | * Note that relative URLs _NOT_ starting with a "/" will also include the path 919 | * of the current site. 920 | * 921 | * Also because downgrading security is not permitted by browsers, when using 922 | * relative URLs both BOSH and WebSocket connections will use their secure 923 | * variants if the current connection to the site is also secure (https). 924 | * 925 | * BOSH options: 926 | * ------------- 927 | * 928 | * By adding "sync" to the options, you can control if requests will 929 | * be made synchronously or not. The default behaviour is asynchronous. 930 | * If you want to make requests synchronous, make "sync" evaluate to true. 931 | * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); 932 | * 933 | * You can also toggle this on an already established connection. 934 | * > conn.options.sync = true; 935 | * 936 | * The *customHeaders* option can be used to provide custom HTTP headers to be 937 | * included in the XMLHttpRequests made. 938 | * 939 | * The *keepalive* option can be used to instruct Strophe to maintain the 940 | * current BOSH session across interruptions such as webpage reloads. 941 | * 942 | * It will do this by caching the sessions tokens in sessionStorage, and when 943 | * "restore" is called it will check whether there are cached tokens with 944 | * which it can resume an existing session. 945 | * 946 | * The *withCredentials* option should receive a Boolean value and is used to 947 | * indicate wether cookies should be included in ajax requests (by default 948 | * they're not). 949 | * Set this value to true if you are connecting to a BOSH service 950 | * and for some reason need to send cookies to it. 951 | * In order for this to work cross-domain, the server must also enable 952 | * credentials by setting the Access-Control-Allow-Credentials response header 953 | * to "true". For most usecases however this setting should be false (which 954 | * is the default). 955 | * Additionally, when using Access-Control-Allow-Credentials, the 956 | * Access-Control-Allow-Origin header can't be set to the wildcard "*", but 957 | * instead must be restricted to actual domains. 958 | * 959 | * The *contentType* option can be set to change the default Content-Type 960 | * of "text/xml; charset=utf-8", which can be useful to reduce the amount of 961 | * CORS preflight requests that are sent to the server. 962 | * 963 | * Parameters: 964 | * (String) service - The BOSH or WebSocket service URL. 965 | * (Object) options - A hash of configuration options 966 | * 967 | * Returns: 968 | * A new Strophe.Connection object. 969 | */ 970 | static StropheConnection Connection(String service, [Map options]) { 971 | StropheConnection stropheConnection = 972 | new StropheConnection(service, options); 973 | return stropheConnection; 974 | } 975 | 976 | static StropheWebSocket Websocket(StropheConnection connection) { 977 | return new StropheWebSocket(connection); 978 | } 979 | 980 | static StropheBosh Bosh(StropheConnection connection) { 981 | return new StropheBosh(connection); 982 | } 983 | 984 | static StropheSASLAnonymous SASLAnonymous = new StropheSASLAnonymous(); 985 | static StropheSASLPlain SASLPlain = new StropheSASLPlain(); 986 | static StropheSASLMD5 SASLMD5 = new StropheSASLMD5(); 987 | static StropheSASLSHA1 SASLSHA1 = new StropheSASLSHA1(); 988 | static StropheSASLOAuthBearer SASLOAuthBearer = new StropheSASLOAuthBearer(); 989 | static StropheSASLExternal SASLExternal = new StropheSASLExternal(); 990 | static StropheSASLXOAuth2 SASLXOAuth2 = new StropheSASLXOAuth2(); 991 | } 992 | -------------------------------------------------------------------------------- /lib/src/md5.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:crypto/crypto.dart'; 3 | 4 | class MD5 { 5 | /* 6 | * Convert an array of little-endian words to a string 7 | */ 8 | static String binl2str(List bytes) { 9 | return String.fromCharCodes(bytes); 10 | } 11 | 12 | static String binl2hex(List binarray) { 13 | return md5.convert(binarray).toString(); 14 | } 15 | 16 | static List coreMd5(String s, int len) { 17 | var bytes = utf8.encode(s); // data being hashed 18 | Digest digest = md5.convert(bytes); 19 | return digest.bytes; 20 | } 21 | 22 | static String hexdigest(String s) { 23 | return binl2hex(coreMd5(s, s.length * 8)); 24 | } 25 | 26 | static String hash(String s) { 27 | return binl2str(coreMd5(s, s.length * 8)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/plugins/administration.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | 5 | class AdministrationPlugin extends PluginClass { 6 | @override 7 | init(StropheConnection conn) { 8 | this.connection = conn; 9 | if (Strophe.NS['COMMANDS'] == null) 10 | Strophe.addNamespace('COMMANDS', 'http://jabber.org/protocol/commands'); 11 | Strophe.addNamespace('REGISTERED_USERS_NUM', 12 | 'http://jabber.org/protocol/admin#get-registered-users-num'); 13 | Strophe.addNamespace('ONLINE_USERS_NUM', 14 | 'http://jabber.org/protocol/admin#get-online-users-num'); 15 | } 16 | 17 | getRegisteredUsersNum(Function success, [Function error]) { 18 | String id = this.connection.getUniqueId('get-registered-users-num'); 19 | this.connection.sendIQ( 20 | Strophe.$iq({ 21 | 'type': 'set', 22 | 'id': id, 23 | 'xml:lang': 'en', 24 | 'to': this.connection.domain 25 | }).c('command', { 26 | 'xmlns': Strophe.NS['COMMANDS'], 27 | 'action': 'execute', 28 | 'node': Strophe.NS['REGISTERED_USERS_NUM'] 29 | }).tree(), 30 | success, 31 | error); 32 | return id; 33 | } 34 | 35 | getOnlineUsersNum(Function success, [Function error]) { 36 | String id = this.connection.getUniqueId('get-registered-users-num'); 37 | this.connection.sendIQ( 38 | Strophe.$iq({ 39 | 'type': 'set', 40 | 'id': id, 41 | 'xml:lang': 'en', 42 | 'to': this.connection.domain 43 | }).c('command', { 44 | 'xmlns': Strophe.NS['COMMANDS'], 45 | 'action': 'execute', 46 | 'node': Strophe.NS['ONLINE_USERS_NUM'] 47 | }).tree(), 48 | success, 49 | error); 50 | return id; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/plugins/bookmark.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | 6 | class BookMarkPlugin extends PluginClass { 7 | init(StropheConnection connection) { 8 | this.connection = connection; 9 | Strophe.addNamespace('PRIVATE', 'jabber:iq:private'); 10 | Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks'); 11 | Strophe.addNamespace('PRIVACY', 'jabber:iq:privacy'); 12 | Strophe.addNamespace('DELAY', 'jabber:x:delay'); 13 | Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); 14 | } 15 | 16 | /// 17 | /// Create private bookmark node. 18 | /// 19 | /// @param {function} [success] - Callback after success 20 | /// @param {function} [error] - Callback after error 21 | /// 22 | bool createBookmarksNode([Function success, Function error]) { 23 | // We do this instead of using publish-options because this is not 24 | // mandatory to implement according to XEP-0060 25 | this.connection.sendIQ( 26 | Strophe.$iq({'type': 'set'}) 27 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}) 28 | .c('create', {'node': Strophe.NS['BOOKMARKS']}) 29 | .up() 30 | .c('configure') 31 | .c('x', {'xmlns': 'jabber:x:data', 'type': 'submit'}) 32 | .c('field', {'var': 'FORM_TYPE', 'type': 'hidden'}) 33 | .c('value') 34 | .t('http://jabber.org/protocol/pubsub#node_config') 35 | .up() 36 | .up() 37 | .c('field', {'var': 'pubsub#persist_items'}) 38 | .c('value') 39 | .t('1') 40 | .up() 41 | .up() 42 | .c('field', {'var': 'pubsub#access_model'}) 43 | .c('value') 44 | .t('whitelist') 45 | .tree(), 46 | success, 47 | error); 48 | 49 | return true; 50 | } 51 | 52 | /** 53 | * Add bookmark to storage or update it. 54 | * 55 | * The specified room is bookmarked into the remote bookmark storage. If the room is 56 | * already bookmarked, then it is updated with the specified arguments. 57 | * 58 | * @param {string} roomJid - The JabberID of the chat roomJid 59 | * @param {string} [alias] - A friendly name for the bookmark 60 | * @param {string} [nick] - The users's preferred roomnick for the chatroom 61 | * @param {boolean} [autojoin=false] - Whether the client should automatically join 62 | * the conference room on login. 63 | * @param {function} [success] - Callback after success 64 | * @param {function} [error] - Callback after error 65 | */ 66 | add(String roomJid, String alias, 67 | [String nick, bool autojoin = true, Function success, Function error]) { 68 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'}) 69 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', { 70 | 'node': Strophe.NS['BOOKMARKS'] 71 | }).c('item', {'id': 'current'}).c( 72 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']}); 73 | 74 | Function _bookmarkGroupChat = (bool bookmarkit) { 75 | if (bookmarkit) { 76 | Map conferenceAttr = { 77 | 'jid': roomJid, 78 | 'autojoin': autojoin || false 79 | }; 80 | 81 | if (alias != null && alias.isNotEmpty) { 82 | conferenceAttr['name'] = alias; 83 | } 84 | 85 | stanza.c('conference', conferenceAttr); 86 | if (nick != null && nick.isNotEmpty) { 87 | stanza.c('nick').t(nick); 88 | } 89 | } 90 | 91 | this.connection.sendIQ(stanza.tree(), success, error); 92 | }; 93 | 94 | this.get((xml.XmlElement s) { 95 | List confs = s.findAllElements('conference').toList(); 96 | bool bookmarked = false; 97 | for (int i = 0; i < confs.length; i++) { 98 | Map conferenceAttr = { 99 | 'jid': confs[i].getAttribute('jid'), 100 | 'autojoin': confs[i].getAttribute('autojoin') ?? false 101 | }; 102 | String roomName = confs[i].getAttribute('name'); 103 | List nickname = 104 | confs[i].findAllElements('nick').toList(); 105 | 106 | if (conferenceAttr['jid'] == roomJid) { 107 | // the room is already bookmarked, then update it 108 | bookmarked = true; 109 | 110 | conferenceAttr['autojoin'] = autojoin || false; 111 | 112 | if (alias != null && alias.isNotEmpty) { 113 | conferenceAttr['name'] = alias; 114 | } 115 | stanza.c('conference', conferenceAttr); 116 | 117 | if (nick != null && nick.isNotEmpty) { 118 | stanza.c('nick').t(nick).up(); 119 | } 120 | } else { 121 | if (roomName != null && roomName.isNotEmpty) { 122 | conferenceAttr['name'] = roomName; 123 | } 124 | stanza.c('conference', conferenceAttr); 125 | 126 | if (nickname.length == 1) { 127 | stanza.c('nick').t(nickname[0].text).up(); 128 | } 129 | } 130 | 131 | stanza.up(); 132 | } 133 | 134 | _bookmarkGroupChat(!bookmarked); 135 | }, (xml.XmlElement s) { 136 | if (s.findAllElements('item-not-found').length > 0) { 137 | _bookmarkGroupChat(true); 138 | } else { 139 | error(s); 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * Retrieve all stored bookmarks. 146 | * 147 | * @param {function} [success] - Callback after success 148 | * @param {function} [error] - Callback after error 149 | */ 150 | get([Function success, Function error]) { 151 | this.connection.sendIQ( 152 | Strophe.$iq({'type': 'get'}) 153 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 154 | 'items', {'node': Strophe.NS['BOOKMARKS']}).tree(), 155 | success, 156 | error); 157 | } 158 | 159 | /** 160 | * Delete the bookmark with the given roomJid in the bookmark storage. 161 | * 162 | * The whole remote bookmark storage is just updated by removing the 163 | * bookmark corresponding to the specified room. 164 | * 165 | * @param {string} roomJid - The JabberID of the chat roomJid you want to remove 166 | * @param {function} [success] - Callback after success 167 | * @param {function} [error] - Callback after error 168 | */ 169 | delete(String roomJid, [Function success, Function error]) { 170 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'}) 171 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', { 172 | 'node': Strophe.NS['BOOKMARKS'] 173 | }).c('item', {'id': 'current'}).c( 174 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']}); 175 | 176 | this.get((xml.XmlElement s) { 177 | List confs = s.findAllElements('conference').toList(); 178 | for (int i = 0; i < confs.length; i++) { 179 | Map conferenceAttr = { 180 | 'jid': confs[i].getAttribute('jid'), 181 | 'autojoin': confs[i].getAttribute('autojoin') 182 | }; 183 | if (conferenceAttr['jid'] == roomJid) { 184 | continue; 185 | } 186 | String roomName = confs[i].getAttribute('name'); 187 | if (roomName != null && roomName.isNotEmpty) { 188 | conferenceAttr['name'] = roomName; 189 | } 190 | stanza.c('conference', conferenceAttr); 191 | List nickname = 192 | confs[i].findAllElements('nick').toList(); 193 | if (nickname.length == 1) { 194 | stanza.c('nick').t(nickname[0].text).up(); 195 | } 196 | stanza.up(); 197 | } 198 | this.connection.sendIQ(stanza.tree(), success, error); 199 | }, (s) { 200 | error(s); 201 | }); 202 | } 203 | 204 | /** 205 | * Update the bookmark with the given roomJid in the bookmark storage. 206 | * 207 | * The whole remote bookmark storage is just updated by updating the 208 | * bookmark corresponding to the specified room. 209 | * 210 | * @param {string} roomJid - The JabberID of the chat roomJid you want to remove 211 | * @param {function} [success] - Callback after success 212 | * @param {function} [error] - Callback after error 213 | */ 214 | update(String roomJid, String alias, 215 | [String nick, bool autojoin = true, Function success, Function error]) { 216 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'}) 217 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', { 218 | 'node': Strophe.NS['BOOKMARKS'] 219 | }).c('item', {'id': 'current'}).c( 220 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']}); 221 | 222 | this.get((xml.XmlElement s) { 223 | List confs = s.findAllElements('conference').toList(); 224 | for (int i = 0; i < confs.length; i++) { 225 | Map conferenceAttr = { 226 | 'jid': confs[i].getAttribute('jid'), 227 | 'autojoin': confs[i].getAttribute('autojoin'), 228 | 'name': confs[i].getAttribute('name') 229 | }; 230 | if (conferenceAttr['jid'] == roomJid) { 231 | conferenceAttr['autojoin'] = autojoin ?? conferenceAttr['autojoin']; 232 | String roomName = confs[i].getAttribute('name'); 233 | if (alias != null && alias.isNotEmpty) roomName = alias; 234 | conferenceAttr['name'] = roomName ?? ''; 235 | } 236 | stanza.c('conference', conferenceAttr); 237 | List nickname = 238 | confs[i].findAllElements('nick').toList(); 239 | if (nick != null && 240 | nick.isNotEmpty && 241 | conferenceAttr['jid'] == roomJid) { 242 | stanza.c('nick').t(nick).up(); 243 | } else if (nickname.length == 1) { 244 | stanza.c('nick').t(nickname[0].text).up(); 245 | } 246 | stanza.up(); 247 | } 248 | this.connection.sendIQ(stanza.tree(), success, error); 249 | }, (s) { 250 | error(s); 251 | }); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /lib/src/plugins/caps.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:strophe/src/sha1.dart'; 5 | 6 | class CapsPlugin extends PluginClass { 7 | String _hash = 'sha-1'; 8 | String _node = 'http://strophe.im/strophejs/'; 9 | init(StropheConnection c) { 10 | this.connection = c; 11 | Strophe.addNamespace('CAPS', "http://jabber.org/protocol/caps"); 12 | if (this.connection.disco == null) { 13 | throw {'error': "disco plugin required!"}; 14 | } 15 | this.connection.disco.addFeature(Strophe.NS['CAPS']); 16 | this.connection.disco.addFeature(Strophe.NS['DISCO_INFO']); 17 | if (this.connection.disco.identities.length == 0) { 18 | return this.connection.disco.addIdentity("client", "pc", "strophejs", ""); 19 | } 20 | } 21 | 22 | addFeature(String feature) { 23 | return this.connection.disco.addFeature(feature); 24 | } 25 | 26 | removeFeature(String feature) { 27 | return this.connection.disco.removeFeature(feature); 28 | } 29 | 30 | sendPres() { 31 | StanzaBuilder caps = createCapsNode(); 32 | return this.connection.send(Strophe.$pres().cnode(caps.tree())); 33 | } 34 | 35 | StanzaBuilder createCapsNode() { 36 | String node; 37 | if (this.connection.disco.identities.length > 0) { 38 | node = this.connection.disco.identities[0]['name'] ?? ""; 39 | } else { 40 | node = this._node; 41 | } 42 | return Strophe.$build("c", { 43 | 'xmlns': Strophe.NS['CAPS'], 44 | 'hash': this._hash, 45 | 'node': node, 46 | 'ver': generateVerificationString() 47 | }); 48 | } 49 | 50 | propertySort(List> array, String property) { 51 | return array.sort((a, b) { 52 | return a[property].compareTo(b[property]); 53 | }); 54 | } 55 | 56 | String generateVerificationString() { 57 | String ns; 58 | List _ref1; 59 | List> ids = []; 60 | List> _ref = this.connection.disco.identities; 61 | for (int _i = 0, _len = _ref.length; _i < _len; _i++) { 62 | ids.add(_ref[_i]); 63 | } 64 | List features = []; 65 | _ref1 = this.connection.disco.features; 66 | for (int _j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 67 | features.add(_ref1[_j]); 68 | } 69 | String S = ""; 70 | propertySort(ids, "category"); 71 | propertySort(ids, "type"); 72 | propertySort(ids, "lang"); 73 | ids.forEach((Map id) { 74 | S += "" + 75 | id['category'] + 76 | "/" + 77 | id['type'] + 78 | "/" + 79 | id['lang'] + 80 | "/" + 81 | id['name'] + 82 | "<"; 83 | }); 84 | features.sort(); 85 | for (int _k = 0, _len2 = features.length; _k < _len2; _k++) { 86 | ns = features[_k]; 87 | S += "" + ns + "<"; 88 | } 89 | return "" + (SHA1.b64Sha1(S)) + "="; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/plugins/chat-notifications.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | 6 | /// 7 | /// Chat state notifications (XEP 0085) plugin 8 | /// @see http://xmpp.org/extensions/xep-0085.html 9 | /// 10 | class ChatStatesNotificationPlugin extends PluginClass { 11 | @override 12 | init(StropheConnection conn) { 13 | this.connection = conn; 14 | statusChanged = (int status, [condition, el]) { 15 | if (status == Strophe.Status['CONNECTED'] || 16 | status == Strophe.Status['ATTACHED']) { 17 | this.connection.addHandler( 18 | this._notificationReceived, Strophe.NS['CHATSTATES'], "message"); 19 | } 20 | }; 21 | Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); 22 | } 23 | 24 | addActive(StanzaBuilder message) { 25 | return message.c('active', {'xmlns': Strophe.NS['CHATSTATES']}).up(); 26 | } 27 | 28 | _notificationReceived(element) { 29 | xml.XmlElement message; 30 | if (element is String) { 31 | message = xml.parse(element).rootElement; 32 | } else if (element is xml.XmlElement) 33 | message = element; 34 | else if (element is xml.XmlDocument) message = element.rootElement; 35 | 36 | if (message != null && message.findAllElements('error').length > 0) 37 | return true; 38 | 39 | /* List composing = 40 | message.findAllElements('composing').toList(), 41 | paused = message.findAllElements('paused').toList(), 42 | active = message.findAllElements('active').toList(), 43 | inactive = message.findAllElements('inactive').toList(), 44 | gone = message.findAllElements('gone').toList(); 45 | 46 | String jid = message.getAttribute('from'); */ 47 | 48 | /* if (composing.length > 0) { 49 | $(document).trigger('composing.chatstates', jid); 50 | } 51 | 52 | if (paused.length > 0) { 53 | $(document).trigger('paused.chatstates', jid); 54 | } 55 | 56 | if (active.length > 0) { 57 | $(document).trigger('active.chatstates', jid); 58 | } 59 | 60 | if (inactive.length > 0) { 61 | $(document).trigger('inactive.chatstates', jid); 62 | } 63 | 64 | if (gone.length > 0) { 65 | $(document).trigger('gone.chatstates', jid); 66 | } */ 67 | 68 | return true; 69 | } 70 | 71 | sendActive(String jid, String type) { 72 | this._sendNotification(jid, type, 'active'); 73 | } 74 | 75 | sendComposing(String jid, String type) { 76 | this._sendNotification(jid, type, 'composing'); 77 | } 78 | 79 | sendPaused(String jid, String type) { 80 | this._sendNotification(jid, type, 'paused'); 81 | } 82 | 83 | sendInactive(String jid, String type) { 84 | this._sendNotification(jid, type, 'inactive'); 85 | } 86 | 87 | sendGone(String jid, String type) { 88 | this._sendNotification(jid, type, 'gone'); 89 | } 90 | 91 | _sendNotification(String jid, String type, String notification) { 92 | if (type == null || type.isEmpty) type = 'chat'; 93 | 94 | this.connection.send(Strophe.$msg({'to': jid, 'type': type}) 95 | .c(notification, {'xmlns': Strophe.NS['CHATSTATES']})); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/plugins/disco.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml/nodes/element.dart'; 5 | 6 | class DiscoPlugin extends PluginClass { 7 | List> _identities = []; 8 | List _features = []; 9 | List> _items = []; 10 | 11 | /** Function: init 12 | * Plugin init 13 | * 14 | * Parameters: 15 | * (Strophe.Connection) conn - Strophe connection 16 | */ 17 | List> get identities { 18 | return _identities; 19 | } 20 | 21 | List get features { 22 | return _features; 23 | } 24 | 25 | init(StropheConnection conn) { 26 | this.connection = conn; 27 | this._identities = []; 28 | this._features = []; 29 | this._items = []; 30 | // disco info 31 | conn.addHandler( 32 | this._onDiscoInfo, Strophe.NS['DISCO_INFO'], 'iq', 'get', null, null); 33 | // disco items 34 | conn.addHandler( 35 | this._onDiscoItems, Strophe.NS['DISCO_ITEMS'], 'iq', 'get', null, null); 36 | } 37 | 38 | /** Function: addIdentity 39 | * See http://xmpp.org/registrar/disco-categories.html 40 | * Parameters: 41 | * (String) category - category of identity (like client, automation, etc ...) 42 | * (String) type - type of identity (like pc, web, bot , etc ...) 43 | * (String) name - name of identity in natural language 44 | * (String) lang - lang of name parameter 45 | * 46 | * Returns: 47 | * Boolean 48 | */ 49 | bool addIdentity(String category, String type, 50 | [String name = '', String lang = '']) { 51 | for (int i = 0; i < this._identities.length; i++) { 52 | if (this._identities[i]['category'] == category && 53 | this._identities[i]['type'] == type && 54 | this._identities[i]['name'] == name && 55 | this._identities[i]['lang'] == lang) { 56 | return false; 57 | } 58 | } 59 | this 60 | ._identities 61 | .add({'category': category, 'type': type, 'name': name, 'lang': lang}); 62 | return true; 63 | } 64 | 65 | /** Function: addFeature 66 | * 67 | * Parameters: 68 | * (String) var_name - feature name (like jabber:iq:version) 69 | * 70 | * Returns: 71 | * boolean 72 | */ 73 | bool addFeature(String varName) { 74 | for (int i = 0; i < this._features.length; i++) { 75 | if (this._features[i] == varName) return false; 76 | } 77 | this._features.add(varName); 78 | return true; 79 | } 80 | 81 | /** Function: removeFeature 82 | * 83 | * Parameters: 84 | * (String) var_name - feature name (like jabber:iq:version) 85 | * 86 | * Returns: 87 | * boolean 88 | */ 89 | bool removeFeature(String varName) { 90 | for (int i = 0; i < this._features.length; i++) { 91 | if (this._features[i] == varName) { 92 | this._features.removeAt(i); 93 | return true; 94 | } 95 | } 96 | return false; 97 | } 98 | 99 | /** Function: addItem 100 | * 101 | * Parameters: 102 | * (String) jid 103 | * (String) name 104 | * (String) node 105 | * (Function) call_back 106 | * 107 | * Returns: 108 | * boolean 109 | */ 110 | addItem(String jid, String name, String node, [Function callback]) { 111 | if (node != null && node.isNotEmpty && callback == null) return false; 112 | this 113 | ._items 114 | .add({'jid': jid, 'name': name, 'node': node, 'call_back': callback}); 115 | return true; 116 | } 117 | 118 | /** Function: info 119 | * Info query 120 | * 121 | * Parameters: 122 | * (Function) call_back 123 | * (String) jid 124 | * (String) node 125 | */ 126 | info(String jid, 127 | [String node, Function success, Function error, int timeout]) { 128 | Map attrs = {'xmlns': Strophe.NS['DISCO_INFO']}; 129 | if (node != null && node.isNotEmpty) attrs['node'] = node; 130 | 131 | StanzaBuilder info = Strophe 132 | .$iq({'from': this.connection.jid, 'to': jid, 'type': 'get'}).c( 133 | 'query', attrs); 134 | this.connection.sendIQ(info.tree(), success, error, timeout); 135 | } 136 | 137 | /** Function: items 138 | * Items query 139 | * 140 | * Parameters: 141 | * (Function) call_back 142 | * (String) jid 143 | * (String) node 144 | */ 145 | items(String jid, 146 | [String node, Function success, Function error, int timeout]) { 147 | Map attrs = {'xmlns': Strophe.NS['DISCO_ITEMS']}; 148 | if (node != null && node.isNotEmpty) attrs['node'] = node; 149 | 150 | StanzaBuilder items = Strophe 151 | .$iq({'from': this.connection.jid, 'to': jid, 'type': 'get'}).c( 152 | 'query', attrs); 153 | this.connection.sendIQ(items.tree(), success, error, timeout); 154 | } 155 | 156 | /** PrivateFunction: _buildIQResult 157 | */ 158 | StanzaBuilder _buildIQResult( 159 | XmlElement stanza, Map queryAttrs) { 160 | String id = stanza.getAttribute('id'); 161 | String from = stanza.getAttribute('from'); 162 | StanzaBuilder iqresult = Strophe.$iq({'type': 'result', id: id}); 163 | 164 | if (from != null) { 165 | iqresult.attrs({'to': from}); 166 | } 167 | 168 | return iqresult.c('query', queryAttrs); 169 | } 170 | 171 | /** PrivateFunction: _onDiscoInfo 172 | * Called when receive info request 173 | */ 174 | _onDiscoInfo(XmlElement stanza) { 175 | String node = 176 | stanza.findAllElements('query').toList()[0].getAttribute('node'); 177 | Map attrs = {'xmlns': Strophe.NS['DISCO_INFO']}; 178 | if (node != null && node.isNotEmpty) { 179 | attrs['node'] = node; 180 | } 181 | StanzaBuilder iqresult = this._buildIQResult(stanza, attrs); 182 | for (int i = 0; i < this._identities.length; i++) { 183 | attrs = { 184 | 'category': this._identities[i]['category'], 185 | 'type': this._identities[i]['type'] 186 | }; 187 | if (this._identities[i]['name'] != null) 188 | attrs['name'] = this._identities[i]['name']; 189 | if (this._identities[i]['lang'] != null) 190 | attrs['xml:lang'] = this._identities[i]['lang']; 191 | iqresult.c('identity', attrs).up(); 192 | } 193 | for (int i = 0; i < this._features.length; i++) { 194 | iqresult.c('feature', {'var': this._features[i]}).up(); 195 | } 196 | this.connection.send(iqresult.tree()); 197 | return true; 198 | } 199 | 200 | /** PrivateFunction: _onDiscoItems 201 | * Called when receive items request 202 | */ 203 | bool _onDiscoItems(XmlElement stanza) { 204 | Map queryAttrs = {'xmlns': Strophe.NS['DISCO_ITEMS']}; 205 | String node = 206 | stanza.findAllElements('query').toList()[0].getAttribute('node'); 207 | List items; 208 | if (node != null && node.isNotEmpty) { 209 | queryAttrs['node'] = node; 210 | items = []; 211 | for (int i = 0; i < this._items.length; i++) { 212 | if (this._items[i]['node'] == node) { 213 | items = this._items[i]['call_back'](stanza); 214 | break; 215 | } 216 | } 217 | } else { 218 | items = this._items; 219 | } 220 | StanzaBuilder iqresult = this._buildIQResult(stanza, queryAttrs); 221 | for (int i = 0; i < items.length; i++) { 222 | Map attrs = {'jid': items[i].jid}; 223 | if (items[i]['name'] != null) attrs['name'] = items[i].name; 224 | if (items[i].node) attrs['node'] = items[i].node; 225 | iqresult.c('item', attrs).up(); 226 | } 227 | this.connection.send(iqresult.tree()); 228 | return true; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/src/plugins/last-activity.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | 5 | class LastActivity extends PluginClass { 6 | init(StropheConnection conn) { 7 | this.connection = conn; 8 | Strophe.addNamespace('LAST_ACTIVITY', "jabber:iq:last"); 9 | } 10 | 11 | getLastActivity(String jid, Function success, [Function error]) { 12 | String id = this.connection.getUniqueId('last1'); 13 | this.connection.sendIQ( 14 | Strophe.$iq({'id': id, 'type': 'get', 'to': jid}).c( 15 | 'query', {'xmlns': Strophe.NS['LAST_ACTIVITY']}).tree(), 16 | success, 17 | error); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/plugins/pep.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:strophe/src/plugins/pubsub.dart'; 5 | import 'package:xml/xml/nodes/node.dart'; 6 | 7 | class PepPlugin extends PluginClass { 8 | init(StropheConnection c) { 9 | this.connection = c; 10 | if (this.connection.caps == null) { 11 | throw {'error': "caps plugin required!"}; 12 | } 13 | if (this.connection.pubsub == null) { 14 | throw {'error': "pubsub plugin required!"}; 15 | } 16 | } 17 | 18 | subscribe(String node, Function handler) { 19 | this.connection.caps.addFeature(node); 20 | this.connection.caps.addFeature("" + node + "+notify"); 21 | if (handler != null) { 22 | this.connection.addHandler( 23 | handler, Strophe.NS['PUBSUB_EVENT'], "message", null, null, null); 24 | } 25 | return this.connection.caps.sendPres(); 26 | } 27 | 28 | unsubscribe(String node) { 29 | this.connection.caps.removeFeature(node); 30 | this.connection.caps.removeFeature("" + node + "+notify"); 31 | return this.connection.caps.sendPres(); 32 | } 33 | 34 | String publish(String node, items, Function callback) { 35 | String iqid = this.connection.getUniqueId("pubsubpublishnode"); 36 | if (node == null || node.isEmpty) node = 'myPep'; 37 | if (items is List> || items is XmlNode) { 38 | if (callback != null) 39 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 40 | PubsubBuilder c = new PubsubBuilder( 41 | 'iq', {'from': this.connection.jid, 'type': 'set', 'id': iqid}) 42 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 43 | 'publish', {'node': node, 'jid': this.connection.jid}); 44 | if (items is List>) { 45 | Map last = items.last; 46 | if (last != null) last['attrs'].addAll({'id': 'current'}); 47 | this.connection.send(c.list('item', items).tree()); 48 | } else if (items is XmlNode) 49 | this 50 | .connection 51 | .send(c.c('item', {'id': 'current'}).cnode(items).tree()); 52 | return iqid; 53 | } 54 | return ''; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/plugins/plugins.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/enums.dart'; 2 | 3 | abstract class PluginClass { 4 | StropheConnection connection; 5 | Function statusChanged; 6 | PluginClass(); 7 | init(StropheConnection conn); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/plugins/privacy.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart'; 5 | 6 | class PrivacyPlugin extends PluginClass { 7 | /** Variable: lists 8 | * Available privacy lists 9 | */ 10 | Map lists = {}; 11 | /** PrivateVariable: _default 12 | * Default privacy list 13 | */ 14 | String _default = null; 15 | /** PrivateVariable: _active 16 | * Active privacy list 17 | */ 18 | String _active = null; 19 | /** PrivateVariable: _isInitialized 20 | * If lists were pulled from the server, and plugin is ready to work with those. 21 | */ 22 | bool _isInitialized = false; 23 | Function _listChangeCallback; 24 | init(StropheConnection conn) { 25 | this.connection = conn; 26 | this._listChangeCallback = null; 27 | Strophe.addNamespace('PRIVACY', "jabber:iq:privacy"); 28 | } 29 | 30 | bool isInitialized() { 31 | return this._isInitialized; 32 | } 33 | 34 | /** Function: getListNames 35 | * Initial call to get all list names. 36 | * 37 | * This has to be called before any actions with lists. This is separated from init method, to be able to put 38 | * callbacks on the success and fail events. 39 | * 40 | * Params: 41 | * (Function) successCallback - Called upon successful deletion. 42 | * (Function) failCallback - Called upon fail deletion. 43 | * (Function) listChangeCallback - Called upon list change. 44 | */ 45 | getListNames( 46 | [Function successCallback, 47 | Function failCallback, 48 | Function listChangeCallback]) { 49 | this._listChangeCallback = listChangeCallback; 50 | this.connection.sendIQ( 51 | Strophe.$iq({ 52 | 'type': "get", 53 | 'id': this.connection.getUniqueId("privacy") 54 | }).c("query", {'xmlns': Strophe.NS['PRIVACY']}).tree(), 55 | (XmlNode element) { 56 | XmlElement stanza; 57 | if (element is XmlDocument) 58 | stanza = element.rootElement; 59 | else 60 | stanza = element as XmlElement; 61 | Map _lists = this.lists; 62 | this.lists = {}; 63 | List listNames = stanza.findAllElements("list").toList(); 64 | for (int i = 0; i < listNames.length; ++i) { 65 | String listName = listNames[i].getAttribute("name"); 66 | if (_lists[listName] != null) 67 | this.lists[listName] = _lists[listName]; 68 | else 69 | this.lists[listName] = new PrivacyList(listName, false); 70 | this.lists[listName]._isPulled = false; 71 | } 72 | List activeNode = stanza.findAllElements("active").toList(); 73 | if (activeNode.length == 1) 74 | this._active = activeNode[0].getAttribute("name"); 75 | List defaultNode = stanza.findAllElements("default").toList(); 76 | if (defaultNode.length == 1) 77 | this._default = defaultNode[0].getAttribute("name"); 78 | this._isInitialized = true; 79 | if (successCallback != null) 80 | try { 81 | successCallback(); 82 | } catch (e) { 83 | Strophe.error( 84 | "Error while processing callback privacy list names pull."); 85 | } 86 | }, failCallback); 87 | } 88 | 89 | /** Function: newList 90 | * Create new named list. 91 | * 92 | * Params: 93 | * (String) name - New List name. 94 | * 95 | * Returns: 96 | * New list, or existing list if it exists. 97 | */ 98 | PrivacyList newList(String name) { 99 | if (this.lists[name] == null) 100 | this.lists[name] = new PrivacyList(name, true); 101 | return this.lists[name]; 102 | } 103 | 104 | /** Function: newItem 105 | * Create new item. 106 | * 107 | * Params: 108 | * (String) type - Type of item. 109 | * (String) value - Value of item. 110 | * (String) action - Action for the matching. 111 | * (String) order - Order of rule. 112 | * (String) blocked - Block list. 113 | * 114 | * Returns: 115 | * New list, or existing list if it exists. 116 | */ 117 | PrivacyItem newItem(String type, String value, String action, int order, 118 | List blocked) { 119 | PrivacyItem item = new PrivacyItem(); 120 | item.type = type; 121 | item.value = value; 122 | item.action = action; 123 | item.order = order; 124 | item.blocked = blocked; 125 | return item; 126 | } 127 | 128 | /** Function: deleteList 129 | * Delete list. 130 | * 131 | * Params: 132 | * (String) name - List name. 133 | * (Function) successCallback - Called upon successful deletion. 134 | * (Function) failCallback - Called upon fail deletion. 135 | */ 136 | deleteList(String name, Function successCallback, Function failCallback) { 137 | name = name ?? ''; 138 | this.connection.sendIQ( 139 | Strophe.$iq({ 140 | 'type': "set", 141 | 'id': this.connection.getUniqueId("privacy") 142 | }).c("query", {'xmlns': Strophe.NS['PRIVACY']}).c( 143 | "list", {'name': name}).tree(), () { 144 | this.lists.remove(name); 145 | if (successCallback != null) 146 | try { 147 | successCallback(); 148 | } catch (e) { 149 | Strophe.error("Exception while running callback after removing list"); 150 | } 151 | }, failCallback); 152 | } 153 | 154 | /** Function: saveList 155 | * Saves list. 156 | * 157 | * Params: 158 | * (String) name - List name. 159 | * (Function) successCallback - Called upon successful setting. 160 | * (Function) failCallback - Called upon fail setting. 161 | * 162 | * Returns: 163 | * True if list is ok, and is sent to server, false otherwise. 164 | */ 165 | saveList(String name, [Function successCallback, Function failCallback]) { 166 | if (this.lists[name] == null) { 167 | Strophe.error("Trying to save uninitialized list"); 168 | //throw {'error': "List not found"}; 169 | this.newList(name); 170 | } 171 | PrivacyList listModel = this.lists[name]; 172 | if (!listModel.validate()) return false; 173 | StanzaBuilder listIQ = Strophe 174 | .$iq({'type': "set", 'id': this.connection.getUniqueId("privacy")}); 175 | StanzaBuilder list = listIQ 176 | .c("query", {'xmlns': Strophe.NS['PRIVACY']}).c("list", {'name': name}); 177 | 178 | int count = listModel.items.length; 179 | for (int i = 0; i < count; ++i) { 180 | PrivacyItem item = listModel.items[i]; 181 | StanzaBuilder itemNode = list 182 | .c("item", {'action': item.action, 'order': item.order.toString()}); 183 | if (item.type != "") 184 | itemNode.attrs({'type': item.type, 'value': item.value}); 185 | if (item.blocked != null && item.blocked.length > 0) { 186 | int blockCount = item.blocked.length; 187 | for (int j = 0; j < blockCount; ++j) itemNode.c(item.blocked[j]).up(); 188 | } 189 | itemNode.up(); 190 | } 191 | this.connection.sendIQ(listIQ.tree(), () { 192 | listModel._isPulled = true; 193 | if (successCallback != null) 194 | try { 195 | successCallback(); 196 | } catch (e) { 197 | Strophe.error("Exception in callback when saving list " + name); 198 | } 199 | }, failCallback); 200 | return true; 201 | } 202 | 203 | /** Function: loadList 204 | * Loads list from server 205 | * 206 | * Params: 207 | * (String) name - List name. 208 | * (Function) successCallback - Called upon successful load. 209 | * (Function) failCallback - Called upon fail load. 210 | */ 211 | loadList(String name, [Function successCallback, Function failCallback]) { 212 | name = name ?? ''; 213 | this.connection.sendIQ( 214 | Strophe.$iq({ 215 | 'type': "get", 216 | 'id': this.connection.getUniqueId("privacy") 217 | }).c("query", {'xmlns': Strophe.NS['PRIVACY']}).c( 218 | "list", {'name': name}).tree(), (XmlNode element) { 219 | XmlElement stanza; 220 | if (element is XmlDocument) 221 | stanza = element.rootElement; 222 | else 223 | stanza = element as XmlElement; 224 | List lists = stanza.findAllElements("list").toList(); 225 | int listsSize = lists.length; 226 | for (int i = 0; i < listsSize; ++i) { 227 | XmlElement list = lists[i]; 228 | PrivacyList listModel = this.newList(list.getAttribute("name")); 229 | listModel.items = []; 230 | List items = list.findAllElements("item").toList(); 231 | int itemsSize = items.length; 232 | for (int j = 0; j < itemsSize; ++j) { 233 | XmlElement item = items[j]; 234 | List blocks = []; 235 | List blockNodes = item.children; 236 | int nodesSize = blockNodes.length; 237 | for (int k = 0; k < nodesSize; ++k) 238 | blocks.add((blockNodes[k] as XmlElement).name.qualified); 239 | listModel.items.add(this.newItem( 240 | item.getAttribute('type'), 241 | item.getAttribute('value'), 242 | item.getAttribute('action'), 243 | int.parse(item.getAttribute('order')) ?? 0, 244 | blocks)); 245 | } 246 | } 247 | this.lists[name]; 248 | if (successCallback != null) 249 | try { 250 | successCallback(); 251 | } catch (e) { 252 | Strophe.error("Exception while running callback after loading list"); 253 | } 254 | }, failCallback); 255 | } 256 | 257 | /** Function: setActive 258 | * Sets given list as active. 259 | * 260 | * Params: 261 | * (String) name - List name. 262 | * (Function) successCallback - Called upon successful setting. 263 | * (Function) failCallback - Called upon fail setting. 264 | */ 265 | setActive(String name, [Function successCallback, Function failCallback]) { 266 | StanzaBuilder iq = Strophe 267 | .$iq({'type': "set", 'id': this.connection.getUniqueId("privacy")}).c( 268 | "query", {'xmlns': Strophe.NS['PRIVACY']}).c("active"); 269 | if (name != null && name.isNotEmpty) iq.attrs({'name': name}); 270 | this.connection.sendIQ(iq.tree(), () { 271 | this._active = name; 272 | if (successCallback != null) 273 | try { 274 | successCallback(); 275 | } catch (e) { 276 | Strophe.error( 277 | "Exception while running callback after setting active list"); 278 | } 279 | }, failCallback); 280 | } 281 | 282 | /** Function: getActive 283 | * Returns currently active list of null. 284 | */ 285 | String getActive() { 286 | return this._active; 287 | } 288 | 289 | /** Function: setDefault 290 | * Sets given list as default. 291 | * 292 | * Params: 293 | * (String) name - List name. 294 | * (Function) successCallback - Called upon successful setting. 295 | * (Function) failCallback - Called upon fail setting. 296 | */ 297 | setDefault(String name, [Function successCallback, Function failCallback]) { 298 | StanzaBuilder iq = Strophe 299 | .$iq({'type': "set", 'id': this.connection.getUniqueId("privacy")}).c( 300 | "query", {'xmlns': Strophe.NS['PRIVACY']}).c("default"); 301 | if (name != null && name.isNotEmpty) iq.attrs({'name': name}); 302 | this.connection.sendIQ(iq.tree(), () { 303 | this._default = name; 304 | if (successCallback != null) 305 | try { 306 | successCallback(); 307 | } catch (e) { 308 | Strophe.error( 309 | "Exception while running callback after setting default list"); 310 | } 311 | }, failCallback); 312 | } 313 | 314 | /** Function: getDefault 315 | * Returns currently default list of null. 316 | */ 317 | String getDefault() { 318 | return this._default; 319 | } 320 | } 321 | 322 | /** 323 | * Class: PrivacyItem 324 | * Describes single rule. 325 | */ 326 | class PrivacyItem { 327 | /** Variable: type 328 | * One of [jid, group, subscription]. 329 | */ 330 | String type; 331 | String value; 332 | /** Variable: action 333 | * One of [allow, deny]. 334 | * 335 | * Not null. Action to be execute. 336 | */ 337 | String action; 338 | /** Variable: order 339 | * The order in which privacy list items are processed. 340 | * 341 | * Unique, not-null, non-negative integer. 342 | */ 343 | int order; 344 | /** Variable: blocked 345 | * List of blocked stanzas. 346 | * 347 | * One or more of [message, iq, presence-in, presence-out]. Empty set is equivalent to all. 348 | */ 349 | List blocked = []; 350 | /** Function: validate 351 | * Checks if item is of valid structure 352 | */ 353 | bool validate() { 354 | if (["jid", "group", "subscription", ""].indexOf(this.type) < 0) 355 | return false; 356 | if (this.type == "subscription") { 357 | if (["both", "to", "from", "none"].indexOf(this.value) < 0) return false; 358 | } 359 | if (["allow", "deny"].indexOf(this.action) < 0) return false; 360 | bool hasMatch = new RegExp(r"^\d+$").hasMatch(this.order.toString()); 361 | if (this.order == 0 || !hasMatch) return false; 362 | if (this.blocked.length > 0) { 363 | //if(typeof(this.blocked) != "object") return false; 364 | List possibleBlocks = [ 365 | "message", 366 | "iq", 367 | "presence-in", 368 | "presence-out" 369 | ]; 370 | int blockCount = this.blocked.length; 371 | for (int i = 0; i < blockCount; ++i) { 372 | if (possibleBlocks.indexOf(this.blocked[i]) < 0) return false; 373 | possibleBlocks.remove(this.blocked[i]); 374 | } 375 | } 376 | return true; 377 | } 378 | 379 | /** Function: copy 380 | * Copy one item into another. 381 | */ 382 | copy(PrivacyItem item) { 383 | this.type = item.type; 384 | this.value = item.value; 385 | this.action = item.action; 386 | this.order = item.order; 387 | this.blocked = item.blocked.getRange(0, item.blocked.length).toList(); 388 | } 389 | } 390 | 391 | /** 392 | * Class: List 393 | * Contains list of rules. There is no layering. 394 | */ 395 | class PrivacyList { 396 | PrivacyList(this._name, this._isPulled); 397 | /** PrivateVariable: _name 398 | * List name. 399 | * 400 | * Not changeable. Create new, copy this one, and delete, if you wish to rename. 401 | */ 402 | String _name; 403 | /** PrivateVariable: _isPulled 404 | * If list is pulled from server and up to date. 405 | * 406 | * Is false upon first getting of list of lists, or after getting stanza about update 407 | */ 408 | bool _isPulled; 409 | /** Variable: items 410 | * Items of this list. 411 | */ 412 | List items = []; 413 | /** Function: getName 414 | * Returns list name 415 | */ 416 | String getName() { 417 | return this._name; 418 | } 419 | 420 | /** Function: isPulled 421 | * If list is pulled from server. 422 | * 423 | * This is false for list names just taken from server. you need to make loadList to see all the contents of the list. 424 | * Also this is possible when list was changed somewhere else, and you've got announcement about update. Same loadList 425 | * is your savior. 426 | */ 427 | bool isPulled() { 428 | return this._isPulled; 429 | } 430 | 431 | /** Function: validate 432 | * Checks if list is of valid structure 433 | */ 434 | bool validate() { 435 | List orders = []; 436 | this.items = this.items.where((PrivacyItem item) { 437 | return item != null; 438 | }).toList(); 439 | int itemCount = this.items.length; 440 | for (int i = 0; i < itemCount; ++i) { 441 | if (this.items[i] == null || !this.items[i].validate()) return false; 442 | if (orders.indexOf(this.items[i].order) >= 0) return false; 443 | orders.add(this.items[i].order); 444 | } 445 | return true; 446 | } 447 | 448 | /** Function: copy 449 | * Copy all items of one list into another. 450 | * 451 | * Params: 452 | * (List) list - list to copy items from. 453 | */ 454 | copy(PrivacyList list) { 455 | this.items = []; 456 | int l = list.items.length; 457 | for (int i = 0; i < l; ++i) { 458 | this.items[i] = new PrivacyItem(); 459 | this.items[i].copy(list.items[i]); 460 | } 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /lib/src/plugins/private-storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | 6 | /** 7 | * This plugin is distributed under the terms of the MIT licence. 8 | * Please see the LICENCE file for details. 9 | * Copyright (c) Markus Kohlhase, 2011 10 | */ 11 | 12 | /** 13 | * File: strophe.private.js 14 | * A Strophe plugin for XMPP Private XML Storage ( http://xmpp.org/extensions/xep-0049.html ) 15 | */ 16 | class PrivateStorage extends PluginClass { 17 | // called by the Strophe.Connection constructor 18 | 19 | init(StropheConnection conn) { 20 | this.connection = conn; 21 | Strophe.addNamespace('PRIVATE', "jabber:iq:private"); 22 | } 23 | 24 | /** 25 | * Function: set 26 | * 27 | * Parameters: 28 | * (String) tag - the tag name 29 | * (String) ns - the namespace 30 | * (XML) data - the data you want to save 31 | * (Function) success - Callback function on success 32 | * (Function) error - Callback function on error 33 | */ 34 | 35 | set(String tag, String ns, data, [Function success, Function error]) { 36 | String id = this.connection.getUniqueId('saveXML'); 37 | ns = ns ?? 'namespace'; 38 | tag = tag ?? 'tag'; 39 | StanzaBuilder iq = Strophe.$iq({'type': 'set', 'id': id}).c( 40 | 'query', {'xmlns': Strophe.NS['PRIVATE']}).c(tag, {'xmlns': ns}); 41 | 42 | xml.XmlNode d = this._transformData(data); 43 | 44 | if (d != null) { 45 | iq.cnode(d); 46 | } 47 | 48 | this.connection.sendIQ(iq.tree(), success, error); 49 | } 50 | 51 | /** 52 | * Function: get 53 | * 54 | * Parameters: 55 | * (String) tag - the tag name 56 | * (String) ns - the namespace 57 | * (Function) success - Callback function on success 58 | * (Function) error - Callback function on error 59 | */ 60 | 61 | get(String tag, String ns, Function success, [Function error]) { 62 | String id = this.connection.getUniqueId('loadXML'); 63 | ns = ns ?? 'namespace'; 64 | tag = tag ?? 'tag'; 65 | StanzaBuilder iq = Strophe.$iq({'type': 'get', 'id': id}).c( 66 | 'query', {'xmlns': Strophe.NS['PRIVATE']}).c(tag, {'xmlns': ns}); 67 | 68 | this.connection.sendIQ(iq.tree(), (xml.XmlElement iq) { 69 | xml.XmlNode data = iq; 70 | 71 | for (int i = 0; i < 3; i++) { 72 | data = data.children[0]; 73 | if (data == null) { 74 | break; 75 | } 76 | } 77 | 78 | success(data, iq); 79 | }, error); 80 | } 81 | 82 | /** 83 | * PrivateFunction: _transformData 84 | */ 85 | xml.XmlNode _transformData(c) { 86 | switch (c.runtimeType.toString()) { 87 | case "num": 88 | case "bool": 89 | return Strophe.xmlTextNode(c + ''); 90 | case "String": 91 | xml.XmlElement dom = this._textToXml(c); 92 | 93 | if (dom != null) { 94 | return dom; 95 | } else { 96 | return Strophe.xmlTextNode(c + ''); 97 | } 98 | break; 99 | default: 100 | if (this._isNode(c) || this._isElement(c)) { 101 | return c; 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | /** 108 | * PrivateFunction: _textToXml 109 | * 110 | * Parameters: 111 | * (String) text - XML String 112 | * 113 | * Returns: 114 | * (Object) dom - DOM Object 115 | */ 116 | 117 | xml.XmlElement _textToXml(String text) { 118 | try { 119 | return xml.parse(text).rootElement; 120 | } catch (e) { 121 | return xml.parse('$text').rootElement; 122 | } 123 | } 124 | /** 125 | * PrivateFunction: _isNode 126 | * 127 | * Parameters: 128 | * ( Object ) obj - The object to test 129 | * 130 | * Returns: 131 | * True if it is a DOM node 132 | */ 133 | 134 | bool _isNode(obj) { 135 | return obj is xml.XmlNode; 136 | } 137 | 138 | /** 139 | * PrivateFunction: _isElement 140 | * 141 | * Parameters: 142 | * ( Object ) obj - The object to test 143 | * 144 | * Returns: 145 | * True if it is a DOM element. 146 | */ 147 | 148 | bool _isElement(obj) { 149 | return obj is xml.XmlElement; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/plugins/pubsub.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml/nodes/node.dart'; 5 | 6 | /** File: strophe.pubsub.js 7 | * A Strophe plugin for XMPP Publish-Subscribe. 8 | * 9 | * Provides Strophe.Connection.pubsub object, 10 | * parially implementing XEP 0060. 11 | * 12 | * Strophe.Builder.prototype methods should probably move to strophe.js 13 | */ 14 | 15 | class PubsubBuilder extends StanzaBuilder { 16 | PubsubBuilder(String name, [Map attrs]) : super(name, attrs); 17 | /** Function: Strophe.Builder.form 18 | * Add an options form child element. 19 | * 20 | * Does not change the current element. 21 | * 22 | * Parameters: 23 | * (String) ns - form namespace. 24 | * (Object) options - form properties. 25 | * 26 | * Returns: 27 | * The Strophe.Builder object. 28 | */ 29 | form(String ns, Map options) { 30 | XmlNode xmlElement = Strophe.xmlElement('x', 31 | attrs: {"xmlns": "jabber:x:data", "type": "submit"}); 32 | PubsubBuilder aX = this.cnode(xmlElement); 33 | aX 34 | .cnode(Strophe.xmlElement('field', 35 | attrs: {"var": "FORM_TYPE", "type": "hidden"})) 36 | .cnode(Strophe.xmlElement('value')) 37 | .t(ns) 38 | .up() 39 | .up(); 40 | options.forEach((String key, value) { 41 | aX 42 | .cnode(Strophe.xmlElement('field', attrs: {"var": key})) 43 | .cnode(Strophe.xmlElement('value')) 44 | .t(options[key].toString()) 45 | .up() 46 | .up(); 47 | }); 48 | return this; 49 | } 50 | 51 | /** Function: Strophe.Builder.list 52 | * Add many child elements. 53 | * 54 | * Does not change the current element. 55 | * 56 | * Parameters: 57 | * (String) tag - tag name for children. 58 | * (Array) array - list of objects with format: 59 | * { attrs: { [string]:[string], ... } // attributes of each tag element 60 | * data: [string | XML_element] } // contents of each tag element 61 | * 62 | * Returns: 63 | * The Strophe.Builder object. 64 | */ 65 | list(String tag, List> array) { 66 | if (array == null) return this; 67 | for (int i = 0; i < array.length; ++i) { 68 | this.c(tag, array[i]['attrs']); 69 | if (array[i]['data'] == null) continue; 70 | if (array[i]['data'] is String) { 71 | this.cnode(Strophe.xmlElement('data', 72 | attrs: array[i]['attrs'], text: array[i]['data'])); 73 | } else { 74 | var stanza = array[i]['data']; 75 | if (array[i]['data'] is StanzaBuilder) stanza = array[i]['data'].tree(); 76 | this.cnode(Strophe.copyElement(stanza as XmlNode)); 77 | } 78 | this.up(); 79 | } 80 | return this; 81 | } 82 | 83 | @override 84 | PubsubBuilder c(String name, [Map attrs, dynamic text]) { 85 | return super.c(name, attrs, text) as PubsubBuilder; 86 | } 87 | 88 | children(Map object) { 89 | object.forEach((key, value) { 90 | if (value is List) { 91 | this.list(key, value); 92 | } else if (value is String) { 93 | this.c(key, {}, value); 94 | } else if (value is num) { 95 | this.c(key, {}, value.toString()); 96 | } else if (value is Map) { 97 | this.c(key).children(value).up(); 98 | } else { 99 | this.c(key).up(); 100 | } 101 | }); 102 | return this; 103 | } 104 | } 105 | 106 | class PubsubPlugin extends PluginClass { 107 | PubsubPlugin() { 108 | // Called by Strophe on connection event 109 | statusChanged = (status, condition) { 110 | if (this._autoService && status == Strophe.Status['CONNECTED']) { 111 | this.service = 112 | 'pubsub.' + Strophe.getDomainFromJid(this.connection.jid); 113 | this.jid = this.connection.jid; 114 | } 115 | }; 116 | } 117 | 118 | /* Extend Strophe.Connection to have member 'pubsub'. 119 | */ 120 | /* 121 | Extend connection object to have plugin name 'pubsub'. 122 | */ 123 | bool _autoService = true; 124 | String service; 125 | String jid; 126 | Map> handler = {}; 127 | 128 | //The plugin must have the init function. 129 | init(StropheConnection conn) { 130 | this.connection = conn; 131 | 132 | /* 133 | Function used to setup plugin. 134 | */ 135 | 136 | /* extend name space 137 | * NS['PUBSUB'] - XMPP Publish Subscribe namespace 138 | * from XEP 60. 139 | * 140 | * NS.PUBSUB_SUBSCRIBE_OPTIONS - XMPP pubsub 141 | * options namespace from XEP 60. 142 | */ 143 | Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); 144 | Strophe.addNamespace('PUBSUB_SUBSCRIBE_OPTIONS', 145 | Strophe.NS['PUBSUB'] + "#subscribe_options"); 146 | Strophe.addNamespace('PUBSUB_ERRORS', Strophe.NS['PUBSUB'] + "#errors"); 147 | Strophe.addNamespace('PUBSUB_EVENT', Strophe.NS['PUBSUB'] + "#event"); 148 | Strophe.addNamespace('PUBSUB_OWNER', Strophe.NS['PUBSUB'] + "#owner"); 149 | Strophe.addNamespace( 150 | 'PUBSUB_AUTO_CREATE', Strophe.NS['PUBSUB'] + "#auto-create"); 151 | Strophe.addNamespace( 152 | 'PUBSUB_PUBLISH_OPTIONS', Strophe.NS['PUBSUB'] + "#publish-options"); 153 | Strophe.addNamespace( 154 | 'PUBSUB_NODE_CONFIG', Strophe.NS['PUBSUB'] + "#node_config"); 155 | Strophe.addNamespace('PUBSUB_CREATE_AND_CONFIGURE', 156 | Strophe.NS['PUBSUB'] + "#create-and-configure"); 157 | Strophe.addNamespace('PUBSUB_SUBSCRIBE_AUTHORIZATION', 158 | Strophe.NS['PUBSUB'] + "#subscribe_authorization"); 159 | Strophe.addNamespace( 160 | 'PUBSUB_GET_PENDING', Strophe.NS['PUBSUB'] + "#get-pending"); 161 | Strophe.addNamespace('PUBSUB_MANAGE_SUBSCRIPTIONS', 162 | Strophe.NS['PUBSUB'] + "#manage-subscriptions"); 163 | Strophe.addNamespace( 164 | 'PUBSUB_META_DATA', Strophe.NS['PUBSUB'] + "#meta-data"); 165 | Strophe.addNamespace('ATOM', "http://www.w3.org/2005/Atom"); 166 | 167 | if (conn.disco != null) conn.disco.addFeature(Strophe.NS['PUBSUB']); 168 | } 169 | 170 | /***Function 171 | Parameters: 172 | (String) jid - The node owner's jid. 173 | (String) service - The name of the pubsub service. 174 | */ 175 | connect(String jid, [String service]) { 176 | if (service == null) { 177 | service = jid; 178 | jid = null; 179 | } 180 | this.jid = jid ?? this.connection.jid; 181 | this.service = service ?? null; 182 | this._autoService = false; 183 | } 184 | 185 | /***Function 186 | Parameters: 187 | (String) node - The name of node 188 | (String) handler - reference to registered strophe handler 189 | */ 190 | storeHandler(String node, StanzaHandler handler) { 191 | if (this.handler[node] == null) { 192 | this.handler[node] = []; 193 | } 194 | this.handler[node].add(handler); 195 | } 196 | 197 | /***Function 198 | Parameters: 199 | (String) node - The name of node 200 | */ 201 | removeHandler(String node) { 202 | List toberemoved = this.handler[node]; 203 | this.handler[node] = []; 204 | 205 | // remove handler 206 | if (toberemoved != null && toberemoved.length > 0) { 207 | for (int i = 0, l = toberemoved.length; i < l; i++) { 208 | this.connection.deleteHandler(toberemoved[i]); 209 | } 210 | } 211 | } 212 | 213 | /***Function 214 | Create a pubsub node on the given service with the given node 215 | name. 216 | Parameters: 217 | (String) node - The name of the pubsub node. 218 | (Dictionary) options - The configuration options for the node. 219 | (Function) call_back - Used to determine if node 220 | creation was sucessful. 221 | Returns: 222 | Iq id used to send subscription. 223 | */ 224 | createNode(String node, 225 | [String service, Map options, Function callback]) { 226 | String iqid = this.connection.getUniqueId("pubsubcreatenode"); 227 | service = service != null && service.isNotEmpty ? service : this.service; 228 | PubsubBuilder iq = new PubsubBuilder('iq', { 229 | 'from': Strophe.getBareJidFromJid(this.jid), 230 | 'to': service, 231 | 'type': 'set', 232 | 'id': iqid 233 | }).c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 234 | 'create', node != null ? {'node': node} : null); 235 | if (options != null) { 236 | iq = iq.up().c('configure'); 237 | iq.form(Strophe.NS['PUBSUB_NODE_CONFIG'], options); 238 | } 239 | if (callback != null) 240 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 241 | this.connection.send(iq.tree()); 242 | return iqid; 243 | } 244 | 245 | /** Function: deleteNode 246 | * Delete a pubsub node. 247 | * 248 | * Parameters: 249 | * (String) node - The name of the pubsub node. 250 | * (Function) call_back - Called on server response. 251 | * 252 | * Returns: 253 | * Iq id 254 | */ 255 | deleteNode(String node, [Function callback]) { 256 | String iqid = this.connection.getUniqueId("pubsubdeletenode"); 257 | 258 | StanzaBuilder iq = Strophe.$iq( 259 | {'from': this.jid, 'to': this.service, 'type': 'set', 'id': iqid}) 260 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB_OWNER']}).c( 261 | 'delete', {'node': node}); 262 | 263 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 264 | this.connection.send(iq.tree()); 265 | 266 | return iqid; 267 | } 268 | 269 | /** Function 270 | * 271 | * Get all nodes this.connection currently exist. 272 | * 273 | * Parameters: 274 | * (Function) success - Used to determine if node creation was sucessful. 275 | * (Function) error - Used to determine if node 276 | * creation had errors. 277 | */ 278 | discoverNodes( 279 | [String service, Function success, Function error, int timeout]) { 280 | //ask for all nodes 281 | service = service != null && service.isNotEmpty ? service : this.service; 282 | StanzaBuilder iq = 283 | Strophe.$iq({'from': this.jid, 'to': service, 'type': 'get'}) 284 | .c('query', {'xmlns': Strophe.NS['DISCO_ITEMS']}); 285 | 286 | return this.connection.sendIQ(iq.tree(), success, error, timeout); 287 | } 288 | 289 | /** Function: getConfig 290 | * Get node configuration form. 291 | * 292 | * Parameters: 293 | * (String) node - The name of the pubsub node. 294 | * (Function) call_back - Receives config form. 295 | * 296 | * Returns: 297 | * Iq id 298 | */ 299 | getConfig(String node, Function callback) { 300 | String iqid = this.connection.getUniqueId("pubsubconfigurenode"); 301 | 302 | StanzaBuilder iq = Strophe.$iq( 303 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 304 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB_OWNER']}).c( 305 | 'configure', {'node': node}); 306 | 307 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 308 | this.connection.send(iq.tree()); 309 | 310 | return iqid; 311 | } 312 | 313 | /** 314 | * Parameters: 315 | * (Function) call_back - Receives subscriptions. 316 | * 317 | * http://xmpp.org/extensions/tmp/xep-0060-1.13.html 318 | * 8.3 Request Default Node Configuration Options 319 | * 320 | * Returns: 321 | * Iq id 322 | */ 323 | String getDefaultNodeConfig(Function callback) { 324 | String iqid = this.connection.getUniqueId("pubsubdefaultnodeconfig"); 325 | 326 | StanzaBuilder iq = Strophe.$iq( 327 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 328 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB_OWNER']}).c('default'); 329 | 330 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 331 | this.connection.send(iq.tree()); 332 | 333 | return iqid; 334 | } 335 | 336 | /***Function 337 | Subscribe to a node in order to receive event items. 338 | Parameters: 339 | (String) node - The name of the pubsub node. 340 | (Array) options - The configuration options for the node. 341 | (Function) event_cb - Used to recieve subscription events. 342 | (Function) success - callback function for successful node creation. 343 | (Function) error - error callback function. 344 | (Boolean) barejid - use barejid creation was sucessful. 345 | Returns: 346 | Iq id used to send subscription. 347 | */ 348 | subscribe(String node, 349 | [String service, 350 | Map options, 351 | Function eventcb, 352 | Function success, 353 | Function error, 354 | bool barejid = true]) { 355 | String iqid = this.connection.getUniqueId("subscribenode"); 356 | 357 | String jid = this.jid; 358 | if (barejid) jid = Strophe.getBareJidFromJid(jid); 359 | service = service != null && service.isNotEmpty ? service : this.service; 360 | PubsubBuilder iq = new PubsubBuilder( 361 | 'iq', {'from': this.jid, 'to': service, 'type': 'set', 'id': iqid}) 362 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 363 | 'subscribe', {'node': node, 'jid': jid}); 364 | if (options != null) { 365 | PubsubBuilder c = iq.up().c('options'); 366 | 367 | c.form(Strophe.NS['PUBSUB_SUBSCRIBE_OPTIONS'], options); 368 | } 369 | 370 | //add the event handler to receive items 371 | StanzaHandler hand = 372 | this.connection.addHandler(eventcb, null, 'message', null, null, null); 373 | this.storeHandler(node, hand); 374 | this.connection.sendIQ(iq.tree(), success, error); 375 | return iqid; 376 | } 377 | 378 | /***Function 379 | Unsubscribe from a node. 380 | Parameters: 381 | (String) node - The name of the pubsub node. 382 | (Function) success - callback function for successful node creation. 383 | (Function) error - error callback function. 384 | */ 385 | unsubscribe(String node, String jid, 386 | [String service, String subid, Function success, Function error]) { 387 | String iqid = this.connection.getUniqueId("pubsubunsubscribenode"); 388 | service = service != null && service.isNotEmpty ? service : this.service; 389 | StanzaBuilder iq = Strophe.$iq( 390 | {'from': this.jid, 'to': service, 'type': 'set', 'id': iqid}) 391 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('unsubscribe', { 392 | 'node': node, 393 | 'jid': jid ?? Strophe.getBareJidFromJid(connection.jid) 394 | }); 395 | if (subid != null && subid.isNotEmpty) iq.attrs({'subid': subid}); 396 | 397 | this.connection.sendIQ(iq.tree(), success, error); 398 | this.removeHandler(node); 399 | return iqid; 400 | } 401 | 402 | /***Function 403 | Publish and item to the given pubsub node. 404 | Parameters: 405 | (String) node - The name of the pubsub node. 406 | (Array) items - The list of items to be published. 407 | (Function) call_back - Used to determine if node 408 | creation was sucessful. 409 | */ 410 | String publish( 411 | String node, List> items, Function callback) { 412 | String iqid = this.connection.getUniqueId("pubsubpublishnode"); 413 | 414 | PubsubBuilder iq = new PubsubBuilder('iq', { 415 | 'from': this.jid, 416 | 'to': this.service, 417 | 'type': 'set', 418 | 'id': iqid 419 | }).c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 420 | 'publish', {'node': node, 'jid': this.jid}); 421 | iq.list('item', items); 422 | 423 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 424 | this.connection.send(iq.tree()); 425 | 426 | return iqid; 427 | } 428 | 429 | /*Function: items 430 | Used to retrieve the persistent items from the pubsub node. 431 | */ 432 | String items(String node, [Function success, Function error, int timeout]) { 433 | //ask for all items 434 | StanzaBuilder iq = 435 | Strophe.$iq({'from': this.jid, 'to': this.service, 'type': 'get'}) 436 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 437 | 'items', {'node': node}); 438 | 439 | return this.connection.sendIQ(iq.tree(), success, error, timeout); 440 | } 441 | 442 | /** Function: getSubscriptions 443 | * Get subscriptions of a JID. 444 | * 445 | * Parameters: 446 | * (Function) call_back - Receives subscriptions. 447 | * 448 | * http://xmpp.org/extensions/tmp/xep-0060-1.13.html 449 | * 5.6 Retrieve Subscriptions 450 | * 451 | * Returns: 452 | * Iq id 453 | */ 454 | getSubscriptions(Function callback) { 455 | String iqid = this.connection.getUniqueId("pubsubsubscriptions"); 456 | 457 | StanzaBuilder iq = Strophe.$iq( 458 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 459 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('subscriptions'); 460 | 461 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 462 | this.connection.send(iq.tree()); 463 | 464 | return iqid; 465 | } 466 | 467 | /** Function: getNodeSubscriptions 468 | * Get node subscriptions of a JID. 469 | * 470 | * Parameters: 471 | * (Function) call_back - Receives subscriptions. 472 | * 473 | * http://xmpp.org/extensions/tmp/xep-0060-1.13.html 474 | * 5.6 Retrieve Subscriptions 475 | * 476 | * Returns: 477 | * Iq id 478 | */ 479 | getNodeSubscriptions(String node, Function callback) { 480 | String iqid = this.connection.getUniqueId("pubsubsubscriptions"); 481 | 482 | StanzaBuilder iq = Strophe.$iq( 483 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 484 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB_OWNER']}).c( 485 | 'subscriptions', {'node': node}); 486 | 487 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 488 | this.connection.send(iq.tree()); 489 | 490 | return iqid; 491 | } 492 | 493 | /** Function: getSubOptions 494 | * Get subscription options form. 495 | * 496 | * Parameters: 497 | * (String) node - The name of the pubsub node. 498 | * (String) subid - The subscription id (optional). 499 | * (Function) call_back - Receives options form. 500 | * 501 | * Returns: 502 | * Iq id 503 | */ 504 | getSubOptions(String node, String subid, Function callback) { 505 | String iqid = this.connection.getUniqueId("pubsubsuboptions"); 506 | 507 | StanzaBuilder iq = Strophe.$iq( 508 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 509 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c( 510 | 'options', {'node': node, 'jid': this.jid}); 511 | if (subid != null && subid.isNotEmpty) iq.attrs({'subid': subid}); 512 | 513 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 514 | this.connection.send(iq.tree()); 515 | 516 | return iqid; 517 | } 518 | 519 | /** 520 | * Parameters: 521 | * (String) node - The name of the pubsub node. 522 | * (Function) call_back - Receives subscriptions. 523 | * 524 | * http://xmpp.org/extensions/tmp/xep-0060-1.13.html 525 | * 8.9 Manage Affiliations - 8.9.1.1 Request 526 | * 527 | * Returns: 528 | * Iq id 529 | */ 530 | getAffiliations(String node, Function callback) { 531 | String iqid = this.connection.getUniqueId("pubsubaffiliations"); 532 | 533 | Map attrs = {}, xmlns = {'xmlns': Strophe.NS['PUBSUB']}; 534 | if (node != null && node.isNotEmpty) { 535 | attrs['node'] = node; 536 | xmlns = {'xmlns': Strophe.NS['PUBSUB_OWNER']}; 537 | } 538 | 539 | StanzaBuilder iq = Strophe.$iq( 540 | {'from': this.jid, 'to': this.service, 'type': 'get', 'id': iqid}) 541 | .c('pubsub', xmlns) 542 | .c('affiliations', attrs); 543 | 544 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 545 | this.connection.send(iq.tree()); 546 | 547 | return iqid; 548 | } 549 | 550 | /** 551 | * Parameters: 552 | * (String) node - The name of the pubsub node. 553 | * (Function) call_back - Receives subscriptions. 554 | * 555 | * http://xmpp.org/extensions/tmp/xep-0060-1.13.html 556 | * 8.9.2 Modify Affiliation - 8.9.2.1 Request 557 | * 558 | * Returns: 559 | * Iq id 560 | */ 561 | setAffiliation( 562 | String node, String jid, String affiliation, Function callback) { 563 | String iqid = this.connection.getUniqueId("pubsubaffiliations"); 564 | 565 | StanzaBuilder iq = Strophe.$iq( 566 | {'from': this.jid, 'to': this.service, 'type': 'set', 'id': iqid}) 567 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB_OWNER']}).c('affiliations', { 568 | 'node': node 569 | }).c('affiliation', {'jid': jid, 'affiliation': affiliation}); 570 | 571 | this.connection.addHandler(callback, null, 'iq', null, iqid, null); 572 | this.connection.send(iq.tree()); 573 | 574 | return iqid; 575 | } 576 | 577 | /** Function: publishAtom 578 | */ 579 | publishAtom(String node, List atoms, Function callback) { 580 | Map atom; 581 | List> entries = []; 582 | for (int i = 0; i < atoms.length; i++) { 583 | atom = atoms[i]; 584 | 585 | atom['updated'] = atom['updated'] ?? new DateTime.now().toIso8601String(); 586 | if (atom['published'] && atom['published'].toIso8601String()) 587 | atom['published'] = atom['published'].toIso8601String(); 588 | PubsubBuilder data = 589 | Strophe.$build("entry", {'xmlns': Strophe.NS['ATOM']}); 590 | entries.add({ 591 | 'data': data.children(atom).tree(), 592 | 'attrs': atom['id'] ? {'id': atom['id']} : {}, 593 | }); 594 | } 595 | return this.publish(node, entries, callback); 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /lib/src/plugins/register.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:strophe/src/core.dart'; 4 | import 'package:strophe/src/enums.dart'; 5 | import 'package:strophe/src/plugins/plugins.dart'; 6 | import 'package:strophe/src/utils.dart'; 7 | import 'package:xml/xml/nodes/document.dart'; 8 | import 'package:xml/xml/nodes/element.dart'; 9 | 10 | /* 11 | This library is free software; you can redistribute it and/or modify it 12 | under the terms of the GNU Lesser General Public License as published 13 | by the Free Software Foundation; either version 2.1 of the License, or 14 | (at your option) any later version. 15 | . 16 | This library is distributed in the hope that it will be useful, but 17 | WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 19 | General Public License for more details. 20 | Copyright (c) dodo , 2011 21 | */ 22 | 23 | class RegisterPlugin extends PluginClass { 24 | String domain; 25 | String instructions; 26 | Map fields; 27 | bool registered = false; 28 | bool _registering = false; 29 | bool processed_features = false; 30 | 31 | Map _connect_cb_data = {}; 32 | //The plugin must have the init function. 33 | @override 34 | init(StropheConnection conn) { 35 | this.connection = conn; 36 | // compute free emun index number 37 | int i = 0; 38 | Strophe.Status.forEach((String key, int value) { 39 | i = max(i, Strophe.Status[key]); 40 | }); 41 | 42 | /* extend name space 43 | * NS['REGISTER'] - In-Band Registration 44 | * from XEP 77. 45 | */ 46 | Strophe.addNamespace('REGISTER', 'jabber:iq:register'); 47 | Strophe.Status['REGIFAIL'] = i + 1; 48 | Strophe.Status['REGISTER'] = i + 2; 49 | Strophe.Status['REGISTERED'] = i + 3; 50 | Strophe.Status['CONFLICT'] = i + 4; 51 | Strophe.Status['NOTACCEPTABLE'] = i + 5; 52 | if (conn.disco != null) { 53 | if (conn.disco.addFeature is Function) 54 | conn.disco.addFeature(Strophe.NS['REGISTER']); 55 | //if (conn.disco.addNode is Function) 56 | //conn.disco.addNode(Strophe.NS['REGISTER'], {'items': []}); 57 | } 58 | 59 | // hooking strophe's connection.reset 60 | Function reset = conn.reset; 61 | conn.reset = () { 62 | reset(); 63 | this.instructions = ""; 64 | this.fields = {}; 65 | this.registered = false; 66 | }; 67 | 68 | // hooking strophe's _connect_cb 69 | Function connect_cb = conn.connectCb; 70 | conn.connectCb = (req, Function _callback, String raw) { 71 | if (!this._registering) { 72 | if (this.processed_features) { 73 | // exchange Input hooks to not print the stream:features twice 74 | //var xmlInput = conn.xmlInput; 75 | //conn.xmlInput = Strophe.Connection.xmlInput; 76 | //var rawInput = conn.rawInput; 77 | //conn.rawInput = Strophe.Connection.prototype.rawInput; 78 | connect_cb(req, _callback, raw); 79 | //conn.xmlInput = xmlInput; 80 | //conn.rawInput = rawInput; 81 | this.processed_features = false; 82 | } else { 83 | connect_cb(req, _callback, raw); 84 | } 85 | } else { 86 | // Save this request in case we want to authenticate later 87 | this._connect_cb_data = {'req': req, 'raw': raw}; 88 | if (this._register_cb(req, _callback, raw)) { 89 | // remember that we already processed stream:features 90 | this.processed_features = true; 91 | this._registering = false; 92 | } 93 | } 94 | }; 95 | 96 | // hooking strophe`s authenticate 97 | Function auth_old = conn.authenticate; 98 | conn.authenticate = (List matched) { 99 | if (matched == null) { 100 | var conn = this.connection; 101 | 102 | if (this.fields['username'] == null || 103 | this.fields['username'].isEmpty || 104 | this.domain == null || 105 | this.fields['password'] == null || 106 | this.fields['password'].isEmpty) { 107 | Strophe.info("Register a JID first!"); 108 | return; 109 | } 110 | 111 | String jid = this.fields['username'] + "@" + this.domain; 112 | 113 | conn.jid = jid; 114 | conn.authzid = Strophe.getBareJidFromJid(conn.jid); 115 | conn.authcid = Strophe.getNodeFromJid(conn.jid); 116 | conn.pass = this.fields['password']; 117 | var req = this._connect_cb_data['req']; 118 | var callback = conn.connectCallback; 119 | var raw = this._connect_cb_data['raw']; 120 | conn.connectCb(req, callback, raw); 121 | } else { 122 | auth_old(matched); 123 | } 124 | }; 125 | } 126 | 127 | /** Function: connect 128 | * Starts the registration process. 129 | * 130 | * As the registration process proceeds, the user supplied callback will 131 | * be triggered multiple times with status updates. The callback 132 | * should take two arguments - the status code and the error condition. 133 | * 134 | * The status code will be one of the values in the Strophe.Status 135 | * constants. The error condition will be one of the conditions 136 | * defined in RFC 3920 or the condition 'strophe-parsererror'. 137 | * 138 | * Please see XEP 77 for a more detailed explanation of the optional 139 | * parameters below. 140 | * 141 | * Parameters: 142 | * (String) domain - The xmpp server's Domain. This will be the server, 143 | * which will be contacted to register a new JID. 144 | * The server has to provide and allow In-Band Registration (XEP-0077). 145 | * (Function) callback The connect callback function. 146 | * (Integer) wait - The optional HTTPBIND wait value. This is the 147 | * time the server will wait before returning an empty result for 148 | * a request. The default setting of 60 seconds is recommended. 149 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 150 | * (Integer) hold - The optional HTTPBIND hold value. This is the 151 | * number of connections the server will hold at one time. This 152 | * should almost always be set to 1 (the default). 153 | */ 154 | connect(String domain, ConnectCallBack callback, 155 | [int wait, int hold, String route]) { 156 | StropheConnection conn = this.connection; 157 | this.domain = Strophe.getDomainFromJid(domain); 158 | this.instructions = ""; 159 | this.fields = {}; 160 | this.registered = false; 161 | 162 | this._registering = true; 163 | conn.connect(this.domain, "", callback, wait, hold, route); 164 | } 165 | 166 | /** PrivateFunction: _register_cb 167 | * _Private_ handler for initial registration request. 168 | * 169 | * This handler is used to process the initial registration request 170 | * response from the BOSH server. It is used to set up a bosh session 171 | * and requesting registration fields from host. 172 | *type 173 | * Parameters: 174 | * (Strophe.Request) req - The current request. 175 | */ 176 | _register_cb(req, Function _callback, String raw) { 177 | StropheConnection conn = this.connection; 178 | Strophe.info("_register_cb was called"); 179 | conn.connected = true; 180 | 181 | XmlElement bodyWrap = conn.proto.reqToData(req); 182 | if (bodyWrap == null) { 183 | return false; 184 | } 185 | //if (conn.xmlInput !== Strophe.Connection.prototype.xmlInput) { 186 | if (bodyWrap.name.qualified == conn.proto.strip && 187 | bodyWrap.children.length > 0) { 188 | conn.xmlInput(bodyWrap.firstChild); 189 | } else { 190 | conn.xmlInput(bodyWrap); 191 | } 192 | //} 193 | //if (conn.rawInput !== Strophe.Connection.prototype.rawInput) { 194 | if (raw != null) { 195 | conn.rawInput(raw); 196 | } else { 197 | conn.rawInput(Strophe.serialize(bodyWrap)); 198 | } 199 | //} 200 | 201 | var conncheck = conn.proto.connectCb(bodyWrap); 202 | if (conncheck == Strophe.Status['CONNFAIL']) { 203 | return false; 204 | } 205 | // Check for the stream:features tag 206 | List register = bodyWrap.findAllElements("register").toList(); 207 | List mechanisms = 208 | bodyWrap.findAllElements("mechanism").toList(); 209 | if (register.length == 0 && mechanisms.length == 0) { 210 | conn.noAuthReceived(_callback); 211 | return false; 212 | } 213 | 214 | if (register.length == 0) { 215 | conn.changeConnectStatus(Strophe.Status['REGIFAIL'], null); 216 | return true; 217 | } 218 | 219 | // send a get request for registration, to get all required data fields 220 | conn.addSysHandler(this._get_register_cb, null, "iq", null, null); 221 | conn.sendIQ(Strophe.$iq({'type': "get"}).c( 222 | "query", {'xmlns': Strophe.NS['REGISTER']}).tree()); 223 | 224 | return true; 225 | } 226 | 227 | /** PrivateFunction: _get_register_cb 228 | * _Private_ handler for Registration Fields Request. 229 | * 230 | * Parameters: 231 | * (XMLElement) elem - The query stanza. 232 | * 233 | * Returns: 234 | * false to remove SHOULD contain the registration information currentlSHOULD contain the registration information currentlSHOULD contain the registration information currentlthe handler. 235 | */ 236 | _get_register_cb(dynamic elem) { 237 | XmlElement field; 238 | List queries; 239 | StropheConnection conn = this.connection; 240 | XmlElement stanza; 241 | if (elem is XmlDocument) 242 | stanza = elem.rootElement; 243 | else if (elem is XmlElement) 244 | stanza = elem; 245 | else 246 | stanza = elem; 247 | queries = stanza.findAllElements("query").toList(); 248 | 249 | if (queries.length != 1) { 250 | conn.changeConnectStatus(Strophe.Status['REGIFAIL'], "unknown"); 251 | return false; 252 | } 253 | XmlElement query = queries.first; 254 | // get required fields 255 | for (int i = 0; i < query.children.length; i++) { 256 | field = query.children[i]; 257 | if (field.name.qualified.toLowerCase() == 'instructions') { 258 | // this is a special element 259 | // it provides info about given data fields in a textual way. 260 | conn.register.instructions = Strophe.getText(field); 261 | continue; 262 | } else if (field.name.qualified.toLowerCase() == 'x') { 263 | // ignore x for now 264 | continue; 265 | } 266 | conn.register.fields[field.name.qualified.toLowerCase()] = 267 | Strophe.getText(field); 268 | } 269 | conn.changeConnectStatus(Strophe.Status['REGISTER'], null); 270 | return false; 271 | } 272 | 273 | /** Function: submit 274 | * Submits Registration data. 275 | * 276 | * As the registration process proceeds, the user supplied callback will 277 | * be triggered with status code Strophe.Status['REGISTER']. At this point 278 | * the user should fill all required fields in connection['REGISTER'].fields 279 | * and invoke this function to procceed in the registration process. 280 | */ 281 | submit() { 282 | String name; 283 | List fields; 284 | StropheConnection conn = this.connection; 285 | StanzaBuilder query = Strophe 286 | .$iq({'type': "set"}).c("query", {'xmlns': Strophe.NS['REGISTER']}); 287 | // set required fields 288 | fields = this.fields.keys.toList(); 289 | for (int i = 0; i < fields.length; i++) { 290 | name = fields[i]; 291 | query.c(name).t(this.fields[name]).up(); 292 | } 293 | 294 | // providing required information 295 | conn.addSysHandler(this._submit_cb, null, "iq", null, null); 296 | conn.sendIQ(query.tree()); 297 | } 298 | 299 | /** PrivateFunction: _submit_cb 300 | * _Private_ handler for submitted registration information. 301 | * 302 | * Parameters: 303 | * (XMLElement) elem - The query stanza. 304 | * 305 | * Returns: 306 | * false to remove the handler. 307 | */ 308 | _submit_cb(XmlElement stanza) { 309 | XmlElement field; 310 | List errors; 311 | List queries; 312 | StropheConnection conn = this.connection; 313 | 314 | queries = stanza.findAllElements("query").toList(); 315 | if (queries.length > 0) { 316 | XmlElement query = queries[0]; 317 | // update fields 318 | for (int i = 0; i < query.children.length; i++) { 319 | field = query.children[i]; 320 | if (field.name.qualified.toLowerCase() == 'instructions') { 321 | // this is a special element 322 | // it provides info about given data fields in a textual way 323 | this.instructions = Strophe.getText(field); 324 | continue; 325 | } 326 | this.fields[field.name.qualified.toLowerCase()] = 327 | Strophe.getText(field); 328 | } 329 | } 330 | 331 | if (stanza.getAttribute("type") == "error") { 332 | errors = stanza.findAllElements("error").toList(); 333 | if (errors.length != 1) { 334 | conn.changeConnectStatus(Strophe.Status['REGIFAIL'], "unknown"); 335 | return false; 336 | } 337 | 338 | Strophe.info("Registration failed."); 339 | 340 | // this is either 'conflict' or 'not-acceptable' 341 | XmlElement firstChild = errors[0].firstChild as XmlElement; 342 | String error = firstChild.name.qualified.toLowerCase(); 343 | if (error == 'conflict') { 344 | conn.changeConnectStatus(Strophe.Status['CONFLICT'], error); 345 | } else if (error == 'not-acceptable') { 346 | conn.changeConnectStatus(Strophe.Status['NOTACCEPTABLE'], error); 347 | } else { 348 | String text = 349 | Strophe.getText(errors[0].findElements('text').toList()[0]) + 350 | '/$error'; 351 | conn.changeConnectStatus(Strophe.Status['REGIFAIL'], text ?? error); 352 | } 353 | } else { 354 | Strophe.info("Registration successful."); 355 | 356 | conn.changeConnectStatus(Strophe.Status['REGISTERED'], null); 357 | } 358 | 359 | return false; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /lib/src/plugins/roster.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | import 'package:xml/xml/nodes/element.dart'; 6 | 7 | class RosterPlugin extends PluginClass { 8 | List _callbacks; 9 | 10 | List _callbacksRequest; 11 | 12 | List items; 13 | 14 | String ver; 15 | 16 | init(StropheConnection conn) { 17 | this.connection = conn; 18 | this._callbacks = []; 19 | this._callbacksRequest = []; 20 | 21 | /** Property: items 22 | * Roster items 23 | * [ 24 | * { 25 | * name : "", 26 | * jid : "", 27 | * subscription : "", 28 | * ask : "", 29 | * groups : ["", ""], 30 | * resources : { 31 | * myresource : { 32 | * show : "", 33 | * status : "", 34 | * priority : "" 35 | * } 36 | * } 37 | * } 38 | * ] 39 | */ 40 | 41 | /** Property: ver 42 | * current roster revision 43 | * always null if server doesn't support xep 237 44 | */ 45 | 46 | // Override the connect and attach methods to always add presence and roster handlers. 47 | // They are removed when the connection disconnects, so must be added on connection. 48 | Function oldCallback; 49 | Function _connect = conn.connect; 50 | Function _attach = conn.attach; 51 | Function newCallback = (int status, condition, ele) { 52 | if (status == Strophe.Status['ATTACHED'] || 53 | status == Strophe.Status['CONNECTED']) { 54 | try { 55 | // Presence subscription 56 | conn.addHandler( 57 | this._onReceivePresence, null, 'presence', null, null, null); 58 | conn.addHandler( 59 | this._onReceiveIQ, Strophe.NS['ROSTER'], 'iq', "set", null, null); 60 | } catch (e) { 61 | Strophe.error(e); 62 | } 63 | } 64 | if (oldCallback != null && oldCallback is Function) { 65 | oldCallback(status, condition, ele); 66 | } 67 | }; 68 | conn.connect = (String jid, String pass, Function callback, 69 | [int wait, int hold, String route, String authcid]) { 70 | oldCallback = callback; 71 | callback = newCallback; 72 | _connect(jid, pass, callback, wait, hold, route, authcid); 73 | }; 74 | conn.attach = (String jid, String sid, int rid, Function callback, int wait, 75 | int hold, int wind) { 76 | oldCallback = callback; 77 | callback = newCallback; 78 | _attach(jid, sid, rid, callback, wait, hold, wind); 79 | }; 80 | 81 | Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver'); 82 | Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); 83 | } 84 | 85 | /** Function: supportVersioning 86 | * return true if roster versioning is enabled on server 87 | */ 88 | bool supportVersioning() { 89 | return (this.connection.features != null && 90 | this.connection.features.findAllElements('ver').length > 0); 91 | } 92 | 93 | /** Function: get 94 | * Get Roster on server 95 | * 96 | * Parameters: 97 | * (Function) userCallback - callback on roster result 98 | * (String) ver - current rev of roster 99 | * (only used if roster versioning is enabled) 100 | * (Array) items - initial items of ver 101 | * (only used if roster versioning is enabled) 102 | * In browser context you can use sessionStorage 103 | * to store your roster in json (JSON.stringify()) 104 | */ 105 | get(Function userCallback, [String ver, List items]) { 106 | Map attrs = {'xmlns': Strophe.NS['ROSTER']}; 107 | this.items = []; 108 | if (this.supportVersioning()) { 109 | // empty rev because i want an rev attribute in the result 110 | attrs['ver'] = ver ?? ''; 111 | this.items = items ?? []; 112 | } 113 | StanzaBuilder iq = Strophe 114 | .$iq({'type': 'get', 'id': this.connection.getUniqueId('roster')}).c( 115 | 'query', attrs); 116 | return this.connection.sendIQ(iq.tree(), (XmlElement stanza) { 117 | this._onReceiveRosterSuccess(userCallback, stanza); 118 | }, (XmlElement stanza) { 119 | this._onReceiveRosterError(userCallback, stanza); 120 | }); 121 | } 122 | 123 | /** Function: registerCallback 124 | * register callback on roster (presence and iq) 125 | * 126 | * Parameters: 127 | * (Function) callback 128 | */ 129 | registerCallback(Function callback) { 130 | if (callback != null) this._callbacks.add(callback); 131 | } 132 | 133 | registerRequestCallback(Function callback) { 134 | if (callback != null) this._callbacksRequest.add(callback); 135 | } 136 | 137 | /** Function: findItem 138 | * Find item by JID 139 | * 140 | * Parameters: 141 | * (String) jid 142 | */ 143 | RosterItem findItem(String jid) { 144 | if (this.items != null) { 145 | for (int i = 0; i < this.items.length; i++) { 146 | if (this.items[i] != null && this.items[i].jid == jid) { 147 | return this.items[i]; 148 | } 149 | } 150 | } 151 | return null; 152 | } 153 | 154 | /** Function: removeItem 155 | * Remove item by JID 156 | * 157 | * Parameters: 158 | * (String) jid 159 | */ 160 | bool removeItem(String jid) { 161 | for (int i = 0; i < this.items.length; i++) { 162 | if (this.items[i] != null && this.items[i].jid == jid) { 163 | this.items.remove(i); 164 | return true; 165 | } 166 | } 167 | return false; 168 | } 169 | 170 | /** Function: subscribe 171 | * Subscribe presence 172 | * 173 | * Parameters: 174 | * (String) jid 175 | * (String) message (optional) 176 | * (String) nick (optional) 177 | */ 178 | subscribe(String jid, [String message, String nick]) { 179 | StanzaBuilder pres = Strophe.$pres({'to': jid, 'type': "subscribe"}); 180 | if (message != null && message != "") { 181 | pres.c("status").t(message).up(); 182 | } 183 | if (nick != null && nick != "") { 184 | pres.c('nick', {'xmlns': Strophe.NS['NICK']}).t(nick).up(); 185 | } 186 | this.connection.send(pres); 187 | } 188 | 189 | /** Function: unsubscribe 190 | * Unsubscribe presence 191 | * 192 | * Parameters: 193 | * (String) jid 194 | * (String) message 195 | */ 196 | unsubscribe(String jid, [String message]) { 197 | StanzaBuilder pres = Strophe.$pres({'to': jid, 'type': "unsubscribe"}); 198 | if (message != null && message != "") pres.c("status").t(message); 199 | this.connection.send(pres); 200 | } 201 | 202 | /** Function: authorize 203 | * Authorize presence subscription 204 | * 205 | * Parameters: 206 | * (String) jid 207 | * (String) message 208 | */ 209 | authorize(String jid, [String message]) { 210 | StanzaBuilder pres = Strophe.$pres({'to': jid, 'type': "subscribed"}); 211 | if (message != null && message != "") pres.c("status").t(message); 212 | this.connection.send(pres); 213 | } 214 | 215 | /** Function: unauthorize 216 | * Unauthorize presence subscription 217 | * 218 | * Parameters: 219 | * (String) jid 220 | * (String) message 221 | */ 222 | unauthorize(String jid, [String message]) { 223 | StanzaBuilder pres = Strophe.$pres({'to': jid, 'type': "unsubscribed"}); 224 | if (message != null && message != "") pres.c("status").t(message); 225 | this.connection.send(pres); 226 | } 227 | 228 | /** Function: add 229 | * Add roster item 230 | * 231 | * Parameters: 232 | * (String) jid - item jid 233 | * (String) name - name 234 | * (Array) groups 235 | * (Function) callback 236 | */ 237 | add(String jid, String name, [List groups, Function callback]) { 238 | StanzaBuilder iq = Strophe 239 | .$iq({'type': 'set'}).c('query', {'xmlns': Strophe.NS['ROSTER']}).c( 240 | 'item', {'jid': jid, 'name': name}); 241 | if (groups != null) { 242 | for (int i = 0; i < groups.length; i++) { 243 | iq.c('group').t(groups[i]).up(); 244 | } 245 | } 246 | this.connection.sendIQ(iq.tree(), callback, callback); 247 | } 248 | 249 | /** Function: update 250 | * Update roster item 251 | * 252 | * Parameters: 253 | * (String) jid - item jid 254 | * (String) name - name 255 | * (Array) groups 256 | * (Function) callback 257 | */ 258 | update(String jid, String name, [List groups, Function callback]) { 259 | RosterItem item = this.findItem(jid); 260 | if (item == null) { 261 | throw "item not found"; 262 | } 263 | String newName = name ?? item.name; 264 | List newGroups = groups ?? item.groups; 265 | StanzaBuilder iq = Strophe 266 | .$iq({'type': 'set'}).c('query', {'xmlns': Strophe.NS['ROSTER']}).c( 267 | 'item', {'jid': item.jid, 'name': newName}); 268 | for (int i = 0; i < newGroups.length; i++) { 269 | iq.c('group').t(newGroups[i]).up(); 270 | } 271 | return this.connection.sendIQ(iq.tree(), callback, callback); 272 | } 273 | 274 | /** Function: remove 275 | * Remove roster item 276 | * 277 | * Parameters: 278 | * (String) jid - item jid 279 | * (Function) callback 280 | */ 281 | remove(String jid, [Function callback]) { 282 | RosterItem item = this.findItem(jid); 283 | if (item == null) { 284 | throw "item not found"; 285 | } 286 | StanzaBuilder iq = Strophe 287 | .$iq({'type': 'set'}).c('query', {'xmlns': Strophe.NS['ROSTER']}).c( 288 | 'item', {'jid': item.jid, 'subscription': "remove"}); 289 | this.connection.sendIQ(iq.tree(), callback, callback); 290 | } 291 | 292 | /** PrivateFunction: _onReceiveRosterSuccess 293 | * 294 | */ 295 | _onReceiveRosterSuccess(Function userCallback, XmlElement stanza) { 296 | this._updateItems(stanza); 297 | this._call_backs(this.items); 298 | if (userCallback != null) { 299 | userCallback(this.items); 300 | } 301 | } 302 | 303 | /** PrivateFunction: _onReceiveRosterError 304 | * 305 | */ 306 | _onReceiveRosterError(Function userCallback, XmlElement stanza) { 307 | userCallback(this.items); 308 | } 309 | 310 | /** PrivateFunction: _onReceivePresence 311 | * Handle presence 312 | */ 313 | _onReceivePresence(xml.XmlElement presence) { 314 | // TODO: from is optional 315 | String jid = presence.getAttribute('from'); 316 | String from = Strophe.getBareJidFromJid(jid); 317 | RosterItem item = this.findItem(from); 318 | String type = presence.getAttribute('type'); 319 | // not in roster 320 | if (item == null) { 321 | // if 'friend request' presence 322 | if (type == 'subscribe') { 323 | this._call_backs_request(from); 324 | } 325 | return true; 326 | } 327 | if (type == 'unavailable') { 328 | item.resources.remove(Strophe.getResourceFromJid(jid)); 329 | } else if (type == null || type == '') { 330 | // TODO: add timestamp 331 | item.resources[Strophe.getResourceFromJid(jid)] = { 332 | 'show': (presence.findAllElements('show').length > 0) 333 | ? Strophe.getText(presence.findAllElements('show').toList()[0]) 334 | : "", 335 | 'status': (presence.findAllElements('status').length > 0) 336 | ? Strophe.getText(presence.findAllElements('status').toList()[0]) 337 | : "", 338 | 'priority': (presence.findAllElements('priority').length > 0) 339 | ? Strophe.getText(presence.findAllElements('priority').toList()[0]) 340 | : "" 341 | }; 342 | } else { 343 | // Stanza is not a presence notification. (It's probably a subscription type stanza.) 344 | return true; 345 | } 346 | this._call_backs(this.items, item); 347 | return true; 348 | } 349 | 350 | /** PrivateFunction: _call_backs_request 351 | * call all the callbacks waiting for 'friend request' presences 352 | */ 353 | _call_backs_request(String from) { 354 | for (int i = 0; i < this._callbacksRequest.length; i++) { 355 | this._callbacksRequest[i](from); 356 | } 357 | } 358 | 359 | /** PrivateFunction: _call_backs 360 | * first parameter is the full roster 361 | * second is optional, newly added or updated item 362 | * third is otional, in case of update, send the previous state of the 363 | * update item 364 | */ 365 | _call_backs(List items, [item, previousItem]) { 366 | for (int i = 0; i < this._callbacks.length; i++) // [].forEach my love ... 367 | { 368 | this._callbacks[i](items, item, previousItem); 369 | } 370 | } 371 | 372 | /** PrivateFunction: _onReceiveIQ 373 | * Handle roster push. 374 | */ 375 | _onReceiveIQ(xml.XmlElement iq) { 376 | String id = iq.getAttribute('id'); 377 | String from = iq.getAttribute('from'); 378 | // Receiving client MUST ignore stanza unless it has no from or from = user's JID. 379 | if (from != null && 380 | from != "" && 381 | from != this.connection.jid && 382 | from != Strophe.getBareJidFromJid(this.connection.jid)) return true; 383 | StanzaBuilder iqresult = 384 | Strophe.$iq({'type': 'result', 'id': id, 'from': this.connection.jid}); 385 | this.connection.send(iqresult); 386 | this._updateItems(iq); 387 | return true; 388 | } 389 | 390 | /** PrivateFunction: _updateItems 391 | * Update items from iq 392 | */ 393 | _updateItems(xml.XmlElement iq) { 394 | List queries = iq.findAllElements('query').toList(); 395 | if (queries.length != 0) { 396 | xml.XmlElement query = queries[0]; 397 | if (query == null) return; 398 | List listItem = query.findAllElements('item').toList(); 399 | if (listItem.length > 0) { 400 | xml.XmlElement item = listItem[0]; 401 | this.ver = item.getAttribute('ver'); 402 | } 403 | Strophe.forEachChild(query, 'item', (rosterItem) { 404 | this._updateItem(rosterItem); 405 | }); 406 | } 407 | } 408 | 409 | /** PrivateFunction: _updateItem 410 | * Update internal representation of roster item 411 | */ 412 | _updateItem(xml.XmlElement itemTag) { 413 | if (itemTag == null) return; 414 | String jid = itemTag.getAttribute("jid"); 415 | String name = itemTag.getAttribute("name"); 416 | String subscription = itemTag.getAttribute("subscription"); 417 | String ask = itemTag.getAttribute("ask"); 418 | List groups = []; 419 | 420 | Strophe.forEachChild(itemTag, 'group', (group) { 421 | groups.add(Strophe.getText(group)); 422 | }); 423 | 424 | if (subscription == "remove") { 425 | bool hashBeenRemoved = this.removeItem(jid); 426 | if (hashBeenRemoved) { 427 | this._call_backs(this.items, {'jid': jid, 'subscription': 'remove'}); 428 | } 429 | return; 430 | } 431 | 432 | RosterItem item = this.findItem(jid); 433 | RosterItem previousItem; 434 | if (item == null) { 435 | item = new RosterItem.fromMap({ 436 | 'name': name, 437 | 'jid': jid, 438 | 'subscription': subscription, 439 | 'ask': ask, 440 | 'groups': groups, 441 | 'resources': {} 442 | }); 443 | if (this.items != null) { 444 | this.items.add(item); 445 | } 446 | } else { 447 | previousItem = new RosterItem.fromMap({ 448 | 'name': item.name, 449 | 'subscription': item.subscription, 450 | 'ask': item.ask, 451 | 'groups': item.groups 452 | }); 453 | item.name = name; 454 | item.subscription = subscription; 455 | item.ask = ask; 456 | item.groups = groups; 457 | } 458 | this._call_backs(this.items, item, previousItem); 459 | } 460 | } 461 | 462 | class RosterItem { 463 | String name; 464 | String jid; 465 | String subscription; 466 | String ask; 467 | List groups; 468 | Map resources; 469 | RosterItem.fromMap(Map map) { 470 | this.name = map['name'] ?? ''; 471 | this.jid = map['jid'] ?? ''; 472 | this.subscription = map['subscription'] ?? ''; 473 | this.ask = map['ask'] ?? ''; 474 | this.groups = map['groups'] ?? []; 475 | this.resources = map['resources'] ?? {}; 476 | } 477 | /* { 478 | myresource : { 479 | show : "", 480 | status : "", 481 | priority : "" 482 | } 483 | } */ 484 | } 485 | -------------------------------------------------------------------------------- /lib/src/plugins/vcard-temp.dart: -------------------------------------------------------------------------------- 1 | import 'package:strophe/src/core.dart'; 2 | import 'package:strophe/src/enums.dart'; 3 | import 'package:strophe/src/plugins/plugins.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | import 'package:xml/xml.dart'; 6 | 7 | class VCardTemp extends PluginClass { 8 | StanzaBuilder _buildIq(String type, String jid, [xml.XmlElement vCardEl]) { 9 | StanzaBuilder iq = 10 | Strophe.$iq(jid != null ? {'type': type, 'to': jid} : {'type': type}); 11 | 12 | if (vCardEl != null) { 13 | iq.cnode(vCardEl); 14 | } else 15 | iq.c("vCard", {'xmlns': Strophe.NS['VCARD']}); 16 | return iq; 17 | } 18 | 19 | init(StropheConnection conn) { 20 | this.connection = conn; 21 | return Strophe.addNamespace('VCARD', 'vcard-temp'); 22 | } 23 | 24 | /* Function 25 | * Retrieve a vCard for a JID/Entity 26 | * Parameters: 27 | * (Function) handler_cb - The callback function used to handle the request. 28 | * (String) jid - optional - The name of the entity to request the vCard 29 | * If no jid is given, this function retrieves the current user's vcard. 30 | * */ 31 | get(Function handlerCb, String jid, Function errorCb) { 32 | var iq = 33 | _buildIq("get", jid ?? Strophe.getBareJidFromJid(this.connection.jid)); 34 | return this.connection.sendIQ(iq.tree(), handlerCb, errorCb); 35 | } 36 | 37 | /* Function 38 | * Set an entity's vCard. 39 | */ 40 | set(Function handlerCb, VCardEl vCardEl, String jid, Function errorCb) { 41 | if (vCardEl == null) return null; 42 | StanzaBuilder iq = _buildIq("set", 43 | jid ?? Strophe.getBareJidFromJid(this.connection.jid), vCardEl.tree()); 44 | return this.connection.sendIQ(iq.tree(), handlerCb, errorCb); 45 | } 46 | } 47 | 48 | class VCardEl { 49 | String FN = ''; 50 | String FAMILY = ''; 51 | String GIVEN = ''; 52 | String MIDDLE = ''; 53 | String NICKNAME = ''; 54 | String URL = ''; 55 | String BDAY = ''; 56 | String ORGNAME = ''; 57 | String ORGUNIT = ''; 58 | String TITLE = ''; 59 | String ROLE = ''; 60 | String USERID = ''; 61 | String JABBERID = ''; 62 | String DESC = ''; 63 | String EMAIL = ''; 64 | List _addresses = []; 65 | List get addresses { 66 | return _addresses; 67 | } 68 | 69 | set addresses(List addr) { 70 | if (addr != null) _addresses = addr; 71 | } 72 | 73 | VCardEl( 74 | {String fn, 75 | String family, 76 | String given, 77 | String middle, 78 | String nickName, 79 | String url, 80 | String bday, 81 | String orgName, 82 | String orgUnit, 83 | String title, 84 | String role, 85 | String userId, 86 | String jabberdId, 87 | String desc, 88 | String email}) { 89 | FN = fn ?? ''; 90 | FAMILY = family ?? ''; 91 | GIVEN = given ?? ''; 92 | MIDDLE = middle ?? ''; 93 | NICKNAME = nickName ?? ''; 94 | URL = url ?? ''; 95 | EMAIL = email ?? ''; 96 | BDAY = bday ?? ''; 97 | ORGNAME = orgName ?? ''; 98 | ORGUNIT = orgUnit ?? ''; 99 | TITLE = title ?? ''; 100 | ROLE = role ?? ''; 101 | USERID = userId ?? ''; 102 | JABBERID = jabberdId ?? ''; 103 | DESC = desc ?? ''; 104 | } 105 | XmlElement tree() { 106 | StanzaBuilder build = 107 | Strophe.$build("vCard", {'xmlns': Strophe.NS['VCARD']}) 108 | .c('FN') 109 | .t(FN) 110 | .up() 111 | .c('N') 112 | .c('FAMILY') 113 | .t(FAMILY) 114 | .up() 115 | .c('GIVEN') 116 | .t(GIVEN) 117 | .up() 118 | .c('MIDDLE') 119 | .t(MIDDLE) 120 | .up() 121 | .up() 122 | .c('NICKNAME') 123 | .t(NICKNAME) 124 | .up() 125 | .c('URL') 126 | .t(URL) 127 | .up() 128 | .c('EMAIL') 129 | .t(EMAIL) 130 | .up() 131 | .c('BDAY') 132 | .t(BDAY) 133 | .up() 134 | .c('ORG') 135 | .c('ORGNAME') 136 | .t(ORGNAME) 137 | .up() 138 | .c('ORGUNIT') 139 | .t(ORGUNIT) 140 | .up() 141 | .up() 142 | .c('TITLE') 143 | .t(TITLE) 144 | .up() 145 | .c('ROLE') 146 | .t(ROLE) 147 | .up(); 148 | addresses.forEach((VCardElAddr addr) { 149 | if (addr != null) { 150 | addr.tree().children.forEach((XmlNode elem) { 151 | build.cnode(elem).up(); 152 | }); 153 | } 154 | }); 155 | build 156 | .c('EMAIL') 157 | .c('INTERNET') 158 | .t(EMAIL) 159 | .up() 160 | .c('PREF') 161 | .t(EMAIL) 162 | .up() 163 | .c('USERID') 164 | .t(USERID) 165 | .up() 166 | .up() 167 | .c('JABBERID') 168 | .t(JABBERID) 169 | .c('DESC') 170 | .t(DESC); 171 | print(build.tree()); 172 | return build.tree(); 173 | } 174 | } 175 | 176 | class VCardElAddr { 177 | String VOICE_NUMBER = ''; 178 | String FAX_NUMBER = ''; 179 | String MSG_NUMBER = ''; 180 | String WORK = ''; 181 | String EXTADD = ''; 182 | String STREET = ''; 183 | String LOCALITY = ''; 184 | String REGION = ''; 185 | String PCODE = ''; 186 | String CTRY = ''; 187 | String typeAddr; 188 | VCardElAddr(this.typeAddr, 189 | {String voiceNum, 190 | String faxNum, 191 | String msgNum, 192 | String work, 193 | String extAddr, 194 | String street, 195 | String locality, 196 | String region, 197 | String pCode, 198 | String country}) { 199 | VOICE_NUMBER = voiceNum ?? ''; 200 | FAX_NUMBER = faxNum ?? ''; 201 | MSG_NUMBER = msgNum ?? ''; 202 | WORK = work ?? ''; 203 | EXTADD = extAddr ?? ''; 204 | STREET = street ?? ''; 205 | LOCALITY = locality ?? ''; 206 | REGION = region ?? ''; 207 | PCODE = pCode ?? ''; 208 | CTRY = country ?? ''; 209 | } 210 | XmlElement tree() { 211 | return Strophe.$build('addr', {}) 212 | .c('TEL') 213 | .c(typeAddr != null ? typeAddr.toUpperCase() : 'WORK') 214 | .up() 215 | .c('VOICE') 216 | .up() 217 | .c('NUMBER') 218 | .t(VOICE_NUMBER) 219 | .up() 220 | .up() 221 | .c('TEL') 222 | .c(typeAddr != null ? typeAddr.toUpperCase() : 'WORK') 223 | .up() 224 | .c('FAX') 225 | .up() 226 | .c('NUMBER') 227 | .t(FAX_NUMBER) 228 | .up() 229 | .up() 230 | .c('TEL') 231 | .c(typeAddr != null ? typeAddr.toUpperCase() : 'WORK') 232 | .up() 233 | .c('MSG') 234 | .up() 235 | .c('NUMBER') 236 | .t(MSG_NUMBER) 237 | .up() 238 | .up() 239 | .c('ADR') 240 | .c('WORK') 241 | .t(WORK) 242 | .up() 243 | .c('EXTADD') 244 | .t(EXTADD) 245 | .up() 246 | .c('STREET') 247 | .t(STREET) 248 | .up() 249 | .c('LOCALITY') 250 | .t(LOCALITY) 251 | .up() 252 | .c('REGION') 253 | .t(REGION) 254 | .up() 255 | .c('PCODE') 256 | .t(PCODE) 257 | .up() 258 | .c('CTRY') 259 | .t(CTRY) 260 | .up() 261 | .tree(); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/src/sessionstorage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class SessionStorage { 4 | static Map _session = {}; 5 | static List _cookie = []; 6 | List get cookies { 7 | return _cookie; 8 | } 9 | 10 | Map get session { 11 | return _session; 12 | } 13 | 14 | static void addCookie(Cookie newCookie) { 15 | _cookie.add(newCookie); 16 | } 17 | 18 | static void clearCookie() { 19 | _cookie.clear(); 20 | } 21 | 22 | static void removeCookie(Cookie removedCookie) { 23 | _cookie.remove(removedCookie); 24 | } 25 | 26 | static void removeCookieAt(int index) { 27 | _cookie.removeAt(index); 28 | } 29 | 30 | static Cookie getCookie(int index) { 31 | return _cookie.elementAt(index); 32 | } 33 | 34 | static void setItem(String name, String value) { 35 | if (name == null || name.isEmpty || name.trim().length == 0) return; 36 | if (_session.containsKey(name)) { 37 | _session.update(name, (String str) { 38 | return value; 39 | }); 40 | } else { 41 | _session.addAll({name: value}); 42 | } 43 | } 44 | 45 | static String getItem(String name) { 46 | return _session[name]; 47 | } 48 | 49 | static void clear(String name) { 50 | _session.clear(); 51 | } 52 | 53 | static void removeItem(String name) { 54 | _session.remove(name); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/sha1.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:crypto/crypto.dart'; 4 | 5 | class SHA1 { 6 | static List coreSha1(String s, int len) { 7 | var bytes = utf8.encode(s); // data being hashed 8 | Digest digest = sha1.convert(bytes); 9 | return digest.bytes; 10 | } 11 | 12 | /* 13 | * Perform the appropriate triplet combination function for the current 14 | * iteration 15 | 16 | * Calculate the HMAC-SHA1 of a key and some data 17 | */ 18 | static List coreHmacSha1(String cle, String data) { 19 | List key = utf8.encode(cle); 20 | List bytes = utf8.encode(data); 21 | 22 | Hmac hmacSha1 = new Hmac(sha1, key); // HMAC-SHA1 23 | Digest digest = hmacSha1.convert(bytes); 24 | return digest.bytes; 25 | } 26 | 27 | /* 28 | * Convert an array of big-endian words to a string 29 | */ 30 | static String binb2str(List bytes) { 31 | return String.fromCharCodes(bytes); 32 | } 33 | 34 | /* 35 | * Convert an array of big-endian words to a base-64 string 36 | */ 37 | static String binb2b64(List binarray) { 38 | return base64.encode(binarray); 39 | } 40 | 41 | static String b64HmacSha1(String key, String data) { 42 | return binb2b64(coreHmacSha1(key, data)); 43 | } 44 | 45 | static String b64Sha1(String s) { 46 | return binb2b64(coreSha1(s, s.length * 8)); 47 | } 48 | 49 | static String strHmacSha1(String key, String data) { 50 | return binb2str(coreHmacSha1(key, data)); 51 | } 52 | 53 | static String strSha1(String s) { 54 | return binb2str(coreSha1(s, s.length * 8)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:strophe/src/enums.dart'; 4 | import 'package:xml/xml/nodes/element.dart'; 5 | 6 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 7 | // for details. All rights reserved. Use of this source code is governed by a 8 | // BSD-style license that can be found in the LICENSE file. 9 | 10 | /// A bitmask that limits an integer to 32 bits. 11 | const mask32 = 0xFFFFFFFF; 12 | 13 | /// The number of bits in a byte. 14 | const bitsPerByte = 8; 15 | 16 | /// The number of bytes in a 32-bit word. 17 | const bytesPerWord = 4; 18 | 19 | /// Adds [x] and [y] with 32-bit overflow semantics. 20 | int add32(int x, int y) => (x + y) & mask32; 21 | 22 | /// Bitwise rotates [val] to the left by [shift], obeying 32-bit overflow 23 | /// semantics. 24 | int rotl32(int val, int shift) { 25 | var modShift = shift & 31; 26 | return ((val << modShift) & mask32) | ((val & mask32) >> (32 - modShift)); 27 | } 28 | 29 | class Utils { 30 | static String utf16to8(String str) { 31 | int c; 32 | String out = ""; 33 | int len = str.length; 34 | for (int i = 0; i < len; i++) { 35 | c = str.codeUnitAt(i); 36 | if ((c >= 0x0000) && (c <= 0x007F)) { 37 | out += new String.fromCharCode(str.codeUnitAt(i)); 38 | } else if (c > 0x07FF) { 39 | out += new String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); 40 | out += new String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); 41 | out += new String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); 42 | } else { 43 | out += new String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); 44 | out += new String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); 45 | } 46 | } 47 | return out; 48 | } 49 | 50 | static List addCookies(Map cookies) { 51 | String cookieName; 52 | dynamic cookieObj; 53 | bool isObj; 54 | String cookieValue; 55 | DateTime expires; 56 | String domain; 57 | String path; 58 | List allCookies = []; 59 | cookies = cookies ?? {}; 60 | cookies.forEach((String key, dynamic value) { 61 | expires = null; 62 | domain = ''; 63 | path = ''; 64 | cookieObj = value; 65 | isObj = cookieObj is String ? false : true; 66 | cookieValue = Uri.encodeFull(isObj ? cookieObj.value : cookieObj); 67 | if (isObj) { 68 | expires = cookieObj.expires ?? null; 69 | domain = cookieObj.domain ?? ''; 70 | path = cookieObj.path ?? ''; 71 | } 72 | Cookie cookie = new Cookie(cookieName, cookieValue); 73 | cookie.domain = domain; 74 | cookie.expires = expires; 75 | cookie.path = path; 76 | allCookies.add(cookie); 77 | }); 78 | return allCookies; 79 | } 80 | } 81 | 82 | typedef void ConnectCallBack(int status, dynamic condition, dynamic elem); 83 | typedef void XmlInputCallback(XmlElement elem); 84 | typedef void RawInputCallback(String elem); 85 | typedef void ConnexionCallback(req, Function _callback, String raw); 86 | typedef void AuthenticateCallback(List matched); 87 | -------------------------------------------------------------------------------- /lib/src/websocket.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:strophe/src/core.dart'; 5 | import 'package:strophe/src/enums.dart'; 6 | import 'package:xml/xml.dart' as xml; 7 | 8 | class StropheWebSocket extends ServiceType { 9 | StropheConnection _conn; 10 | 11 | String strip; 12 | 13 | WebSocket socket; 14 | 15 | StreamSubscription _socketListen; 16 | 17 | StreamSubscription get socketListen { 18 | return _socketListen; 19 | } 20 | 21 | set socketListen(StreamSubscription listen) { 22 | if (listen != null) _socketListen = listen; 23 | } 24 | 25 | StropheWebSocket(StropheConnection connection) { 26 | this._conn = connection; 27 | this.strip = "wrapper"; 28 | 29 | //String service = connection.service; 30 | //if (service.indexOf("ws:") != 0 && service.indexOf("wss:") != 0) { 31 | // If the service is not an absolute URL, assume it is a path and put the absolute 32 | // URL together from options, current URL and the path. 33 | /* var new_service = ""; */ 34 | 35 | /* if (connection.options['protocol'] == "ws" && window.location.protocol != "https:") { 36 | new_service += "ws"; 37 | } else { 38 | new_service += "wss"; 39 | } 40 | 41 | new_service += "://" + service; 42 | 43 | if (service.indexOf("/") != 0) { 44 | new_service += window.location.pathname + service; 45 | } else { 46 | new_service += service; 47 | } */ 48 | 49 | //connection.service = new_service; 50 | //} 51 | } 52 | /** PrivateFunction: _buildStream 53 | * _Private_ helper function to generate the start tag for WebSockets 54 | * 55 | * Returns: 56 | * A Strophe.Builder with a element. 57 | */ 58 | StanzaBuilder _buildStream() { 59 | return Strophe.$build("open", { 60 | "xmlns": Strophe.NS['FRAMING'], 61 | "to": this._conn.domain, 62 | "version": '1.0' 63 | }); 64 | } 65 | 66 | /** PrivateFunction: _check_streamerror 67 | * _Private_ checks a message for stream:error 68 | * 69 | * Parameters: 70 | * (Strophe.Request) bodyWrap - The received stanza. 71 | * connectstatus - The ConnectStatus that will be set on error. 72 | * Returns: 73 | * true if there was a streamerror, false otherwise. 74 | */ 75 | bool _checkStreamError(xml.XmlNode bodyWrapNode, int connectstatus) { 76 | Iterable errors; 77 | xml.XmlElement bodyWrap; 78 | if (bodyWrapNode is xml.XmlDocument) 79 | bodyWrap = bodyWrapNode.rootElement; 80 | else if (bodyWrapNode is xml.XmlElement) bodyWrap = bodyWrapNode; 81 | 82 | if (bodyWrap.getAttribute("xmlns") == Strophe.NS['STREAM']) { 83 | errors = bodyWrap.findAllElements("error"); 84 | } else { 85 | errors = bodyWrap.findAllElements("stream:error"); 86 | } 87 | if (errors.length == 0) { 88 | return false; 89 | } 90 | xml.XmlElement error = errors.elementAt(0); 91 | 92 | String condition = ""; 93 | String text = ""; 94 | 95 | String ns = "urn:ietf:params:xml:ns:xmpp-streams"; 96 | xml.XmlElement e; 97 | for (int i = 0; i < error.children.length; i++) { 98 | e = error.children.elementAt(i) as xml.XmlElement; 99 | if (e.getAttribute("xmlns") != ns) { 100 | break; 101 | } 102 | if (e.name.qualified == "text") { 103 | text = e.text; 104 | } else { 105 | condition = e.name.qualified; 106 | } 107 | } 108 | 109 | String errorString = "WebSocket stream error: "; 110 | 111 | if (condition != null) { 112 | errorString += condition; 113 | } else { 114 | errorString += "unknown"; 115 | } 116 | 117 | if (text != null) { 118 | errorString += " - " + text; 119 | } 120 | 121 | Strophe.error(errorString); 122 | 123 | // close the connection on stream_error 124 | this._conn.changeConnectStatus(connectstatus, condition); 125 | this._conn.doDisconnect(); 126 | return true; 127 | } 128 | 129 | /** PrivateFunction: _reset 130 | * Reset the connection. 131 | * 132 | * This function is called by the reset function of the Strophe Connection. 133 | * Is not needed by WebSockets. 134 | */ 135 | reset() { 136 | this._reset(); 137 | } 138 | 139 | _reset() { 140 | return; 141 | } 142 | 143 | /** PrivateFunction: _connect 144 | * _Private_ function called by Strophe.Connection.connect 145 | * 146 | * Creates a WebSocket for a connection and assigns Callbacks to it. 147 | * Does nothing if there already is a WebSocket. 148 | */ 149 | connect([int wait, int hold, String route]) { 150 | this._connect(); 151 | } 152 | 153 | _connect() { 154 | // Ensure that there is no open WebSocket from a previous Connection. 155 | this._disconnect(); 156 | if (this.socketListen == null || this.socket == null) { 157 | // Create the new WebSocket 158 | WebSocket.connect(this._conn.service, protocols: ['xmpp']) 159 | .then((WebSocket socket) { 160 | this.socket = socket; 161 | this.socketListen = this.socket.listen(this._connectCbWrapper, 162 | onError: this._onError, onDone: this._onClose); 163 | this._onOpen(); 164 | }).catchError((e) { 165 | this._conn.connexionError("impossible de joindre le serveur XMPP : $e"); 166 | }); 167 | } 168 | } 169 | 170 | /** PrivateFunction: _connect_cb 171 | * _Private_ function called by Strophe.Connection._connect_cb 172 | * 173 | * checks for stream:error 174 | * 175 | * Parameters: 176 | * (Strophe.Request) bodyWrap - The received stanza. 177 | */ 178 | connectCb(bodyWrap) { 179 | this._connectCb(bodyWrap); 180 | } 181 | 182 | _connectCb(bodyWrap) { 183 | bool error = this._checkStreamError(bodyWrap, Strophe.Status['CONNFAIL']); 184 | if (error) { 185 | return Strophe.Status['CONNFAIL']; 186 | } 187 | } 188 | 189 | /** PrivateFunction: _handleStreamStart 190 | * _Private_ function that checks the opening tag for errors. 191 | * 192 | * Disconnects if there is an error and returns false, true otherwise. 193 | * 194 | * Parameters: 195 | * (Node) message - Stanza containing the tag. 196 | */ 197 | bool _handleStreamStart(xml.XmlDocument message) { 198 | String error = ""; 199 | 200 | // Check for errors in the tag 201 | String ns = message.rootElement.getAttribute("xmlns"); 202 | if (ns == null) { 203 | error = "Missing xmlns in "; 204 | } else if (ns != Strophe.NS['FRAMING']) { 205 | error = "Wrong xmlns in : " + ns; 206 | } 207 | 208 | String ver = message.rootElement.getAttribute("version"); 209 | if (ver == null) { 210 | error = "Missing version in "; 211 | } else if (ver != "1.0") { 212 | error = "Wrong version in : " + ver; 213 | } 214 | 215 | if (error != null && error.isNotEmpty) { 216 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], error); 217 | this._conn.doDisconnect(); 218 | return false; 219 | } 220 | return true; 221 | } 222 | 223 | /** PrivateFunction: _connect_cb_wrapper 224 | * _Private_ function that handles the first connection messages. 225 | * 226 | * On receiving an opening stream tag this callback replaces itself with the real 227 | * message handler. On receiving a stream error the connection is terminated. 228 | */ 229 | void _connectCbWrapper(message) { 230 | try { 231 | message = message as String; 232 | } catch (e) { 233 | message = message.toString(); 234 | } 235 | if (message == null || message.isEmpty) return; 236 | if (message.trim().indexOf("\s*)*"), ""); 240 | if (data == '') return; 241 | 242 | xml.XmlDocument streamStart = xml.parse(data); 243 | this._conn.xmlInput(streamStart.rootElement); 244 | this._conn.rawInput(message); 245 | 246 | //_handleStreamSteart will check for XML errors and disconnect on error 247 | if (this._handleStreamStart(streamStart)) { 248 | //_connect_cb will check for stream:error and disconnect on error 249 | this.connectCb(streamStart.rootElement); 250 | } 251 | } else if (message.trim().indexOf(" tag."); 299 | } 300 | this._conn.doDisconnect(); 301 | } 302 | } 303 | 304 | /** PrivateFunction: _doDisconnect 305 | * _Private_ function to disconnect. 306 | * 307 | * Just closes the Socket for WebSockets 308 | */ 309 | void doDisconnect() { 310 | this._doDisconnect(); 311 | } 312 | 313 | void _doDisconnect() { 314 | this._closeSocket(); 315 | } 316 | 317 | /** PrivateFunction _streamWrap 318 | * _Private_ helper function to wrap a stanza in a tag. 319 | * This is used so Strophe can process stanzas from WebSockets like BOSH 320 | */ 321 | String _streamWrap(String stanza) { 322 | return "" + stanza + ''; 323 | } 324 | 325 | /** PrivateFunction: _closeSocket 326 | * _Private_ function to close the WebSocket. 327 | * 328 | * Closes the socket if it is still open and deletes it 329 | */ 330 | void _closeSocket() { 331 | if (this.socket != null) { 332 | try { 333 | this.socket.handleError(() {}); 334 | this.socketListen.cancel(); 335 | this.socketListen = null; 336 | this.socket.close(); 337 | this.socket = null; 338 | } catch (e) {} 339 | } 340 | } 341 | 342 | /** PrivateFunction: _emptyQueue 343 | * _Private_ function to check if the message queue is empty. 344 | * 345 | * Returns: 346 | * True, because WebSocket messages are send immediately after queueing. 347 | */ 348 | bool emptyQueue() { 349 | return this._emptyQueue(); 350 | } 351 | 352 | bool _emptyQueue() { 353 | return true; 354 | } 355 | 356 | /** PrivateFunction: _onClose 357 | * _Private_ function to handle websockets closing. 358 | * 359 | * Nothing to do here for WebSockets 360 | */ 361 | void _onClose() { 362 | if (this._conn.connected && !this._conn.disconnecting) { 363 | Strophe.error("Websocket closed unexpectedly"); 364 | this._conn.doDisconnect(); 365 | } else if (!this._conn.connected && this.socket != null) { 366 | // in case the onError callback was not called (Safari 10 does not 367 | // call onerror when the initial connection fails) we need to 368 | // dispatch a CONNFAIL status update to be consistent with the 369 | // behavior on other browsers. 370 | Strophe.error("Websocket closed unexcectedly"); 371 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], 372 | "The WebSocket connection could not be established or was disconnected."); 373 | this._conn.doDisconnect(); 374 | } else { 375 | Strophe.info("Websocket closed"); 376 | } 377 | } 378 | 379 | /** PrivateFunction: _onDisconnectTimeout 380 | * _Private_ timeout handler for handling non-graceful disconnection. 381 | * 382 | * This does nothing for WebSockets 383 | */ 384 | void onDisconnectTimeout() { 385 | this._onDisconnectTimeout(); 386 | } 387 | 388 | void _onDisconnectTimeout() {} 389 | 390 | /** PrivateFunction: _abortAllRequests 391 | * _Private_ helper function that makes sure all pending requests are aborted. 392 | */ 393 | void abortAllRequests() { 394 | _abortAllRequests(); 395 | } 396 | 397 | void _abortAllRequests() {} 398 | 399 | /** PrivateFunction: _onError 400 | * _Private_ function to handle websockets errors. 401 | * 402 | * Parameters: 403 | * (Object) error - The websocket error. 404 | */ 405 | void _onError(Object error) { 406 | Strophe.error("Websocket error " + error.toString()); 407 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], 408 | "The WebSocket connection could not be established or was disconnected."); 409 | this._disconnect(); 410 | } 411 | 412 | /** PrivateFunction: _onIdle 413 | * _Private_ function called by Strophe.Connection._onIdle 414 | * 415 | * sends all queued stanzas 416 | */ 417 | void onIdle() { 418 | this._onIdle(); 419 | } 420 | 421 | void _onIdle() { 422 | List data = this._conn.data; 423 | if (data.length > 0 && !this._conn.paused) { 424 | for (int i = 0; i < data.length; i++) { 425 | if (data[i] != null) { 426 | xml.XmlElement stanza; 427 | String rawStanza; 428 | if (data[i] == "restart") { 429 | stanza = this._buildStream().tree(); 430 | } else { 431 | stanza = data[i]; 432 | } 433 | rawStanza = Strophe.serialize(stanza); 434 | this._conn.xmlOutput(stanza); 435 | this._conn.rawOutput(rawStanza); 436 | if (this.socket != null) this.socket.add(rawStanza); 437 | } 438 | } 439 | this._conn.data = []; 440 | } 441 | } 442 | 443 | /** PrivateFunction: _onMessage 444 | * _Private_ function to handle websockets messages. 445 | * 446 | * This function parses each of the messages as if they are full documents. 447 | * [TODO : We may actually want to use a SAX Push parser]. 448 | * 449 | * Since all XMPP traffic starts with 450 | * 456 | * 457 | * The first stanza will always fail to be parsed. 458 | * 459 | * Additionally, the seconds stanza will always be with 460 | * the stream NS defined in the previous stanza, so we need to 'force' 461 | * the inclusion of the NS in this stanza. 462 | * 463 | * Parameters: 464 | * (string) message - The websocket message. 465 | */ 466 | void _onMessage(dynamic msg) { 467 | String message = msg as String; 468 | xml.XmlDocument elem; 469 | String data; 470 | // check for closing stream 471 | String close = ''; 472 | if (message == close) { 473 | this._conn.rawInput(close); 474 | this._conn.xmlInput(xml.parse(message).rootElement); 475 | if (!this._conn.disconnecting) { 476 | this._conn.doDisconnect(); 477 | } 478 | return; 479 | } else if (message.trim().indexOf(" tag before we close the connection 502 | return; 503 | } 504 | this._conn.dataRecv(elem.rootElement, message); 505 | } 506 | 507 | /** PrivateFunction: _onOpen 508 | * _Private_ function to handle websockets connection setup. 509 | * 510 | * The opening stream tag is sent here. 511 | */ 512 | _onOpen() { 513 | StanzaBuilder start = this._buildStream(); 514 | this._conn.xmlOutput(start.tree()); 515 | 516 | String startString = Strophe.serialize(start.tree()); 517 | this._conn.rawOutput(startString); 518 | if (this.socket != null) this.socket.add(startString); 519 | } 520 | 521 | /** PrivateFunction: _reqToData 522 | * _Private_ function to get a stanza out of a request. 523 | * 524 | * WebSockets don't use requests, so the passed argument is just returned. 525 | * 526 | * Parameters: 527 | * (Object) stanza - The stanza. 528 | * 529 | * Returns: 530 | * The stanza that was passed. 531 | */ 532 | xml.XmlElement reqToData(stanza) { 533 | return this._reqToData(stanza); 534 | } 535 | 536 | xml.XmlElement _reqToData(stanza) { 537 | if (stanza == null) return null; 538 | //if (stanza is StropheRequest) return stanza.getResponse(); 539 | if (stanza is xml.XmlDocument) return stanza.rootElement; 540 | return stanza as xml.XmlElement; 541 | } 542 | 543 | /** PrivateFunction: _send 544 | * _Private_ part of the Connection.send function for WebSocket 545 | * 546 | * Just flushes the messages that are in the queue 547 | */ 548 | send() { 549 | this._send(); 550 | } 551 | 552 | _send() { 553 | this._conn.flush(); 554 | } 555 | 556 | /** PrivateFunction: _sendRestart 557 | * 558 | * Send an xmpp:restart stanza. 559 | */ 560 | sendRestart() { 561 | this._sendRestart(); 562 | } 563 | 564 | _sendRestart() { 565 | this._conn.idleTimeout.cancel(); 566 | this._conn.onIdle(); 567 | } 568 | 569 | StropheConnection get conn => null; 570 | } 571 | -------------------------------------------------------------------------------- /lib/strophe.dart: -------------------------------------------------------------------------------- 1 | library strophe; 2 | 3 | export 'src/core.dart'; 4 | export 'src/enums.dart'; 5 | export 'src/websocket.dart'; 6 | export 'src/bosh.dart'; 7 | export 'src/plugins/administration.dart'; 8 | export 'src/plugins/bookmark.dart'; 9 | export 'src/plugins/caps.dart'; 10 | export 'src/plugins/chat-notifications.dart'; 11 | export 'src/plugins/disco.dart'; 12 | export 'src/plugins/last-activity.dart'; 13 | export 'src/plugins/muc.dart'; 14 | export 'src/plugins/pep.dart'; 15 | export 'src/plugins/plugins.dart'; 16 | export 'src/plugins/privacy.dart'; 17 | export 'src/plugins/pubsub.dart'; 18 | export 'src/plugins/private-storage.dart'; 19 | export 'src/plugins/register.dart'; 20 | export 'src/plugins/roster.dart'; 21 | export 'src/plugins/vcard-temp.dart'; 22 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.8" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.0.4" 18 | charcode: 19 | dependency: transitive 20 | description: 21 | name: charcode 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.2" 25 | collection: 26 | dependency: transitive 27 | description: 28 | name: collection 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.14.11" 32 | convert: 33 | dependency: transitive 34 | description: 35 | name: convert 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.0.2" 39 | cookie_jar: 40 | dependency: transitive 41 | description: 42 | name: cookie_jar 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.0.7" 46 | crypto: 47 | dependency: "direct main" 48 | description: 49 | name: crypto 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.0.6" 53 | dio: 54 | dependency: "direct main" 55 | description: 56 | name: dio 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.0.9" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.3+1" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.1.6" 84 | path: 85 | dependency: transitive 86 | description: 87 | name: path 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.6.2" 91 | petitparser: 92 | dependency: transitive 93 | description: 94 | name: petitparser 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "2.0.2" 98 | quiver: 99 | dependency: transitive 100 | description: 101 | name: quiver 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "2.0.1" 105 | sky_engine: 106 | dependency: transitive 107 | description: flutter 108 | source: sdk 109 | version: "0.0.99" 110 | source_span: 111 | dependency: transitive 112 | description: 113 | name: source_span 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.4.1" 117 | stack_trace: 118 | dependency: transitive 119 | description: 120 | name: stack_trace 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.9.3" 124 | stream_channel: 125 | dependency: transitive 126 | description: 127 | name: stream_channel 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.6.8" 131 | string_scanner: 132 | dependency: transitive 133 | description: 134 | name: string_scanner 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.0.4" 138 | term_glyph: 139 | dependency: transitive 140 | description: 141 | name: term_glyph 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.0.1" 145 | test_api: 146 | dependency: transitive 147 | description: 148 | name: test_api 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "0.2.1" 152 | typed_data: 153 | dependency: transitive 154 | description: 155 | name: typed_data 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.1.6" 159 | vector_math: 160 | dependency: transitive 161 | description: 162 | name: vector_math 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "2.0.8" 166 | xml: 167 | dependency: "direct main" 168 | description: 169 | name: xml 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "3.2.3" 173 | sdks: 174 | dart: ">=2.0.0 <3.0.0" 175 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: strophe 2 | description: A new flutter package project. 3 | version: 0.0.5 4 | author: Andre_developer 5 | homepage: https://github.com/developerandre/strophe-dart 6 | 7 | dependencies: 8 | xml: ^3.2.3 9 | dio: ^1.0.9 10 | crypto: ^2.0.6 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | environment: 19 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://www.dartlang.org/tools/pub/pubspec 23 | 24 | # The following section is specific to Flutter. 25 | flutter: 26 | 27 | # To add assets to your package, add an assets section, like this: 28 | # assets: 29 | # - images/a_dot_burr.jpeg 30 | # - images/a_dot_ham.jpeg 31 | # 32 | # For details regarding assets in packages, see 33 | # https://flutter.io/assets-and-images/#from-packages 34 | # 35 | # An image asset can refer to one or more resolution-specific "variants", see 36 | # https://flutter.io/assets-and-images/#resolution-aware. 37 | 38 | # To add custom fonts to your package, add a fonts section here, 39 | # in this "flutter" section. Each entry in this list should have a 40 | # "family" key with the font family name, and a "fonts" key with a 41 | # list giving the asset and other descriptors for the font. For 42 | # example: 43 | # fonts: 44 | # - family: Schyler 45 | # fonts: 46 | # - asset: fonts/Schyler-Regular.ttf 47 | # - asset: fonts/Schyler-Italic.ttf 48 | # style: italic 49 | # - family: Trajan Pro 50 | # fonts: 51 | # - asset: fonts/TrajanPro.ttf 52 | # - asset: fonts/TrajanPro_Bold.ttf 53 | # weight: 700 54 | # 55 | # For details regarding fonts in packages, see 56 | # https://flutter.io/custom-fonts/#from-packages 57 | -------------------------------------------------------------------------------- /strophe.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/strophe_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:crypto/crypto.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:strophe/src/sha1.dart'; 6 | import 'package:strophe/strophe.dart'; 7 | 8 | void main() async { 9 | test('adds one to input values', () async { 10 | //var key = utf8.encode("p@ssw0rd"); 11 | //var bytes = utf8.encode("foobar"); 12 | 13 | //var hmacSha1 = new Hmac(sha1, key); // HMAC-SHA256 14 | //var digest = hmacSha1.convert(bytes); 15 | //String decode = utf8.decode(digest.bytes, allowMalformed: true); 16 | //print(" $decode"); 17 | /* print(String.fromCharCodes(digest.bytes) == 'Í¡ÊSQâ9þ5RŠ ðAåÜhl'); 18 | print("HMAC digest as bytes: ${digest.bytes}"); 19 | print("HMAC digest as hex string: $digest"); 20 | print("HMAC digest as base64 string: ${base64.encode(digest.bytes)}"); */ 21 | StropheConnection _connection = 22 | Strophe.Connection("ws://127.0.0.1:5280/xmpp"); 23 | _connection.xmlInput = (elem) { 24 | //print('input $elem'); 25 | }; 26 | _connection.xmlOutput = (elem) { 27 | //print('output $elem'); 28 | }; 29 | _connection.connect('11111@localhost', 'jesuis123', 30 | (int status, condition, ele) { 31 | print("$status $ele"); 32 | }); 33 | await Future.delayed(Duration(days: 1), () { 34 | print('kehhh'); 35 | }); 36 | }); 37 | } 38 | --------------------------------------------------------------------------------