From cbc2ccda7c253379ad5368497f87c1993b050c05 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:05 +0400 Subject: [PATCH 01/12] feat(echo): lil base structure change & attached ext Changed structure of the current send method, added resultCallback and errorCallback as did earlier for the method sendIQ Attached CapsExtension as default extension. By this we have two embedded extensions (disco and caps) for now Other extensions which has used send method has changed its passes params including newly added params --- lib/extensions/pubsub/pubsub_extension.dart | 67 ++++++++++--------- .../registration/registration_extension.dart | 12 +--- lib/src/echo.dart | 16 ++++- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/extensions/pubsub/pubsub_extension.dart b/lib/extensions/pubsub/pubsub_extension.dart index 1bcac77..15eb664 100644 --- a/lib/extensions/pubsub/pubsub_extension.dart +++ b/lib/extensions/pubsub/pubsub_extension.dart @@ -235,15 +235,8 @@ class PubSubExtension extends Extension { final completer = Completer>(); - echo!.addHandler( - callback, - resultCallback: resultCallback, - errorCallback: errorCallback, - completer: completer, - name: 'iq', - id: id, - ); - echo!.send(iq.nodeTree, completer); + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -328,7 +321,7 @@ class PubSubExtension extends Extension { id: id, ); - echo!.send(iq.nodeTree); + echo!.send(iq); return id; } @@ -343,9 +336,7 @@ class PubSubExtension extends Extension { /// data. /// * @return A [String] resolves to the ID of the sent IQ stanza.This ID can /// be used to track the response or correlate it with the original request. - String getDefaultNodeConfig([ - FutureOr Function(XmlElement)? callback, - ]) { + String getDefaultNodeConfig([FutureOr Function(XmlElement)? callback]) { final id = echo!.getUniqueId('pubsubdefaultnodeconfig'); final iq = EchoBuilder.iq( @@ -353,7 +344,7 @@ class PubSubExtension extends Extension { ).c('pubsub', attributes: {'xmlns': ns['PUBSUB_OWNER']!}).c('default'); echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + echo!.send(iq); return id; } @@ -520,16 +511,8 @@ class PubSubExtension extends Extension { final completer = Completer>(); - echo!.addHandler( - callback, - name: 'iq', - id: id, - resultCallback: resultCallback, - errorCallback: errorCallback, - completer: completer, - ); - - echo!.send(iq, completer); + echo!.addHandler(callback, name: 'iq', id: id, completer: completer); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -634,6 +617,12 @@ class PubSubExtension extends Extension { /// /// * @param node The identifier of the PubSub node for which the /// subscriptions are requested. + /// * @param resultCallback (Function) An optional callback function that + /// will be invoked when a successful response to the IQ stanza is received. + /// It can be used to process the received items XML and perform any necessary + /// actions. + /// * @param errorCallback An optional callback function that will be invoked + /// when an error response or no response to the IQ stanza is received. /// * @param callback (Function) An optional callback function that will be /// invoked when a response to the IQ stanza is received. This callback can /// be used to process the response or perform additional actions based on @@ -648,10 +637,12 @@ class PubSubExtension extends Extension { /// return true; /// }); /// ``` - String getNodeSubscriptions( + Future getNodeSubscriptions( String node, [ FutureOr Function(XmlElement)? callback, - ]) { + FutureOr Function(XmlElement)? resultCallback, + FutureOr Function(EchoException)? errorCallback, + ]) async { final id = echo!.getUniqueId('pubsubsubscriptions'); final iq = EchoBuilder.iq( @@ -661,8 +652,10 @@ class PubSubExtension extends Extension { attributes: {'node': node}, ); - echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + final completer = Completer>(); + + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -676,6 +669,12 @@ class PubSubExtension extends Extension { /// requested. /// * @param subID The identifier of the subscription for which options are /// requested. + /// * @param resultCallback (Function) An optional callback function that + /// will be invoked when a successful response to the IQ stanza is received. + /// It can be used to process the received items XML and perform any necessary + /// actions. + /// * @param errorCallback An optional callback function that will be invoked + /// when an error response or no response to the IQ stanza is received. /// * @param callback (Function) An optional callback function which returns /// bool or Future. This callback can be used to process the response or /// perform additional actions based on the received data. @@ -692,11 +691,13 @@ class PubSubExtension extends Extension { /// }, /// ); /// ``` - String getSubscriptionOptions( + Future getSubscriptionOptions( String node, { String? subID, FutureOr Function(XmlElement)? callback, - }) { + FutureOr Function(XmlElement)? resultCallback, + FutureOr Function(EchoException)? errorCallback, + }) async { final id = echo!.getUniqueId('pubsubsuboptions'); final iq = EchoBuilder.iq( @@ -715,8 +716,10 @@ class PubSubExtension extends Extension { iq.addAttributes({'subid': subID}); } - echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + final completer = Completer>(); + + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } diff --git a/lib/extensions/registration/registration_extension.dart b/lib/extensions/registration/registration_extension.dart index 1c8abab..f73b5fd 100644 --- a/lib/extensions/registration/registration_extension.dart +++ b/lib/extensions/registration/registration_extension.dart @@ -62,7 +62,7 @@ class RegistrationExtension extends Extension { final completer = Completer>(); /// Add system handler for accepting incoming stanzas. - super.echo!._addSystemHandler( + echo!._addSystemHandler( (stanza) { final query = stanza.findAllElements('query'); @@ -85,14 +85,6 @@ class RegistrationExtension extends Extension { ); /// Send stanza which built using [EchoBuilder.iq] constructor. - await super.echo!.send(query.nodeTree); - - /// Wait for the answer from `completer`. - final either = await completer.future; - - return either.fold( - (stanza) => resultCallback?.call(stanza), - (exception) => errorCallback?.call(exception), - ); + await echo!.send(query, completer, resultCallback, errorCallback); } } diff --git a/lib/src/echo.dart b/lib/src/echo.dart index 96b1659..fcd586a 100644 --- a/lib/src/echo.dart +++ b/lib/src/echo.dart @@ -133,6 +133,10 @@ class Echo { /// Initialize [DiscoExtension] class and attach to the current [Echo]. disco = DiscoExtension(); attachExtension(disco); + + /// Initialize [CapsExtension] class and attach to the current [Echo]. + caps = CapsExtension(); + attachExtension(caps); } /// `version` constant. @@ -268,6 +272,9 @@ class Echo { /// Late initialization of [DiscoExtension]. late final DiscoExtension disco; + /// Late initialization of [CapsExtension]. + late final CapsExtension caps; + /// The selected mechanism to provide authentication. late SASL? _mechanism; @@ -830,6 +837,8 @@ class Echo { FutureOr send( dynamic message, [ Completer>? completer, + FutureOr Function(xml.XmlElement stanza)? resultCallback, + FutureOr Function(EchoException)? errorCallback, ]) async { /// If the message is null or empty, exit from the function. if (message == null) return; @@ -858,10 +867,15 @@ class Echo { /// If `completer` param is not null, then wait for the incoming stanza /// result. if (completer != null) { - await completer.future.timeout( + final either = await completer.future.timeout( Duration(milliseconds: stanzaResponseTimeout), onTimeout: () => Right(EchoExceptionMapper.requestTimedOut()), ); + + either.fold( + (stanza) => resultCallback?.call(stanza), + (exception) => errorCallback?.call(exception), + ); } } From 47500cc88aaecb06a7c0f9ad4544a23fe33243bb Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:38 +0400 Subject: [PATCH 02/12] refactor(ext): change two params from private to public --- lib/extensions/disco/disco_extension.dart | 54 +++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/extensions/disco/disco_extension.dart b/lib/extensions/disco/disco_extension.dart index b52d786..2e5fd65 100644 --- a/lib/extensions/disco/disco_extension.dart +++ b/lib/extensions/disco/disco_extension.dart @@ -21,10 +21,10 @@ class DiscoExtension extends Extension { late final _items = []; /// The list of feature strings. - late final _features = []; + late final features = []; /// The list of [DiscoIdentity] objects. - late final _identities = []; + late final identities = []; /// This method is not implemented and will not be affected in the use of this /// extension. @@ -72,16 +72,16 @@ class DiscoExtension extends Extension { String name = '', String language = '', }) { - for (int i = 0; i < _identities.length; i++) { - if (_identities[i].category == category && - _identities[i].type == type && - _identities[i].name == name && - _identities[i].language == language) { + for (int i = 0; i < identities.length; i++) { + if (identities[i].category == category && + identities[i].type == type && + identities[i].name == name && + identities[i].language == language) { return false; } } - _identities.add( + identities.add( DiscoIdentity( category: category, type: type, @@ -110,6 +110,7 @@ class DiscoExtension extends Extension { FutureOr Function(EchoException)? errorCallback, int? timeout, }) async { + final id = super.echo!.getUniqueId('info'); final attributes = {'xmlns': ns['DISCO_INFO']!}; if (node != null) { @@ -117,7 +118,7 @@ class DiscoExtension extends Extension { } final info = EchoBuilder.iq( - attributes: {'from': echo!.jid, 'to': jid, 'type': 'get'}, + attributes: {'from': super.echo!.jid, 'to': jid, 'type': 'get', 'id': id}, ).c('query', attributes: attributes); return echo!.sendIQ( @@ -125,7 +126,6 @@ class DiscoExtension extends Extension { resultCallback: resultCallback, errorCallback: errorCallback, waitForResult: true, - timeout: timeout, ); } @@ -171,12 +171,12 @@ class DiscoExtension extends Extension { /// * @return `true` if the feature was added successfully, or `false` if the /// feature already exists. bool addFeature(String variableName) { - for (int i = 0; i < _features.length; i++) { - if (_features[i] == variableName) { + for (int i = 0; i < features.length; i++) { + if (features[i] == variableName) { return false; } } - _features.add(variableName); + features.add(variableName); return true; } @@ -186,9 +186,9 @@ class DiscoExtension extends Extension { /// * @return `true` feature was removed successfully, or `false` if the /// feature does not exist. bool removeFeature(String variableName) { - for (int i = 0; i < _features.length; i++) { - if (_features[i] == variableName) { - _features.removeAt(i); + for (int i = 0; i < features.length; i++) { + if (features[i] == variableName) { + features.removeAt(i); return true; } } @@ -209,25 +209,25 @@ class DiscoExtension extends Extension { final iqResult = _buildIQResult(stanza: stanza, queryAttributes: attributes); - for (int i = 0; i < _identities.length; i++) { + for (int i = 0; i < identities.length; i++) { attributes = { - 'category': _identities[i].category, - 'type': _identities[i].type + 'category': identities[i].category, + 'type': identities[i].type }; - if (_identities[i].name != null) { - attributes['name'] = _identities[i].name!; + if (identities[i].name != null) { + attributes['name'] = identities[i].name!; } - if (_identities[i].language != null) { - attributes['language'] = _identities[i].language!; + if (identities[i].language != null) { + attributes['language'] = identities[i].language!; } iqResult.c('identity', attributes: attributes).up(); } - for (int i = 0; i < _features.length; i++) { - iqResult.c('feature', attributes: {'var': _features[i]}).up(); + for (int i = 0; i < features.length; i++) { + iqResult.c('feature', attributes: {'var': features[i]}).up(); } - echo!.send(iqResult.nodeTree); + echo!.send(iqResult); return true; } @@ -265,7 +265,7 @@ class DiscoExtension extends Extension { } iqResult.c('item', attributes: attributes).up(); } - echo!.send(iqResult.nodeTree); + echo!.send(iqResult); return true; } From d8931dca0701fee917f47aaa754674eace3d3e76 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:51 +0400 Subject: [PATCH 03/12] export `CapsExtension` --- lib/extensions/extensions.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/extensions/extensions.dart b/lib/extensions/extensions.dart index 3fd9ec5..5d4e8e0 100644 --- a/lib/extensions/extensions.dart +++ b/lib/extensions/extensions.dart @@ -1,3 +1,4 @@ +export 'caps/caps_extension.dart'; export 'disco/disco_extension.dart'; export 'pubsub/pubsub_extension.dart'; export 'v-card/vcard_extension.dart'; From 76a4c7489f8cb31267920ba883781f36ad6ef060 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:54:36 +0400 Subject: [PATCH 04/12] feat(ext): create which is required by the extension of PEP --- lib/extensions/caps/caps_extension.dart | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 lib/extensions/caps/caps_extension.dart diff --git a/lib/extensions/caps/caps_extension.dart b/lib/extensions/caps/caps_extension.dart new file mode 100644 index 0000000..7e6e162 --- /dev/null +++ b/lib/extensions/caps/caps_extension.dart @@ -0,0 +1,136 @@ +import 'package:echo/echo.dart'; +import 'package:echo/src/constants.dart'; + +class CapsExtension extends Extension { + CapsExtension() : super('caps-extension'); + + final _verificationString = ''; + final _knownCapabilities = >>{}; + final _jidIndex = {}; + + @override + void changeStatus(EchoStatus status, String? condition) { + if (status == EchoStatus.connected) { + sendPresence(); + } + } + + @override + void initialize(Echo echo) { + echo.addNamespace('CAPS', 'http://jabber.org/protocol/caps'); + + echo.disco.addFeature(ns['CAPS']!); + echo.disco.addIdentity(category: 'client', type: 'mobile', name: 'echo'); + echo.addHandler( + (stanza) { + final from = stanza.getAttribute('from'); + final c = stanza.findAllElements('c').first; + final ver = c.getAttribute('ver'); + final node = c.getAttribute('node'); + + if (!_knownCapabilities.containsKey(ver)) { + return _requestCapabilities(to: from!, node: node!, ver: ver!); + } else { + _jidIndex[from!] = ver!; + } + if (!_jidIndex.containsKey(from) || _jidIndex[from] != ver) { + _jidIndex[from] = ver; + } + return true; + }, + namespace: ns['CAPS'], + name: 'presence', + ); + + super.echo = echo; + } + + Future _requestCapabilities({ + required String to, + required String node, + required String ver, + }) async { + if (to != echo!.jid) { + await echo!.disco + .info(to, node: '$node#$ver', resultCallback: _handleDiscoInfoReply); + } + return true; + } + + bool _handleDiscoInfoReply(XmlElement stanza) { + final query = stanza.findAllElements('query').first; + final node = query.getAttribute('node')!.split('#'); + final ver = node.first; + final from = stanza.getAttribute('from'); + + if (!_knownCapabilities.containsKey(ver) || + _knownCapabilities[ver] == null) { + final nodes = query.descendantElements.toList(); + _knownCapabilities[ver] = []; + for (int i = 0; i < nodes.length; i++) { + final node = nodes[i]; + _knownCapabilities[ver]! + .add({'name': node.name.local, 'attributes': node.attributes}); + } + _jidIndex[from!] = ver; + } else if (_jidIndex[from] == null || _jidIndex[from] != ver) { + _jidIndex[from!] = ver; + } + + return false; + } + + Map get _generateCapsAttributes => { + 'xmlns': ns['CAPS']!, + 'hash': 'sha-1', + 'node': 'echo 0.0.6<', + 'ver': _generateVerificationString, + }; + + XmlElement? get _createCapsNode => + EchoBuilder('c', _generateCapsAttributes).nodeTree; + + void sendPresence() => echo!.send(EchoBuilder.pres().cnode(_createCapsNode!)); + + String get _generateVerificationString { + if (_verificationString.isNotEmpty) { + return _verificationString; + } + + final verificationStringBuffer = StringBuffer(); + final identities = echo!.disco.identities; + _sort(identities, 'category'); + _sort(identities, 'type'); + _sort(identities, 'language'); + final features = echo!.disco.features..sort(); + + for (int i = 0; i < identities.length; i++) { + final id = identities[i]; + verificationStringBuffer + ..writeAll([id.category, id.type, id.language], '/') + ..write(id.name) + ..write('<'); + } + + for (int i = 0; i < features.length; i++) { + verificationStringBuffer + ..write(features[i]) + ..write('<'); + } + + return Echotils.btoa( + Echotils.utf16to8(verificationStringBuffer.toString()), + ); + } + + List _sort(List identities, String property) { + if (property == 'category') { + identities.sort((i1, i2) => i1.category.compareTo(i2.category)); + } else if (property == 'type') { + identities.sort((i1, i2) => i1.type.compareTo(i2.type)); + } else if (property == 'language') { + identities.sort((i1, i2) => i1.language!.compareTo(i2.language!)); + } + return identities; + } +} From 13cf703ea7c9973f9a60660e8564374d2dcf49ba Mon Sep 17 00:00:00 2001 From: vsev Date: Fri, 28 Jul 2023 19:00:48 +0400 Subject: [PATCH 05/12] feat(ext): document `caps` extension --- lib/extensions/caps/caps_extension.dart | 56 ++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/extensions/caps/caps_extension.dart b/lib/extensions/caps/caps_extension.dart index 7e6e162..47af14b 100644 --- a/lib/extensions/caps/caps_extension.dart +++ b/lib/extensions/caps/caps_extension.dart @@ -1,15 +1,35 @@ import 'package:echo/echo.dart'; import 'package:echo/src/constants.dart'; +/// Represents Caps (short for Entity Capabilities) plugin. +/// +/// CapsExtension is an implementation of the [Extension] class that provides +/// support for handling capabilities in the XMPP protocol using the +/// Capabilities feature. +/// +/// This extension allows the client to advertise its capabilities to other +/// entities and discover the capabilities of other entities. class CapsExtension extends Extension { + /// Creates an instance of the [CapsExtension] with the provided extension + /// name. + /// + /// For more information about this extension please refer to [CapsExtension] + /// or `Readme`. CapsExtension() : super('caps-extension'); - final _verificationString = ''; + /// A [Map] to store known capabilities of other entities identified by the + /// `verification` attribute received in the presence stanzas. final _knownCapabilities = >>{}; + + /// A [Map] to store the `verification` attribute received in the presence + /// from different entities, indexed by their respective JIDs. final _jidIndex = {}; + /// Called when the connection status changes. @override void changeStatus(EchoStatus status, String? condition) { + /// If the status is in `connected` state, it sends the client's presence + /// with its capabilities. if (status == EchoStatus.connected) { sendPresence(); } @@ -17,10 +37,14 @@ class CapsExtension extends Extension { @override void initialize(Echo echo) { + /// CAPS feature namespace echo.addNamespace('CAPS', 'http://jabber.org/protocol/caps'); + /// Add the `CAPS` feature and client identity to the disco extension. echo.disco.addFeature(ns['CAPS']!); echo.disco.addIdentity(category: 'client', type: 'mobile', name: 'echo'); + + /// Set up presence handler to process the incoming presence stanzas. echo.addHandler( (stanza) { final from = stanza.getAttribute('from'); @@ -29,6 +53,8 @@ class CapsExtension extends Extension { final node = c.getAttribute('node'); if (!_knownCapabilities.containsKey(ver)) { + /// If the capabilities are not known, request capabilities from the + /// entity. return _requestCapabilities(to: from!, node: node!, ver: ver!); } else { _jidIndex[from!] = ver!; @@ -45,6 +71,15 @@ class CapsExtension extends Extension { super.echo = echo; } + /// Requests capabilities from the given entity (identified by `to` JID) with + /// the provided `node` and `ver` attributes. + /// + /// * @param to The Jabber Identifier to indicate who the request is going to. + /// * @param node A Unique Identifier for the capabilities being queried. It + /// helps in distinguishing between different sets of caps provided by the + /// same entity. + /// * @param ver Stands for verification string and helps preventing poisoning + /// of entity capabilities information. Future _requestCapabilities({ required String to, required String node, @@ -57,6 +92,8 @@ class CapsExtension extends Extension { return true; } + /// Handles the reply to the disco#info query and updates the known + /// capabilities for the entity identified by 'from' JID. bool _handleDiscoInfoReply(XmlElement stanza) { final query = stanza.findAllElements('query').first; final node = query.getAttribute('node')!.split('#'); @@ -65,6 +102,8 @@ class CapsExtension extends Extension { if (!_knownCapabilities.containsKey(ver) || _knownCapabilities[ver] == null) { + /// If the capabilities are not known, add them to the knownCapabilities + /// [Map]. final nodes = query.descendantElements.toList(); _knownCapabilities[ver] = []; for (int i = 0; i < nodes.length; i++) { @@ -80,6 +119,8 @@ class CapsExtension extends Extension { return false; } + /// Generates the attributes for the 'c' (capabilities) node in the client's + /// presence. Map get _generateCapsAttributes => { 'xmlns': ns['CAPS']!, 'hash': 'sha-1', @@ -87,16 +128,18 @@ class CapsExtension extends Extension { 'ver': _generateVerificationString, }; + /// Creates the 'c' (capabilities) node for the client's presence. XmlElement? get _createCapsNode => EchoBuilder('c', _generateCapsAttributes).nodeTree; void sendPresence() => echo!.send(EchoBuilder.pres().cnode(_createCapsNode!)); + /// Generates the verification string for the client's capabilities based on + /// the identities and features supported by the client. + /// + /// For more information about this string please refer to the documentation: + /// https://xmpp.org/extensions/xep-0115.html#ver String get _generateVerificationString { - if (_verificationString.isNotEmpty) { - return _verificationString; - } - final verificationStringBuffer = StringBuffer(); final identities = echo!.disco.identities; _sort(identities, 'category'); @@ -123,6 +166,9 @@ class CapsExtension extends Extension { ); } + /// Sorts the list of [DiscoIdentity] objects based on the specified + /// 'property'. The sorting is done in-place and returns the sorted list of + /// identities. List _sort(List identities, String property) { if (property == 'category') { identities.sort((i1, i2) => i1.category.compareTo(i2.category)); From 4585d00712e4cd28e807744807b7c3b2bf8d3e22 Mon Sep 17 00:00:00 2001 From: vsev Date: Fri, 28 Jul 2023 19:01:21 +0400 Subject: [PATCH 06/12] provide information about `caps` in readme --- lib/extensions/caps/README.markdown | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/extensions/caps/README.markdown diff --git a/lib/extensions/caps/README.markdown b/lib/extensions/caps/README.markdown new file mode 100644 index 0000000..c2eea37 --- /dev/null +++ b/lib/extensions/caps/README.markdown @@ -0,0 +1,50 @@ +# Entity Capabilities + +This extension provides support for handling capabilities in the XMPP protocol using Caps (short for **Capabilities**) feature. Allows a client to advertise its capabilities to other entities and discover the capabilities of other entities in the XMPP network. + +## Features + +- Advertise and discover capabilities of XMPP entities. +- Efficiently handle capabilities updates to avoid unnecessary queries. +- Automatic capabilities exchange during XMPP connection establishment. + +## Limitations + +Please note that you need to use `echo.disco.addFeature` and `echo.disco.addIdentity` methods to add capabilities features and identity to the client. This extension does not provide built-in methods for adding features and identities to the disco extension. + +## Embedding + +This extension comes built-in to the client. It means you do not need to attach this extension as you did on other extensions. You can not disable or enable this feature in any way. + +## API + +This code snippet demonstrates how to use this extension for the client. + +```dart + +import 'dart:async'; +import 'dart:developer'; + +import 'package:echo/echo.dart'; + +Future main() async { + final echo = Echo(service: 'ws://example.com:5443/ws'); + + await echo.connect( + jid: 'vsevex@example.com', + password: 'randompasswordwhichisgoingtobeyourpassword', + callback: (status) async { + if (status == EchoStatus.connected) { + log('Connection Established'); + echo.disco.addFeature('someFeature'); + echo.caps.sendPresence(); + } else if (status == EchoStatus.disconnected) { + log('Connection Terminated'); + } else if (status == EchoStatus.register) { + registrationCompleter.complete(true); + } + }, + ); +} + +``` From 92192feb7e4f57135ef031eca69c19f61eb57aad Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:05 +0400 Subject: [PATCH 07/12] feat(echo): lil base structure change & attached ext Changed structure of the current send method, added resultCallback and errorCallback as did earlier for the method sendIQ Attached CapsExtension as default extension. By this we have two embedded extensions (disco and caps) for now Other extensions which has used send method has changed its passes params including newly added params --- lib/extensions/pubsub/pubsub_extension.dart | 67 ++++++++++--------- .../registration/registration_extension.dart | 12 +--- lib/src/echo.dart | 16 ++++- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/extensions/pubsub/pubsub_extension.dart b/lib/extensions/pubsub/pubsub_extension.dart index 1bcac77..15eb664 100644 --- a/lib/extensions/pubsub/pubsub_extension.dart +++ b/lib/extensions/pubsub/pubsub_extension.dart @@ -235,15 +235,8 @@ class PubSubExtension extends Extension { final completer = Completer>(); - echo!.addHandler( - callback, - resultCallback: resultCallback, - errorCallback: errorCallback, - completer: completer, - name: 'iq', - id: id, - ); - echo!.send(iq.nodeTree, completer); + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -328,7 +321,7 @@ class PubSubExtension extends Extension { id: id, ); - echo!.send(iq.nodeTree); + echo!.send(iq); return id; } @@ -343,9 +336,7 @@ class PubSubExtension extends Extension { /// data. /// * @return A [String] resolves to the ID of the sent IQ stanza.This ID can /// be used to track the response or correlate it with the original request. - String getDefaultNodeConfig([ - FutureOr Function(XmlElement)? callback, - ]) { + String getDefaultNodeConfig([FutureOr Function(XmlElement)? callback]) { final id = echo!.getUniqueId('pubsubdefaultnodeconfig'); final iq = EchoBuilder.iq( @@ -353,7 +344,7 @@ class PubSubExtension extends Extension { ).c('pubsub', attributes: {'xmlns': ns['PUBSUB_OWNER']!}).c('default'); echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + echo!.send(iq); return id; } @@ -520,16 +511,8 @@ class PubSubExtension extends Extension { final completer = Completer>(); - echo!.addHandler( - callback, - name: 'iq', - id: id, - resultCallback: resultCallback, - errorCallback: errorCallback, - completer: completer, - ); - - echo!.send(iq, completer); + echo!.addHandler(callback, name: 'iq', id: id, completer: completer); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -634,6 +617,12 @@ class PubSubExtension extends Extension { /// /// * @param node The identifier of the PubSub node for which the /// subscriptions are requested. + /// * @param resultCallback (Function) An optional callback function that + /// will be invoked when a successful response to the IQ stanza is received. + /// It can be used to process the received items XML and perform any necessary + /// actions. + /// * @param errorCallback An optional callback function that will be invoked + /// when an error response or no response to the IQ stanza is received. /// * @param callback (Function) An optional callback function that will be /// invoked when a response to the IQ stanza is received. This callback can /// be used to process the response or perform additional actions based on @@ -648,10 +637,12 @@ class PubSubExtension extends Extension { /// return true; /// }); /// ``` - String getNodeSubscriptions( + Future getNodeSubscriptions( String node, [ FutureOr Function(XmlElement)? callback, - ]) { + FutureOr Function(XmlElement)? resultCallback, + FutureOr Function(EchoException)? errorCallback, + ]) async { final id = echo!.getUniqueId('pubsubsubscriptions'); final iq = EchoBuilder.iq( @@ -661,8 +652,10 @@ class PubSubExtension extends Extension { attributes: {'node': node}, ); - echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + final completer = Completer>(); + + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } @@ -676,6 +669,12 @@ class PubSubExtension extends Extension { /// requested. /// * @param subID The identifier of the subscription for which options are /// requested. + /// * @param resultCallback (Function) An optional callback function that + /// will be invoked when a successful response to the IQ stanza is received. + /// It can be used to process the received items XML and perform any necessary + /// actions. + /// * @param errorCallback An optional callback function that will be invoked + /// when an error response or no response to the IQ stanza is received. /// * @param callback (Function) An optional callback function which returns /// bool or Future. This callback can be used to process the response or /// perform additional actions based on the received data. @@ -692,11 +691,13 @@ class PubSubExtension extends Extension { /// }, /// ); /// ``` - String getSubscriptionOptions( + Future getSubscriptionOptions( String node, { String? subID, FutureOr Function(XmlElement)? callback, - }) { + FutureOr Function(XmlElement)? resultCallback, + FutureOr Function(EchoException)? errorCallback, + }) async { final id = echo!.getUniqueId('pubsubsuboptions'); final iq = EchoBuilder.iq( @@ -715,8 +716,10 @@ class PubSubExtension extends Extension { iq.addAttributes({'subid': subID}); } - echo!.addHandler(callback, name: 'iq', id: id); - echo!.send(iq.nodeTree); + final completer = Completer>(); + + echo!.addHandler(callback, completer: completer, name: 'iq', id: id); + await echo!.send(iq, completer, resultCallback, errorCallback); return id; } diff --git a/lib/extensions/registration/registration_extension.dart b/lib/extensions/registration/registration_extension.dart index 1c8abab..f73b5fd 100644 --- a/lib/extensions/registration/registration_extension.dart +++ b/lib/extensions/registration/registration_extension.dart @@ -62,7 +62,7 @@ class RegistrationExtension extends Extension { final completer = Completer>(); /// Add system handler for accepting incoming stanzas. - super.echo!._addSystemHandler( + echo!._addSystemHandler( (stanza) { final query = stanza.findAllElements('query'); @@ -85,14 +85,6 @@ class RegistrationExtension extends Extension { ); /// Send stanza which built using [EchoBuilder.iq] constructor. - await super.echo!.send(query.nodeTree); - - /// Wait for the answer from `completer`. - final either = await completer.future; - - return either.fold( - (stanza) => resultCallback?.call(stanza), - (exception) => errorCallback?.call(exception), - ); + await echo!.send(query, completer, resultCallback, errorCallback); } } diff --git a/lib/src/echo.dart b/lib/src/echo.dart index 96b1659..fcd586a 100644 --- a/lib/src/echo.dart +++ b/lib/src/echo.dart @@ -133,6 +133,10 @@ class Echo { /// Initialize [DiscoExtension] class and attach to the current [Echo]. disco = DiscoExtension(); attachExtension(disco); + + /// Initialize [CapsExtension] class and attach to the current [Echo]. + caps = CapsExtension(); + attachExtension(caps); } /// `version` constant. @@ -268,6 +272,9 @@ class Echo { /// Late initialization of [DiscoExtension]. late final DiscoExtension disco; + /// Late initialization of [CapsExtension]. + late final CapsExtension caps; + /// The selected mechanism to provide authentication. late SASL? _mechanism; @@ -830,6 +837,8 @@ class Echo { FutureOr send( dynamic message, [ Completer>? completer, + FutureOr Function(xml.XmlElement stanza)? resultCallback, + FutureOr Function(EchoException)? errorCallback, ]) async { /// If the message is null or empty, exit from the function. if (message == null) return; @@ -858,10 +867,15 @@ class Echo { /// If `completer` param is not null, then wait for the incoming stanza /// result. if (completer != null) { - await completer.future.timeout( + final either = await completer.future.timeout( Duration(milliseconds: stanzaResponseTimeout), onTimeout: () => Right(EchoExceptionMapper.requestTimedOut()), ); + + either.fold( + (stanza) => resultCallback?.call(stanza), + (exception) => errorCallback?.call(exception), + ); } } From 842ee5f59ea72855b6af674d9855f91c5222a1e5 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:38 +0400 Subject: [PATCH 08/12] refactor(ext): change two params from private to public --- lib/extensions/disco/disco_extension.dart | 54 +++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/extensions/disco/disco_extension.dart b/lib/extensions/disco/disco_extension.dart index b52d786..2e5fd65 100644 --- a/lib/extensions/disco/disco_extension.dart +++ b/lib/extensions/disco/disco_extension.dart @@ -21,10 +21,10 @@ class DiscoExtension extends Extension { late final _items = []; /// The list of feature strings. - late final _features = []; + late final features = []; /// The list of [DiscoIdentity] objects. - late final _identities = []; + late final identities = []; /// This method is not implemented and will not be affected in the use of this /// extension. @@ -72,16 +72,16 @@ class DiscoExtension extends Extension { String name = '', String language = '', }) { - for (int i = 0; i < _identities.length; i++) { - if (_identities[i].category == category && - _identities[i].type == type && - _identities[i].name == name && - _identities[i].language == language) { + for (int i = 0; i < identities.length; i++) { + if (identities[i].category == category && + identities[i].type == type && + identities[i].name == name && + identities[i].language == language) { return false; } } - _identities.add( + identities.add( DiscoIdentity( category: category, type: type, @@ -110,6 +110,7 @@ class DiscoExtension extends Extension { FutureOr Function(EchoException)? errorCallback, int? timeout, }) async { + final id = super.echo!.getUniqueId('info'); final attributes = {'xmlns': ns['DISCO_INFO']!}; if (node != null) { @@ -117,7 +118,7 @@ class DiscoExtension extends Extension { } final info = EchoBuilder.iq( - attributes: {'from': echo!.jid, 'to': jid, 'type': 'get'}, + attributes: {'from': super.echo!.jid, 'to': jid, 'type': 'get', 'id': id}, ).c('query', attributes: attributes); return echo!.sendIQ( @@ -125,7 +126,6 @@ class DiscoExtension extends Extension { resultCallback: resultCallback, errorCallback: errorCallback, waitForResult: true, - timeout: timeout, ); } @@ -171,12 +171,12 @@ class DiscoExtension extends Extension { /// * @return `true` if the feature was added successfully, or `false` if the /// feature already exists. bool addFeature(String variableName) { - for (int i = 0; i < _features.length; i++) { - if (_features[i] == variableName) { + for (int i = 0; i < features.length; i++) { + if (features[i] == variableName) { return false; } } - _features.add(variableName); + features.add(variableName); return true; } @@ -186,9 +186,9 @@ class DiscoExtension extends Extension { /// * @return `true` feature was removed successfully, or `false` if the /// feature does not exist. bool removeFeature(String variableName) { - for (int i = 0; i < _features.length; i++) { - if (_features[i] == variableName) { - _features.removeAt(i); + for (int i = 0; i < features.length; i++) { + if (features[i] == variableName) { + features.removeAt(i); return true; } } @@ -209,25 +209,25 @@ class DiscoExtension extends Extension { final iqResult = _buildIQResult(stanza: stanza, queryAttributes: attributes); - for (int i = 0; i < _identities.length; i++) { + for (int i = 0; i < identities.length; i++) { attributes = { - 'category': _identities[i].category, - 'type': _identities[i].type + 'category': identities[i].category, + 'type': identities[i].type }; - if (_identities[i].name != null) { - attributes['name'] = _identities[i].name!; + if (identities[i].name != null) { + attributes['name'] = identities[i].name!; } - if (_identities[i].language != null) { - attributes['language'] = _identities[i].language!; + if (identities[i].language != null) { + attributes['language'] = identities[i].language!; } iqResult.c('identity', attributes: attributes).up(); } - for (int i = 0; i < _features.length; i++) { - iqResult.c('feature', attributes: {'var': _features[i]}).up(); + for (int i = 0; i < features.length; i++) { + iqResult.c('feature', attributes: {'var': features[i]}).up(); } - echo!.send(iqResult.nodeTree); + echo!.send(iqResult); return true; } @@ -265,7 +265,7 @@ class DiscoExtension extends Extension { } iqResult.c('item', attributes: attributes).up(); } - echo!.send(iqResult.nodeTree); + echo!.send(iqResult); return true; } From 0b6e2a48f15b29f49c3e1e6b55057abe2b01bf87 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:53:51 +0400 Subject: [PATCH 09/12] export `CapsExtension` --- lib/extensions/extensions.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/extensions/extensions.dart b/lib/extensions/extensions.dart index 3fd9ec5..5d4e8e0 100644 --- a/lib/extensions/extensions.dart +++ b/lib/extensions/extensions.dart @@ -1,3 +1,4 @@ +export 'caps/caps_extension.dart'; export 'disco/disco_extension.dart'; export 'pubsub/pubsub_extension.dart'; export 'v-card/vcard_extension.dart'; From 022b045d47d35376ede0ce2913922989cbbba511 Mon Sep 17 00:00:00 2001 From: vsev Date: Mon, 24 Jul 2023 18:54:36 +0400 Subject: [PATCH 10/12] feat(ext): create which is required by the extension of PEP --- lib/extensions/caps/caps_extension.dart | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 lib/extensions/caps/caps_extension.dart diff --git a/lib/extensions/caps/caps_extension.dart b/lib/extensions/caps/caps_extension.dart new file mode 100644 index 0000000..7e6e162 --- /dev/null +++ b/lib/extensions/caps/caps_extension.dart @@ -0,0 +1,136 @@ +import 'package:echo/echo.dart'; +import 'package:echo/src/constants.dart'; + +class CapsExtension extends Extension { + CapsExtension() : super('caps-extension'); + + final _verificationString = ''; + final _knownCapabilities = >>{}; + final _jidIndex = {}; + + @override + void changeStatus(EchoStatus status, String? condition) { + if (status == EchoStatus.connected) { + sendPresence(); + } + } + + @override + void initialize(Echo echo) { + echo.addNamespace('CAPS', 'http://jabber.org/protocol/caps'); + + echo.disco.addFeature(ns['CAPS']!); + echo.disco.addIdentity(category: 'client', type: 'mobile', name: 'echo'); + echo.addHandler( + (stanza) { + final from = stanza.getAttribute('from'); + final c = stanza.findAllElements('c').first; + final ver = c.getAttribute('ver'); + final node = c.getAttribute('node'); + + if (!_knownCapabilities.containsKey(ver)) { + return _requestCapabilities(to: from!, node: node!, ver: ver!); + } else { + _jidIndex[from!] = ver!; + } + if (!_jidIndex.containsKey(from) || _jidIndex[from] != ver) { + _jidIndex[from] = ver; + } + return true; + }, + namespace: ns['CAPS'], + name: 'presence', + ); + + super.echo = echo; + } + + Future _requestCapabilities({ + required String to, + required String node, + required String ver, + }) async { + if (to != echo!.jid) { + await echo!.disco + .info(to, node: '$node#$ver', resultCallback: _handleDiscoInfoReply); + } + return true; + } + + bool _handleDiscoInfoReply(XmlElement stanza) { + final query = stanza.findAllElements('query').first; + final node = query.getAttribute('node')!.split('#'); + final ver = node.first; + final from = stanza.getAttribute('from'); + + if (!_knownCapabilities.containsKey(ver) || + _knownCapabilities[ver] == null) { + final nodes = query.descendantElements.toList(); + _knownCapabilities[ver] = []; + for (int i = 0; i < nodes.length; i++) { + final node = nodes[i]; + _knownCapabilities[ver]! + .add({'name': node.name.local, 'attributes': node.attributes}); + } + _jidIndex[from!] = ver; + } else if (_jidIndex[from] == null || _jidIndex[from] != ver) { + _jidIndex[from!] = ver; + } + + return false; + } + + Map get _generateCapsAttributes => { + 'xmlns': ns['CAPS']!, + 'hash': 'sha-1', + 'node': 'echo 0.0.6<', + 'ver': _generateVerificationString, + }; + + XmlElement? get _createCapsNode => + EchoBuilder('c', _generateCapsAttributes).nodeTree; + + void sendPresence() => echo!.send(EchoBuilder.pres().cnode(_createCapsNode!)); + + String get _generateVerificationString { + if (_verificationString.isNotEmpty) { + return _verificationString; + } + + final verificationStringBuffer = StringBuffer(); + final identities = echo!.disco.identities; + _sort(identities, 'category'); + _sort(identities, 'type'); + _sort(identities, 'language'); + final features = echo!.disco.features..sort(); + + for (int i = 0; i < identities.length; i++) { + final id = identities[i]; + verificationStringBuffer + ..writeAll([id.category, id.type, id.language], '/') + ..write(id.name) + ..write('<'); + } + + for (int i = 0; i < features.length; i++) { + verificationStringBuffer + ..write(features[i]) + ..write('<'); + } + + return Echotils.btoa( + Echotils.utf16to8(verificationStringBuffer.toString()), + ); + } + + List _sort(List identities, String property) { + if (property == 'category') { + identities.sort((i1, i2) => i1.category.compareTo(i2.category)); + } else if (property == 'type') { + identities.sort((i1, i2) => i1.type.compareTo(i2.type)); + } else if (property == 'language') { + identities.sort((i1, i2) => i1.language!.compareTo(i2.language!)); + } + return identities; + } +} From 00075faab3d36ea6a803fa71ced2ab573ceadef5 Mon Sep 17 00:00:00 2001 From: vsev Date: Fri, 28 Jul 2023 19:00:48 +0400 Subject: [PATCH 11/12] feat(ext): document `caps` extension --- lib/extensions/caps/caps_extension.dart | 56 ++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/extensions/caps/caps_extension.dart b/lib/extensions/caps/caps_extension.dart index 7e6e162..47af14b 100644 --- a/lib/extensions/caps/caps_extension.dart +++ b/lib/extensions/caps/caps_extension.dart @@ -1,15 +1,35 @@ import 'package:echo/echo.dart'; import 'package:echo/src/constants.dart'; +/// Represents Caps (short for Entity Capabilities) plugin. +/// +/// CapsExtension is an implementation of the [Extension] class that provides +/// support for handling capabilities in the XMPP protocol using the +/// Capabilities feature. +/// +/// This extension allows the client to advertise its capabilities to other +/// entities and discover the capabilities of other entities. class CapsExtension extends Extension { + /// Creates an instance of the [CapsExtension] with the provided extension + /// name. + /// + /// For more information about this extension please refer to [CapsExtension] + /// or `Readme`. CapsExtension() : super('caps-extension'); - final _verificationString = ''; + /// A [Map] to store known capabilities of other entities identified by the + /// `verification` attribute received in the presence stanzas. final _knownCapabilities = >>{}; + + /// A [Map] to store the `verification` attribute received in the presence + /// from different entities, indexed by their respective JIDs. final _jidIndex = {}; + /// Called when the connection status changes. @override void changeStatus(EchoStatus status, String? condition) { + /// If the status is in `connected` state, it sends the client's presence + /// with its capabilities. if (status == EchoStatus.connected) { sendPresence(); } @@ -17,10 +37,14 @@ class CapsExtension extends Extension { @override void initialize(Echo echo) { + /// CAPS feature namespace echo.addNamespace('CAPS', 'http://jabber.org/protocol/caps'); + /// Add the `CAPS` feature and client identity to the disco extension. echo.disco.addFeature(ns['CAPS']!); echo.disco.addIdentity(category: 'client', type: 'mobile', name: 'echo'); + + /// Set up presence handler to process the incoming presence stanzas. echo.addHandler( (stanza) { final from = stanza.getAttribute('from'); @@ -29,6 +53,8 @@ class CapsExtension extends Extension { final node = c.getAttribute('node'); if (!_knownCapabilities.containsKey(ver)) { + /// If the capabilities are not known, request capabilities from the + /// entity. return _requestCapabilities(to: from!, node: node!, ver: ver!); } else { _jidIndex[from!] = ver!; @@ -45,6 +71,15 @@ class CapsExtension extends Extension { super.echo = echo; } + /// Requests capabilities from the given entity (identified by `to` JID) with + /// the provided `node` and `ver` attributes. + /// + /// * @param to The Jabber Identifier to indicate who the request is going to. + /// * @param node A Unique Identifier for the capabilities being queried. It + /// helps in distinguishing between different sets of caps provided by the + /// same entity. + /// * @param ver Stands for verification string and helps preventing poisoning + /// of entity capabilities information. Future _requestCapabilities({ required String to, required String node, @@ -57,6 +92,8 @@ class CapsExtension extends Extension { return true; } + /// Handles the reply to the disco#info query and updates the known + /// capabilities for the entity identified by 'from' JID. bool _handleDiscoInfoReply(XmlElement stanza) { final query = stanza.findAllElements('query').first; final node = query.getAttribute('node')!.split('#'); @@ -65,6 +102,8 @@ class CapsExtension extends Extension { if (!_knownCapabilities.containsKey(ver) || _knownCapabilities[ver] == null) { + /// If the capabilities are not known, add them to the knownCapabilities + /// [Map]. final nodes = query.descendantElements.toList(); _knownCapabilities[ver] = []; for (int i = 0; i < nodes.length; i++) { @@ -80,6 +119,8 @@ class CapsExtension extends Extension { return false; } + /// Generates the attributes for the 'c' (capabilities) node in the client's + /// presence. Map get _generateCapsAttributes => { 'xmlns': ns['CAPS']!, 'hash': 'sha-1', @@ -87,16 +128,18 @@ class CapsExtension extends Extension { 'ver': _generateVerificationString, }; + /// Creates the 'c' (capabilities) node for the client's presence. XmlElement? get _createCapsNode => EchoBuilder('c', _generateCapsAttributes).nodeTree; void sendPresence() => echo!.send(EchoBuilder.pres().cnode(_createCapsNode!)); + /// Generates the verification string for the client's capabilities based on + /// the identities and features supported by the client. + /// + /// For more information about this string please refer to the documentation: + /// https://xmpp.org/extensions/xep-0115.html#ver String get _generateVerificationString { - if (_verificationString.isNotEmpty) { - return _verificationString; - } - final verificationStringBuffer = StringBuffer(); final identities = echo!.disco.identities; _sort(identities, 'category'); @@ -123,6 +166,9 @@ class CapsExtension extends Extension { ); } + /// Sorts the list of [DiscoIdentity] objects based on the specified + /// 'property'. The sorting is done in-place and returns the sorted list of + /// identities. List _sort(List identities, String property) { if (property == 'category') { identities.sort((i1, i2) => i1.category.compareTo(i2.category)); From d59ce52e1cc631dc07213d700a0124bc98135250 Mon Sep 17 00:00:00 2001 From: vsev Date: Fri, 28 Jul 2023 19:01:21 +0400 Subject: [PATCH 12/12] provide information about `caps` in readme --- lib/extensions/caps/README.markdown | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/extensions/caps/README.markdown diff --git a/lib/extensions/caps/README.markdown b/lib/extensions/caps/README.markdown new file mode 100644 index 0000000..c2eea37 --- /dev/null +++ b/lib/extensions/caps/README.markdown @@ -0,0 +1,50 @@ +# Entity Capabilities + +This extension provides support for handling capabilities in the XMPP protocol using Caps (short for **Capabilities**) feature. Allows a client to advertise its capabilities to other entities and discover the capabilities of other entities in the XMPP network. + +## Features + +- Advertise and discover capabilities of XMPP entities. +- Efficiently handle capabilities updates to avoid unnecessary queries. +- Automatic capabilities exchange during XMPP connection establishment. + +## Limitations + +Please note that you need to use `echo.disco.addFeature` and `echo.disco.addIdentity` methods to add capabilities features and identity to the client. This extension does not provide built-in methods for adding features and identities to the disco extension. + +## Embedding + +This extension comes built-in to the client. It means you do not need to attach this extension as you did on other extensions. You can not disable or enable this feature in any way. + +## API + +This code snippet demonstrates how to use this extension for the client. + +```dart + +import 'dart:async'; +import 'dart:developer'; + +import 'package:echo/echo.dart'; + +Future main() async { + final echo = Echo(service: 'ws://example.com:5443/ws'); + + await echo.connect( + jid: 'vsevex@example.com', + password: 'randompasswordwhichisgoingtobeyourpassword', + callback: (status) async { + if (status == EchoStatus.connected) { + log('Connection Established'); + echo.disco.addFeature('someFeature'); + echo.caps.sendPresence(); + } else if (status == EchoStatus.disconnected) { + log('Connection Terminated'); + } else if (status == EchoStatus.register) { + registrationCompleter.complete(true); + } + }, + ); +} + +```