From 1bd61076ead0d68ebcb8d8e2ac414e943efea919 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 27 Jan 2023 16:26:01 +0100 Subject: [PATCH] feat: Improve the API provided by the DiscoManager Fixes #21. --- packages/moxxmpp/lib/src/connection.dart | 84 +++---- packages/moxxmpp/lib/src/managers/base.dart | 28 +++ .../moxxmpp/lib/src/managers/namespaces.dart | 1 + packages/moxxmpp/lib/src/presence.dart | 68 +++--- packages/moxxmpp/lib/src/xeps/xep_0004.dart | 3 - .../lib/src/xeps/xep_0030/helpers.dart | 20 +- .../moxxmpp/lib/src/xeps/xep_0030/types.dart | 81 ++++++- .../lib/src/xeps/xep_0030/xep_0030.dart | 205 ++++++++---------- packages/moxxmpp/lib/src/xeps/xep_0115.dart | 93 +++++++- packages/moxxmpp/test/negotiator_test.dart | 5 +- packages/moxxmpp/test/xeps/xep_0030_test.dart | 7 +- packages/moxxmpp/test/xeps/xep_0115_test.dart | 3 + packages/moxxmpp/test/xeps/xep_0198_test.dart | 32 +-- packages/moxxmpp/test/xmpp_test.dart | 15 +- 14 files changed, 396 insertions(+), 249 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 8abbcf8..238171d 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -223,63 +223,47 @@ class XmppConnection { /// none can be found. T? getNegotiatorById(String id) => _featureNegotiators[id] as T?; - /// Registers an [XmppManagerBase] sub-class as a manager on this connection. - /// [sortHandlers] should NOT be touched. It specified if the handler priorities - /// should be set up. The only time this should be false is when called via - /// [registerManagers]. - void registerManager(XmppManagerBase manager, { bool sortHandlers = true }) { - _log.finest('Registering ${manager.getId()}'); - manager.register( - XmppManagerAttributes( - sendStanza: sendStanza, - sendNonza: sendRawXML, - sendEvent: _sendEvent, - getConnectionSettings: () => _connectionSettings, - getManagerById: getManagerById, - isFeatureSupported: _serverFeatures.contains, - getFullJID: () => _connectionSettings.jid.withResource(_resource), - getSocket: () => _socket, - getConnection: () => this, - getNegotiatorById: getNegotiatorById, - ), - ); - - final id = manager.getId(); - _xmppManagers[id] = manager; - - if (id == discoManager) { - // NOTE: It is intentional that we do not exclude the [DiscoManager] from this - // loop. It may also register features. - for (final registeredManager in _xmppManagers.values) { - (manager as DiscoManager).addDiscoFeatures(registeredManager.getDiscoFeatures()); - } - } else if (_xmppManagers.containsKey(discoManager)) { - (_xmppManagers[discoManager]! as DiscoManager).addDiscoFeatures(manager.getDiscoFeatures()); - } - - _incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); - _incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers()); - _outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers()); - _outgoingPostStanzaHandlers.addAll(manager.getOutgoingPostStanzaHandlers()); - - if (sortHandlers) { - _incomingStanzaHandlers.sort(stanzaHandlerSortComparator); - _incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator); - _outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator); - _outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator); - } - } - - /// Like [registerManager], but for a list of managers. - void registerManagers(List managers) { + /// Registers a list of [XmppManagerBase] sub-classes as managers on this connection. + Future registerManagers(List managers) async { for (final manager in managers) { - registerManager(manager, sortHandlers: false); + _log.finest('Registering ${manager.getId()}'); + manager.register( + XmppManagerAttributes( + sendStanza: sendStanza, + sendNonza: sendRawXML, + sendEvent: _sendEvent, + getConnectionSettings: () => _connectionSettings, + getManagerById: getManagerById, + isFeatureSupported: _serverFeatures.contains, + getFullJID: () => _connectionSettings.jid.withResource(_resource), + getSocket: () => _socket, + getConnection: () => this, + getNegotiatorById: getNegotiatorById, + ), + ); + + final id = manager.getId(); + _xmppManagers[id] = manager; + + _incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); + _incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers()); + _outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers()); + _outgoingPostStanzaHandlers.addAll(manager.getOutgoingPostStanzaHandlers()); } // Sort them _incomingStanzaHandlers.sort(stanzaHandlerSortComparator); + _incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator); _outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator); _outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator); + + // Run the post register callbacks + for (final manager in _xmppManagers.values) { + if (!manager.initialized) { + _log.finest('Running post-registration callback for ${manager.getName()}'); + await manager.postRegisterCallback(); + } + } } /// Register a list of negotiator with the connection. diff --git a/packages/moxxmpp/lib/src/managers/base.dart b/packages/moxxmpp/lib/src/managers/base.dart index 11435ce..08800ec 100644 --- a/packages/moxxmpp/lib/src/managers/base.dart +++ b/packages/moxxmpp/lib/src/managers/base.dart @@ -1,14 +1,21 @@ import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; +import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; abstract class XmppManagerBase { late final XmppManagerAttributes _managerAttributes; late final Logger _log; + /// Flag indicating that the post registration callback has been called once. + bool initialized = false; + /// Registers the callbacks from XmppConnection with the manager void register(XmppManagerAttributes attributes) { _managerAttributes = attributes; @@ -48,6 +55,9 @@ abstract class XmppManagerBase { /// Return a list of features that should be included in a disco response. List getDiscoFeatures() => []; + + /// Return a list of identities that should be included in a disco response. + List getDiscoIdentities() => []; /// Return the Id (akin to xmlns) of this manager. String getId(); @@ -63,6 +73,24 @@ abstract class XmppManagerBase { /// Returns true if the XEP is supported on the server. If not, returns false Future isSupported(); + + /// Called after the registration of all managers against the XmppConnection is done. + /// This method is only called once during the entire lifetime of it. + @mustCallSuper + Future postRegisterCallback() async { + initialized = true; + + final disco = getAttributes().getManagerById(discoManager); + if (disco != null) { + if (getDiscoFeatures().isNotEmpty) { + disco.addFeatures(getDiscoFeatures()); + } + + if (getDiscoIdentities().isNotEmpty) { + disco.addIdentities(getDiscoIdentities()); + } + } + } /// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if /// the nonza has been handled by one of the handlers. Resolves to false otherwise. diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index bc0bae4..ab11a3e 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -28,3 +28,4 @@ const messageRetractionManager = 'org.moxxmpp.messageretractionmanager'; const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; const stickersManager = 'org.moxxmpp.stickersmanager'; +const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index eabef7b..3f4cb49 100644 --- a/packages/moxxmpp/lib/src/presence.dart +++ b/packages/moxxmpp/lib/src/presence.dart @@ -8,17 +8,19 @@ import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; -import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; -import 'package:moxxmpp/src/xeps/xep_0115.dart'; -import 'package:moxxmpp/src/xeps/xep_0414.dart'; +/// A function that will be called when presence, outside of subscription request +/// management, will be sent. Useful for managers that want to add [XMLNode]s to said +/// presence. +typedef PresencePreSendCallback = Future> Function(); + +/// A mandatory manager that handles initial presence sending, sending of subscription +/// request management requests and triggers events for incoming presence stanzas. class PresenceManager extends XmppManagerBase { - PresenceManager(this._capHashNode) : _capabilityHash = null, super(); - String? _capabilityHash; - final String _capHashNode; + PresenceManager() : super(); - String get capabilityHashNode => _capHashNode; + /// The list of pre-send callbacks. + final List _presenceCallbacks = List.empty(growable: true); @override String getId() => presenceManager; @@ -39,6 +41,11 @@ class PresenceManager extends XmppManagerBase { @override Future isSupported() async => true; + + /// Register the pre-send callback [callback]. + void registerPreSendCallback(PresencePreSendCallback callback) { + _presenceCallbacks.add(callback); + } Future _onPresence(Stanza presence, StanzaHandlerData state) async { final attrs = getAttributes(); @@ -63,43 +70,26 @@ class PresenceManager extends XmppManagerBase { return state; } - /// Returns the capability hash. - Future getCapabilityHash() async { - final manager = getAttributes().getManagerById(discoManager)! as DiscoManager; - _capabilityHash ??= await calculateCapabilityHash( - DiscoInfo( - manager.getRegisteredDiscoFeatures(), - manager.getIdentities(), - [], - getAttributes().getFullJID(), - ), - getHashByName('sha-1')!, - ); - - return _capabilityHash!; - } - /// Sends the initial presence to enable receiving messages. Future sendInitialPresence() async { + final children = List.from([ + XMLNode( + tag: 'show', + text: 'chat', + ), + ]); + + for (final callback in _presenceCallbacks) { + children.addAll( + await callback(), + ); + } + final attrs = getAttributes(); attrs.sendNonza( Stanza.presence( from: attrs.getFullJID().toString(), - children: [ - XMLNode( - tag: 'show', - text: 'chat', - ), - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': _capHashNode, - 'ver': await getCapabilityHash() - }, - ) - ], + children: children, ), ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0004.dart b/packages/moxxmpp/lib/src/xeps/xep_0004.dart index f45d27f..e24aa8a 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0004.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0004.dart @@ -3,7 +3,6 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; class DataFormOption { - const DataFormOption({ required this.value, this.label }); final String? label; final String value; @@ -23,7 +22,6 @@ class DataFormOption { } class DataFormField { - const DataFormField({ required this.options, required this.values, @@ -60,7 +58,6 @@ class DataFormField { } class DataForm { - const DataForm({ required this.type, required this.instructions, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart index 5cb78fe..8b9c6c9 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart @@ -6,20 +6,20 @@ import 'package:moxxmpp/src/stringxml.dart'; Stanza buildDiscoInfoQueryStanza(String entity, String? node) { return Stanza.iq(to: entity, type: 'get', children: [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoInfoXmlns, - attributes: node != null ? { 'node': node } : {}, - ) + XMLNode.xmlns( + tag: 'query', + xmlns: discoInfoXmlns, + attributes: node != null ? { 'node': node } : {}, + ) ],); } Stanza buildDiscoItemsQueryStanza(String entity, { String? node }) { return Stanza.iq(to: entity, type: 'get', children: [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoItemsXmlns, - attributes: node != null ? { 'node': node } : {}, - ) + XMLNode.xmlns( + tag: 'query', + xmlns: discoItemsXmlns, + attributes: node != null ? { 'node': node } : {}, + ) ],); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart index 430c6d3..7372df1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart @@ -1,9 +1,10 @@ +import 'package:meta/meta.dart'; import 'package:moxxmpp/src/jid.dart'; +import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart'; class Identity { - const Identity({ required this.category, required this.type, this.name, this.lang }); final String category; final String type; @@ -23,24 +24,96 @@ class Identity { } } +@immutable class DiscoInfo { - const DiscoInfo( this.features, this.identities, this.extendedInfo, + this.node, this.jid, ); + + factory DiscoInfo.fromQuery(XMLNode query, JID jid) { + final features = List.empty(growable: true); + final identities = List.empty(growable: true); + final extendedInfo = List.empty(growable: true); + + for (final element in query.children) { + if (element.tag == 'feature') { + features.add(element.attributes['var']! as String); + } else if (element.tag == 'identity') { + identities.add( + Identity( + category: element.attributes['category']! as String, + type: element.attributes['type']! as String, + name: element.attributes['name'] as String?, + ), + ); + } else if (element.tag == 'x' && element.attributes['xmlns'] == dataFormsXmlns) { + extendedInfo.add( + parseDataForm(element), + ); + } + } + + return DiscoInfo( + features, + identities, + extendedInfo, + query.attributes['node'] as String?, + jid, + ); + } + final List features; final List identities; final List extendedInfo; - final JID jid; + final String? node; + final JID? jid; + + XMLNode toXml() { + return XMLNode.xmlns( + tag: 'query', + xmlns: discoInfoXmlns, + attributes: node != null ? + { 'node': node!, } : + {}, + children: [ + ...identities.map((identity) => identity.toXMLNode()), + ...features.map((feature) => XMLNode( + tag: 'feature', + attributes: { 'var': feature, }, + ),), + + if (extendedInfo.isNotEmpty) + ...extendedInfo.map((ei) => ei.toXml()), + ], + ); + } } +@immutable class DiscoItem { - const DiscoItem({ required this.jid, this.node, this.name }); final String jid; final String? node; final String? name; + + XMLNode toXml() { + final attributes = { + 'jid': jid, + }; + if (node != null) { + attributes['node'] = node!; + } + if (name != null) { + attributes['name'] = name!; + } + + return XMLNode( + tag: 'node', + attributes: attributes, + ); + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart index e446db5..696c50c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart @@ -7,17 +7,21 @@ import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart'; -import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; -import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0115.dart'; import 'package:synchronized/synchronized.dart'; +/// Callback that is called when a disco#info requests is received on a given node. +typedef DiscoInfoRequestCallback = Future Function(); + +/// Callback that is called when a disco#items requests is received on a given node. +typedef DiscoItemsRequestCallback = Future> Function(); + @immutable class DiscoCacheKey { const DiscoCacheKey(this.jid, this.node); @@ -33,33 +37,49 @@ class DiscoCacheKey { int get hashCode => jid.hashCode ^ node.hashCode; } +/// This manager implements XEP-0030 by providing a way of performing disco#info and +/// disco#items requests and answering those requests. +/// A caching mechanism is also provided. class DiscoManager extends XmppManagerBase { - DiscoManager() - : _features = List.empty(growable: true), - _capHashCache = {}, - _capHashInfoCache = {}, - _discoInfoCache = {}, - _runningInfoQueries = {}, - _cacheLock = Lock(), + /// [identities] is a list of disco identities that should be added by default + /// to a disco#info response. + DiscoManager(List identities) + : _identities = List.from(identities), super(); - /// Our features - final List _features; + /// Our features + final List _features = List.empty(growable: true); + + /// Disco identities that we advertise + final List _identities; + /// Map full JID to Capability hashes - final Map _capHashCache; + final Map _capHashCache = {}; /// Map capability hash to the disco info - final Map _capHashInfoCache; + final Map _capHashInfoCache = {}; /// Map full JID to Disco Info - final Map _discoInfoCache; + final Map _discoInfoCache = {}; /// Mapping the full JID to a list of running requests - final Map>>> _runningInfoQueries; + final Map>>> _runningInfoQueries = {}; /// Cache lock - final Lock _cacheLock; + final Lock _cacheLock = Lock(); + /// disco#info callbacks: node -> Callback + final Map _discoInfoCallbacks = {}; + + /// disco#items callbacks: node -> Callback + final Map _discoItemsCallbacks = {}; + + /// The list of identities that are registered. + List get identities => _identities; + + /// The list of disco features that are registered. + List get features => _features; + @visibleForTesting bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty; @@ -105,10 +125,20 @@ class DiscoManager extends XmppManagerBase { }); } } + + /// Register a callback [callback] for a disco#info query on [node]. + void registerInfoCallback(String node, DiscoInfoRequestCallback callback) { + _discoInfoCallbacks[node] = callback; + } + + /// Register a callback [callback] for a disco#items query on [node]. + void registerItemsCallback(String node, DiscoItemsRequestCallback callback) { + _discoItemsCallbacks[node] = callback; + } /// Adds a list of features to the possible disco info response. /// This function only adds features that are not already present in the disco features. - void addDiscoFeatures(List features) { + void addFeatures(List features) { for (final feat in features) { if (!_features.contains(feat)) { _features.add(feat); @@ -116,6 +146,16 @@ class DiscoManager extends XmppManagerBase { } } + /// Adds a list of identities to the possible disco info response. + /// This function only adds features that are not already present in the disco features. + void addIdentities(List identities) { + for (final identity in identities) { + if (!_identities.contains(identity)) { + _identities.add(identity); + } + } + } + Future _onPresence(JID from, Stanza presence) async { final c = presence.firstTag('c', xmlns: capsXmlns); if (c == null) return; @@ -146,45 +186,33 @@ class DiscoManager extends XmppManagerBase { }); } - /// Returns the list of disco features registered. - List getRegisteredDiscoFeatures() => _features; - - /// May be overriden. Specifies the identities which will be returned in a disco info response. - List getIdentities() => const [ Identity(category: 'client', type: 'pc', name: 'moxxmpp', lang: 'en') ]; + /// Returns the [DiscoInfo] object that would be used as the response to a disco#info + /// query against our bare JID with no node. The results node attribute is set + /// to [node]. + DiscoInfo getDiscoInfo(String? node) { + return DiscoInfo( + _features, + _identities, + const [], + node, + null, + ); + } Future _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { if (stanza.type != 'get') return state; - final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager; final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!; final node = query.attributes['node'] as String?; - final capHash = await presence.getCapabilityHash(); - final isCapabilityNode = node == '${presence.capabilityHashNode}#$capHash'; - if (!isCapabilityNode && node != null) { + if (_discoInfoCallbacks.containsKey(node)) { + // We can now assume that node != null + final result = await _discoInfoCallbacks[node]!(); await reply( state, - 'error', + 'result', [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoInfoXmlns, - attributes: { - 'node': node - }, - ), - XMLNode( - tag: 'error', - attributes: { - 'type': 'cancel' - }, - children: [ - XMLNode.xmlns( - tag: 'not-allowed', - xmlns: fullStanzaXmlns, - ) - ], - ), + result.toXml(), ], ); @@ -195,24 +223,7 @@ class DiscoManager extends XmppManagerBase { state, 'result', [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoInfoXmlns, - attributes: { - ...!isCapabilityNode ? {} : { - 'node': '${presence.capabilityHashNode}#$capHash' - } - }, - children: [ - ...getIdentities().map((identity) => identity.toXMLNode()), - ..._features.map((feat) { - return XMLNode( - tag: 'feature', - attributes: { 'var': feat }, - ); - }), - ], - ), + getDiscoInfo(node).toXml(), ], ); @@ -223,30 +234,20 @@ class DiscoManager extends XmppManagerBase { if (stanza.type != 'get') return state; final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!; - if (query.attributes['node'] != null) { - // TODO(Unknown): Handle the node we specified for XEP-0115 + final node = query.attributes['node'] as String?; + if (_discoItemsCallbacks.containsKey(node)) { + final result = await _discoItemsCallbacks[node]!(); await reply( state, - 'error', + 'result', [ XMLNode.xmlns( tag: 'query', xmlns: discoItemsXmlns, attributes: { - 'node': query.attributes['node']! as String, + 'node': node!, }, - ), - XMLNode( - tag: 'error', - attributes: { - 'type': 'cancel' - }, - children: [ - XMLNode.xmlns( - tag: 'not-allowed', - xmlns: fullStanzaXmlns, - ), - ], + children: result.map((item) => item.toXml()).toList(), ), ], ); @@ -254,18 +255,7 @@ class DiscoManager extends XmppManagerBase { return state.copyWith(done: true); } - await reply( - state, - 'result', - [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoItemsXmlns, - ), - ], - ); - - return state.copyWith(done: true); + return state; } Future _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result result) async { @@ -322,34 +312,17 @@ class DiscoManager extends XmppManagerBase { return result; } - final error = stanza.firstTag('error'); - if (error != null && stanza.attributes['type'] == 'error') { + if (stanza.attributes['type'] == 'error') { + //final error = stanza.firstTag('error'); final result = Result(ErrorResponseDiscoError()); await _exitDiscoInfoCriticalSection(cacheKey, result); return result; } - final features = List.empty(growable: true); - final identities = List.empty(growable: true); - - for (final element in query.children) { - if (element.tag == 'feature') { - features.add(element.attributes['var']! as String); - } else if (element.tag == 'identity') { - identities.add(Identity( - category: element.attributes['category']! as String, - type: element.attributes['type']! as String, - name: element.attributes['name'] as String?, - ),); - } - } - final result = Result( - DiscoInfo( - features, - identities, - query.findTags('x', xmlns: dataFormsXmlns).map(parseDataForm).toList(), - JID.fromString(stanza.attributes['from']! as String), + DiscoInfo.fromQuery( + query, + JID.fromString(entity), ), ); await _exitDiscoInfoCriticalSection(cacheKey, result); @@ -367,8 +340,8 @@ class DiscoManager extends XmppManagerBase { final query = stanza.firstTag('query'); if (query == null) return Result(InvalidResponseDiscoError()); - final error = stanza.firstTag('error'); - if (error != null && stanza.type == 'error') { + if (stanza.type == 'error') { + //final error = stanza.firstTag('error'); //print("Disco Items error: " + error.toXml()); return Result(ErrorResponseDiscoError()); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0115.dart b/packages/moxxmpp/lib/src/xeps/xep_0115.dart index e29e272..6b21d62 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0115.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0115.dart @@ -1,10 +1,18 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; +import 'package:meta/meta.dart'; +import 'package:moxxmpp/src/managers/base.dart'; +import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; +import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; +import 'package:moxxmpp/src/xeps/xep_0414.dart'; +@immutable class CapabilityHashInfo { - const CapabilityHashInfo(this.ver, this.node, this.hash); final String ver; final String node; @@ -57,3 +65,86 @@ Future calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm) return base64.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes); } + +/// A manager implementing the advertising of XEP-0115. It responds to the +/// disco#info requests on the specified node with the information provided by +/// the DiscoManager. +/// NOTE: This manager requires that the DiscoManager is also registered. +class EntityCapabilitiesManager extends XmppManagerBase { + EntityCapabilitiesManager(this._capabilityHashBase) : super(); + + /// The string that is both the node under which we advertise the disco info + /// and the base for the actual node on which we respond to disco#info requests. + final String _capabilityHashBase; + + /// The cached capability hash. + String? _capabilityHash; + + @override + String getName() => 'EntityCapabilitiesManager'; + + @override + String getId() => entityCapabilitiesManager; + + @override + Future isSupported() async => true; + + @override + List getDiscoFeatures() => [ + capsXmlns, + ]; + + /// Computes, if required, the capability hash of the data provided by + /// the DiscoManager. + Future getCapabilityHash() async { + _capabilityHash ??= await calculateCapabilityHash( + getAttributes() + .getManagerById(discoManager)! + .getDiscoInfo(null), + getHashByName('sha-1')!, + ); + + return _capabilityHash!; + } + + Future _getNode() async { + final hash = await getCapabilityHash(); + return '$_capabilityHashBase#$hash'; + } + + Future _onInfoQuery() async { + return getAttributes() + .getManagerById(discoManager)! + .getDiscoInfo(await _getNode()); + } + + Future> _prePresenceSent() async { + return [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': _capabilityHashBase, + 'ver': await getCapabilityHash(), + }, + ), + ]; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + getAttributes().getManagerById(discoManager)!.registerInfoCallback( + await _getNode(), + _onInfoQuery, + ); + + getAttributes() + .getManagerById(presenceManager)! + .registerPreSendCallback( + _prePresenceSent, + ); + } +} diff --git a/packages/moxxmpp/test/negotiator_test.dart b/packages/moxxmpp/test/negotiator_test.dart index 228992e..37be7d1 100644 --- a/packages/moxxmpp/test/negotiator_test.dart +++ b/packages/moxxmpp/test/negotiator_test.dart @@ -63,10 +63,11 @@ void main() { StubNegotiator2(), ]) ..registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]) ..setConnectionSettings( ConnectionSettings( diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index fb96588..bc08635 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -53,7 +53,7 @@ void main() { ignoreId: true, ), StringExpectation( - "chat", + "chat", '', ), StanzaExpectation( @@ -77,10 +77,11 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager(null, [])), - DiscoManager(), + DiscoManager([]), PingManager(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators( [ diff --git a/packages/moxxmpp/test/xeps/xep_0115_test.dart b/packages/moxxmpp/test/xeps/xep_0115_test.dart index a0dab4e..cfa3969 100644 --- a/packages/moxxmpp/test/xeps/xep_0115_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0115_test.dart @@ -19,6 +19,7 @@ void main() { ) ], [], + null, JID.fromString('some@user.local/test'), ); @@ -50,6 +51,7 @@ void main() { ), ], [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], + null, JID.fromString('some@user.local/test'), ); @@ -158,6 +160,7 @@ void main() { ) ], [], + null, JID.fromString('user@server.local/test'), ); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 8ad8a23..17ddd78 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -246,12 +246,13 @@ void main() { ),); final sm = StreamManagementManager(); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), - RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), - PingManager(), - sm, - CarbonsManager()..forceEnable(), + PresenceManager(), + RosterManager(TestingRosterStateManager('', [])), + DiscoManager([]), + PingManager(), + sm, + CarbonsManager()..forceEnable(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators( [ @@ -347,7 +348,7 @@ void main() { '', ), StringExpectation( - "chat", + "chat", '', ), StanzaExpectation( @@ -372,12 +373,13 @@ void main() { ),); final sm = StreamManagementManager(); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), sm, CarbonsManager()..forceEnable(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators( [ @@ -530,9 +532,9 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), StreamManagementManager(), ]); @@ -626,9 +628,9 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), StreamManagementManager(), ]); @@ -722,9 +724,9 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), StreamManagementManager(), ]); diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index b552471..b70cfc3 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -129,11 +129,12 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), StreamManagementManager(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators( [ @@ -187,10 +188,11 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators([ SaslPlainNegotiator() @@ -245,10 +247,11 @@ void main() { allowPlainAuth: true, ),); conn.registerManagers([ - PresenceManager('http://moxxmpp.example'), + PresenceManager(), RosterManager(TestingRosterStateManager('', [])), - DiscoManager(), + DiscoManager([]), PingManager(), + EntityCapabilitiesManager('http://moxxmpp.example'), ]); conn.registerFeatureNegotiators([ SaslPlainNegotiator()