feat: Improve the API provided by the DiscoManager

Fixes #21.
This commit is contained in:
PapaTutuWawa 2023-01-27 16:26:01 +01:00
parent bff4a6f707
commit 1bd61076ea
14 changed files with 396 additions and 249 deletions

View File

@ -223,63 +223,47 @@ class XmppConnection {
/// none can be found. /// none can be found.
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) => _featureNegotiators[id] as T?; T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) => _featureNegotiators[id] as T?;
/// Registers an [XmppManagerBase] sub-class as a manager on this connection. /// Registers a list of [XmppManagerBase] sub-classes as managers on this connection.
/// [sortHandlers] should NOT be touched. It specified if the handler priorities Future<void> registerManagers(List<XmppManagerBase> managers) async {
/// 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<XmppManagerBase> managers) {
for (final manager in managers) { 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 // Sort them
_incomingStanzaHandlers.sort(stanzaHandlerSortComparator); _incomingStanzaHandlers.sort(stanzaHandlerSortComparator);
_incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator); _outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPostStanzaHandlers.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. /// Register a list of negotiator with the connection.

View File

@ -1,14 +1,21 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.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/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
abstract class XmppManagerBase { abstract class XmppManagerBase {
late final XmppManagerAttributes _managerAttributes; late final XmppManagerAttributes _managerAttributes;
late final Logger _log; 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 /// Registers the callbacks from XmppConnection with the manager
void register(XmppManagerAttributes attributes) { void register(XmppManagerAttributes attributes) {
_managerAttributes = attributes; _managerAttributes = attributes;
@ -48,6 +55,9 @@ abstract class XmppManagerBase {
/// Return a list of features that should be included in a disco response. /// Return a list of features that should be included in a disco response.
List<String> getDiscoFeatures() => []; List<String> getDiscoFeatures() => [];
/// Return a list of identities that should be included in a disco response.
List<Identity> getDiscoIdentities() => [];
/// Return the Id (akin to xmlns) of this manager. /// Return the Id (akin to xmlns) of this manager.
String getId(); String getId();
@ -63,6 +73,24 @@ abstract class XmppManagerBase {
/// Returns true if the XEP is supported on the server. If not, returns false /// Returns true if the XEP is supported on the server. If not, returns false
Future<bool> isSupported(); Future<bool> 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<void> postRegisterCallback() async {
initialized = true;
final disco = getAttributes().getManagerById<DiscoManager>(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 /// 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. /// the nonza has been handled by one of the handlers. Resolves to false otherwise.

View File

@ -28,3 +28,4 @@ const messageRetractionManager = 'org.moxxmpp.messageretractionmanager';
const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager'; const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';

View File

@ -8,17 +8,19 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.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<List<XMLNode>> 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 { class PresenceManager extends XmppManagerBase {
PresenceManager(this._capHashNode) : _capabilityHash = null, super(); PresenceManager() : super();
String? _capabilityHash;
final String _capHashNode;
String get capabilityHashNode => _capHashNode; /// The list of pre-send callbacks.
final List<PresencePreSendCallback> _presenceCallbacks = List.empty(growable: true);
@override @override
String getId() => presenceManager; String getId() => presenceManager;
@ -39,6 +41,11 @@ class PresenceManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
/// Register the pre-send callback [callback].
void registerPreSendCallback(PresencePreSendCallback callback) {
_presenceCallbacks.add(callback);
}
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async {
final attrs = getAttributes(); final attrs = getAttributes();
@ -63,43 +70,26 @@ class PresenceManager extends XmppManagerBase {
return state; return state;
} }
/// Returns the capability hash.
Future<String> 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. /// Sends the initial presence to enable receiving messages.
Future<void> sendInitialPresence() async { Future<void> sendInitialPresence() async {
final children = List<XMLNode>.from([
XMLNode(
tag: 'show',
text: 'chat',
),
]);
for (final callback in _presenceCallbacks) {
children.addAll(
await callback(),
);
}
final attrs = getAttributes(); final attrs = getAttributes();
attrs.sendNonza( attrs.sendNonza(
Stanza.presence( Stanza.presence(
from: attrs.getFullJID().toString(), from: attrs.getFullJID().toString(),
children: [ children: children,
XMLNode(
tag: 'show',
text: 'chat',
),
XMLNode.xmlns(
tag: 'c',
xmlns: capsXmlns,
attributes: {
'hash': 'sha-1',
'node': _capHashNode,
'ver': await getCapabilityHash()
},
)
],
), ),
); );
} }

View File

@ -3,7 +3,6 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
class DataFormOption { class DataFormOption {
const DataFormOption({ required this.value, this.label }); const DataFormOption({ required this.value, this.label });
final String? label; final String? label;
final String value; final String value;
@ -23,7 +22,6 @@ class DataFormOption {
} }
class DataFormField { class DataFormField {
const DataFormField({ const DataFormField({
required this.options, required this.options,
required this.values, required this.values,
@ -60,7 +58,6 @@ class DataFormField {
} }
class DataForm { class DataForm {
const DataForm({ const DataForm({
required this.type, required this.type,
required this.instructions, required this.instructions,

View File

@ -6,20 +6,20 @@ import 'package:moxxmpp/src/stringxml.dart';
Stanza buildDiscoInfoQueryStanza(String entity, String? node) { Stanza buildDiscoInfoQueryStanza(String entity, String? node) {
return Stanza.iq(to: entity, type: 'get', children: [ return Stanza.iq(to: entity, type: 'get', children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoInfoXmlns, xmlns: discoInfoXmlns,
attributes: node != null ? { 'node': node } : {}, attributes: node != null ? { 'node': node } : {},
) )
],); ],);
} }
Stanza buildDiscoItemsQueryStanza(String entity, { String? node }) { Stanza buildDiscoItemsQueryStanza(String entity, { String? node }) {
return Stanza.iq(to: entity, type: 'get', children: [ return Stanza.iq(to: entity, type: 'get', children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: node != null ? { 'node': node } : {}, attributes: node != null ? { 'node': node } : {},
) )
],); ],);
} }

View File

@ -1,9 +1,10 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart';
class Identity { class Identity {
const Identity({ required this.category, required this.type, this.name, this.lang }); const Identity({ required this.category, required this.type, this.name, this.lang });
final String category; final String category;
final String type; final String type;
@ -23,24 +24,96 @@ class Identity {
} }
} }
@immutable
class DiscoInfo { class DiscoInfo {
const DiscoInfo( const DiscoInfo(
this.features, this.features,
this.identities, this.identities,
this.extendedInfo, this.extendedInfo,
this.node,
this.jid, this.jid,
); );
factory DiscoInfo.fromQuery(XMLNode query, JID jid) {
final features = List<String>.empty(growable: true);
final identities = List<Identity>.empty(growable: true);
final extendedInfo = List<DataForm>.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<String> features; final List<String> features;
final List<Identity> identities; final List<Identity> identities;
final List<DataForm> extendedInfo; final List<DataForm> extendedInfo;
final JID jid; final String? node;
final JID? jid;
XMLNode toXml() {
return XMLNode.xmlns(
tag: 'query',
xmlns: discoInfoXmlns,
attributes: node != null ?
<String, String>{ 'node': node!, } :
<String, String>{},
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 { class DiscoItem {
const DiscoItem({ required this.jid, this.node, this.name }); const DiscoItem({ required this.jid, this.node, this.name });
final String jid; final String jid;
final String? node; final String? node;
final String? name; 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,
);
}
} }

View File

@ -7,17 +7,21 @@ import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/presence.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/helpers.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_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0115.dart'; import 'package:moxxmpp/src/xeps/xep_0115.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// Callback that is called when a disco#info requests is received on a given node.
typedef DiscoInfoRequestCallback = Future<DiscoInfo> Function();
/// Callback that is called when a disco#items requests is received on a given node.
typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
@immutable @immutable
class DiscoCacheKey { class DiscoCacheKey {
const DiscoCacheKey(this.jid, this.node); const DiscoCacheKey(this.jid, this.node);
@ -33,33 +37,49 @@ class DiscoCacheKey {
int get hashCode => jid.hashCode ^ node.hashCode; 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 { class DiscoManager extends XmppManagerBase {
DiscoManager() /// [identities] is a list of disco identities that should be added by default
: _features = List.empty(growable: true), /// to a disco#info response.
_capHashCache = {}, DiscoManager(List<Identity> identities)
_capHashInfoCache = {}, : _identities = List<Identity>.from(identities),
_discoInfoCache = {},
_runningInfoQueries = {},
_cacheLock = Lock(),
super(); super();
/// Our features
final List<String> _features;
/// Our features
final List<String> _features = List.empty(growable: true);
/// Disco identities that we advertise
final List<Identity> _identities;
/// Map full JID to Capability hashes /// Map full JID to Capability hashes
final Map<String, CapabilityHashInfo> _capHashCache; final Map<String, CapabilityHashInfo> _capHashCache = {};
/// Map capability hash to the disco info /// Map capability hash to the disco info
final Map<String, DiscoInfo> _capHashInfoCache; final Map<String, DiscoInfo> _capHashInfoCache = {};
/// Map full JID to Disco Info /// Map full JID to Disco Info
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache; final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// Mapping the full JID to a list of running requests /// Mapping the full JID to a list of running requests
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries; final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries = {};
/// Cache lock /// Cache lock
final Lock _cacheLock; final Lock _cacheLock = Lock();
/// disco#info callbacks: node -> Callback
final Map<String, DiscoInfoRequestCallback> _discoInfoCallbacks = {};
/// disco#items callbacks: node -> Callback
final Map<String, DiscoItemsRequestCallback> _discoItemsCallbacks = {};
/// The list of identities that are registered.
List<Identity> get identities => _identities;
/// The list of disco features that are registered.
List<String> get features => _features;
@visibleForTesting @visibleForTesting
bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty; 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. /// 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. /// This function only adds features that are not already present in the disco features.
void addDiscoFeatures(List<String> features) { void addFeatures(List<String> features) {
for (final feat in features) { for (final feat in features) {
if (!_features.contains(feat)) { if (!_features.contains(feat)) {
_features.add(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<Identity> identities) {
for (final identity in identities) {
if (!_identities.contains(identity)) {
_identities.add(identity);
}
}
}
Future<void> _onPresence(JID from, Stanza presence) async { Future<void> _onPresence(JID from, Stanza presence) async {
final c = presence.firstTag('c', xmlns: capsXmlns); final c = presence.firstTag('c', xmlns: capsXmlns);
if (c == null) return; if (c == null) return;
@ -146,45 +186,33 @@ class DiscoManager extends XmppManagerBase {
}); });
} }
/// Returns the list of disco features registered. /// Returns the [DiscoInfo] object that would be used as the response to a disco#info
List<String> getRegisteredDiscoFeatures() => _features; /// query against our bare JID with no node. The results node attribute is set
/// to [node].
/// May be overriden. Specifies the identities which will be returned in a disco info response. DiscoInfo getDiscoInfo(String? node) {
List<Identity> getIdentities() => const [ Identity(category: 'client', type: 'pc', name: 'moxxmpp', lang: 'en') ]; return DiscoInfo(
_features,
_identities,
const [],
node,
null,
);
}
Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager;
final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!; final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!;
final node = query.attributes['node'] as String?; 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( await reply(
state, state,
'error', 'result',
[ [
XMLNode.xmlns( result.toXml(),
tag: 'query',
xmlns: discoInfoXmlns,
attributes: <String, String>{
'node': node
},
),
XMLNode(
tag: 'error',
attributes: <String, String>{
'type': 'cancel'
},
children: [
XMLNode.xmlns(
tag: 'not-allowed',
xmlns: fullStanzaXmlns,
)
],
),
], ],
); );
@ -195,24 +223,7 @@ class DiscoManager extends XmppManagerBase {
state, state,
'result', 'result',
[ [
XMLNode.xmlns( getDiscoInfo(node).toXml(),
tag: 'query',
xmlns: discoInfoXmlns,
attributes: {
...!isCapabilityNode ? {} : {
'node': '${presence.capabilityHashNode}#$capHash'
}
},
children: [
...getIdentities().map((identity) => identity.toXMLNode()),
..._features.map((feat) {
return XMLNode(
tag: 'feature',
attributes: <String, dynamic>{ 'var': feat },
);
}),
],
),
], ],
); );
@ -223,30 +234,20 @@ class DiscoManager extends XmppManagerBase {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!; final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!;
if (query.attributes['node'] != null) { final node = query.attributes['node'] as String?;
// TODO(Unknown): Handle the node we specified for XEP-0115 if (_discoItemsCallbacks.containsKey(node)) {
final result = await _discoItemsCallbacks[node]!();
await reply( await reply(
state, state,
'error', 'result',
[ [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: <String, String>{ attributes: <String, String>{
'node': query.attributes['node']! as String, 'node': node!,
}, },
), children: result.map((item) => item.toXml()).toList(),
XMLNode(
tag: 'error',
attributes: <String, dynamic>{
'type': 'cancel'
},
children: [
XMLNode.xmlns(
tag: 'not-allowed',
xmlns: fullStanzaXmlns,
),
],
), ),
], ],
); );
@ -254,18 +255,7 @@ class DiscoManager extends XmppManagerBase {
return state.copyWith(done: true); return state.copyWith(done: true);
} }
await reply( return state;
state,
'result',
[
XMLNode.xmlns(
tag: 'query',
xmlns: discoItemsXmlns,
),
],
);
return state.copyWith(done: true);
} }
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async { Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
@ -322,34 +312,17 @@ class DiscoManager extends XmppManagerBase {
return result; return result;
} }
final error = stanza.firstTag('error'); if (stanza.attributes['type'] == 'error') {
if (error != null && stanza.attributes['type'] == 'error') { //final error = stanza.firstTag('error');
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError()); final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result);
return result; return result;
} }
final features = List<String>.empty(growable: true);
final identities = List<Identity>.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<DiscoError, DiscoInfo>( final result = Result<DiscoError, DiscoInfo>(
DiscoInfo( DiscoInfo.fromQuery(
features, query,
identities, JID.fromString(entity),
query.findTags('x', xmlns: dataFormsXmlns).map(parseDataForm).toList(),
JID.fromString(stanza.attributes['from']! as String),
), ),
); );
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result);
@ -367,8 +340,8 @@ class DiscoManager extends XmppManagerBase {
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) return Result(InvalidResponseDiscoError()); if (query == null) return Result(InvalidResponseDiscoError());
final error = stanza.firstTag('error'); if (stanza.type == 'error') {
if (error != null && stanza.type == 'error') { //final error = stanza.firstTag('error');
//print("Disco Items error: " + error.toXml()); //print("Disco Items error: " + error.toXml());
return Result(ErrorResponseDiscoError()); return Result(ErrorResponseDiscoError());
} }

View File

@ -1,10 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cryptography/cryptography.dart'; 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/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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0414.dart';
@immutable
class CapabilityHashInfo { class CapabilityHashInfo {
const CapabilityHashInfo(this.ver, this.node, this.hash); const CapabilityHashInfo(this.ver, this.node, this.hash);
final String ver; final String ver;
final String node; final String node;
@ -57,3 +65,86 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
return base64.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes); 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<bool> isSupported() async => true;
@override
List<String> getDiscoFeatures() => [
capsXmlns,
];
/// Computes, if required, the capability hash of the data provided by
/// the DiscoManager.
Future<String> getCapabilityHash() async {
_capabilityHash ??= await calculateCapabilityHash(
getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.getDiscoInfo(null),
getHashByName('sha-1')!,
);
return _capabilityHash!;
}
Future<String> _getNode() async {
final hash = await getCapabilityHash();
return '$_capabilityHashBase#$hash';
}
Future<DiscoInfo> _onInfoQuery() async {
return getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.getDiscoInfo(await _getNode());
}
Future<List<XMLNode>> _prePresenceSent() async {
return [
XMLNode.xmlns(
tag: 'c',
xmlns: capsXmlns,
attributes: {
'hash': 'sha-1',
'node': _capabilityHashBase,
'ver': await getCapabilityHash(),
},
),
];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
getAttributes().getManagerById<DiscoManager>(discoManager)!.registerInfoCallback(
await _getNode(),
_onInfoQuery,
);
getAttributes()
.getManagerById<PresenceManager>(presenceManager)!
.registerPreSendCallback(
_prePresenceSent,
);
}
}

View File

@ -63,10 +63,11 @@ void main() {
StubNegotiator2(), StubNegotiator2(),
]) ])
..registerManagers([ ..registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]) ])
..setConnectionSettings( ..setConnectionSettings(
ConnectionSettings( ConnectionSettings(

View File

@ -53,7 +53,7 @@ void main() {
ignoreId: true, ignoreId: true,
), ),
StringExpectation( StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxmpp.example' ver='QRTBC5cg/oYd+UOTYazSQR4zb/I=' /></presence>", "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxmpp.example' ver='3QvQ2RAy45XBDhArjxy/vEWMl+E=' /></presence>",
'', '',
), ),
StanzaExpectation( StanzaExpectation(
@ -77,10 +77,11 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager(null, [])), RosterManager(TestingRosterStateManager(null, [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators( conn.registerFeatureNegotiators(
[ [

View File

@ -19,6 +19,7 @@ void main() {
) )
], ],
[], [],
null,
JID.fromString('some@user.local/test'), JID.fromString('some@user.local/test'),
); );
@ -50,6 +51,7 @@ void main() {
), ),
], ],
[ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ],
null,
JID.fromString('some@user.local/test'), JID.fromString('some@user.local/test'),
); );
@ -158,6 +160,7 @@ void main() {
) )
], ],
[], [],
null,
JID.fromString('user@server.local/test'), JID.fromString('user@server.local/test'),
); );

View File

@ -246,12 +246,13 @@ void main() {
),); ),);
final sm = StreamManagementManager(); final sm = StreamManagementManager();
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
sm, sm,
CarbonsManager()..forceEnable(), CarbonsManager()..forceEnable(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators( conn.registerFeatureNegotiators(
[ [
@ -347,7 +348,7 @@ void main() {
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />', '<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
), ),
StringExpectation( StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxmpp.example' ver='QRTBC5cg/oYd+UOTYazSQR4zb/I=' /></presence>", "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxmpp.example' ver='3QvQ2RAy45XBDhArjxy/vEWMl+E=' /></presence>",
'<iq type="result" />', '<iq type="result" />',
), ),
StanzaExpectation( StanzaExpectation(
@ -372,12 +373,13 @@ void main() {
),); ),);
final sm = StreamManagementManager(); final sm = StreamManagementManager();
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
sm, sm,
CarbonsManager()..forceEnable(), CarbonsManager()..forceEnable(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators( conn.registerFeatureNegotiators(
[ [
@ -530,9 +532,9 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
StreamManagementManager(), StreamManagementManager(),
]); ]);
@ -626,9 +628,9 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
StreamManagementManager(), StreamManagementManager(),
]); ]);
@ -722,9 +724,9 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
StreamManagementManager(), StreamManagementManager(),
]); ]);

View File

@ -129,11 +129,12 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
StreamManagementManager(), StreamManagementManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators( conn.registerFeatureNegotiators(
[ [
@ -187,10 +188,11 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ conn.registerFeatureNegotiators([
SaslPlainNegotiator() SaslPlainNegotiator()
@ -245,10 +247,11 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ conn.registerFeatureNegotiators([
SaslPlainNegotiator() SaslPlainNegotiator()