parent
bff4a6f707
commit
1bd61076ea
@ -223,11 +223,9 @@ class XmppConnection {
|
||||
/// none can be found.
|
||||
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(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 }) {
|
||||
/// Registers a list of [XmppManagerBase] sub-classes as managers on this connection.
|
||||
Future<void> registerManagers(List<XmppManagerBase> managers) async {
|
||||
for (final manager in managers) {
|
||||
_log.finest('Registering ${manager.getId()}');
|
||||
manager.register(
|
||||
XmppManagerAttributes(
|
||||
@ -247,39 +245,25 @@ class XmppConnection {
|
||||
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) {
|
||||
registerManager(manager, sortHandlers: false);
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -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;
|
||||
@ -49,6 +56,9 @@ abstract class XmppManagerBase {
|
||||
/// Return a list of features that should be included in a disco response.
|
||||
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.
|
||||
String getId();
|
||||
|
||||
@ -64,6 +74,24 @@ abstract class XmppManagerBase {
|
||||
/// Returns true if the XEP is supported on the server. If not, returns false
|
||||
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
|
||||
/// the nonza has been handled by one of the handlers. Resolves to false otherwise.
|
||||
Future<bool> runNonzaHandlers(XMLNode nonza) async {
|
||||
|
@ -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';
|
||||
|
@ -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<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 {
|
||||
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<PresencePreSendCallback> _presenceCallbacks = List.empty(growable: true);
|
||||
|
||||
@override
|
||||
String getId() => presenceManager;
|
||||
@ -40,6 +42,11 @@ class PresenceManager extends XmppManagerBase {
|
||||
@override
|
||||
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 {
|
||||
final attrs = getAttributes();
|
||||
switch (presence.type) {
|
||||
@ -63,43 +70,26 @@ class PresenceManager extends XmppManagerBase {
|
||||
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.
|
||||
Future<void> sendInitialPresence() async {
|
||||
final attrs = getAttributes();
|
||||
attrs.sendNonza(
|
||||
Stanza.presence(
|
||||
from: attrs.getFullJID().toString(),
|
||||
children: [
|
||||
final children = List<XMLNode>.from([
|
||||
XMLNode(
|
||||
tag: 'show',
|
||||
text: 'chat',
|
||||
),
|
||||
XMLNode.xmlns(
|
||||
tag: 'c',
|
||||
xmlns: capsXmlns,
|
||||
attributes: {
|
||||
'hash': 'sha-1',
|
||||
'node': _capHashNode,
|
||||
'ver': await getCapabilityHash()
|
||||
},
|
||||
)
|
||||
],
|
||||
]);
|
||||
|
||||
for (final callback in _presenceCallbacks) {
|
||||
children.addAll(
|
||||
await callback(),
|
||||
);
|
||||
}
|
||||
|
||||
final attrs = getAttributes();
|
||||
attrs.sendNonza(
|
||||
Stanza.presence(
|
||||
from: attrs.getFullJID().toString(),
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<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<Identity> identities;
|
||||
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 {
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<DiscoInfo> Function();
|
||||
|
||||
/// Callback that is called when a disco#items requests is received on a given node.
|
||||
typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
|
||||
|
||||
@immutable
|
||||
class DiscoCacheKey {
|
||||
const DiscoCacheKey(this.jid, this.node);
|
||||
@ -33,32 +37,48 @@ 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<Identity> identities)
|
||||
: _identities = List<Identity>.from(identities),
|
||||
super();
|
||||
|
||||
/// Our features
|
||||
final List<String> _features;
|
||||
final List<String> _features = List.empty(growable: true);
|
||||
|
||||
/// Disco identities that we advertise
|
||||
final List<Identity> _identities;
|
||||
|
||||
/// Map full JID to Capability hashes
|
||||
final Map<String, CapabilityHashInfo> _capHashCache;
|
||||
final Map<String, CapabilityHashInfo> _capHashCache = {};
|
||||
|
||||
/// Map capability hash to the disco info
|
||||
final Map<String, DiscoInfo> _capHashInfoCache;
|
||||
final Map<String, DiscoInfo> _capHashInfoCache = {};
|
||||
|
||||
/// 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
|
||||
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries;
|
||||
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries = {};
|
||||
|
||||
/// 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
|
||||
bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty;
|
||||
@ -106,9 +126,19 @@ 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<String> features) {
|
||||
void addFeatures(List<String> 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<Identity> identities) {
|
||||
for (final identity in identities) {
|
||||
if (!_identities.contains(identity)) {
|
||||
_identities.add(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<String> getRegisteredDiscoFeatures() => _features;
|
||||
|
||||
/// May be overriden. Specifies the identities which will be returned in a disco info response.
|
||||
List<Identity> 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<StanzaHandlerData> _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: <String, String>{
|
||||
'node': node
|
||||
},
|
||||
),
|
||||
XMLNode(
|
||||
tag: 'error',
|
||||
attributes: <String, String>{
|
||||
'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: <String, dynamic>{ 'var': feat },
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
getDiscoInfo(node).toXml(),
|
||||
],
|
||||
);
|
||||
|
||||
@ -223,37 +234,9 @@ 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
|
||||
await reply(
|
||||
state,
|
||||
'error',
|
||||
[
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: discoItemsXmlns,
|
||||
attributes: <String, String>{
|
||||
'node': query.attributes['node']! as String,
|
||||
},
|
||||
),
|
||||
XMLNode(
|
||||
tag: 'error',
|
||||
attributes: <String, dynamic>{
|
||||
'type': 'cancel'
|
||||
},
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'not-allowed',
|
||||
xmlns: fullStanzaXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
}
|
||||
|
||||
final node = query.attributes['node'] as String?;
|
||||
if (_discoItemsCallbacks.containsKey(node)) {
|
||||
final result = await _discoItemsCallbacks[node]!();
|
||||
await reply(
|
||||
state,
|
||||
'result',
|
||||
@ -261,6 +244,10 @@ class DiscoManager extends XmppManagerBase {
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: discoItemsXmlns,
|
||||
attributes: <String, String>{
|
||||
'node': node!,
|
||||
},
|
||||
children: result.map((item) => item.toXml()).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -268,6 +255,9 @@ class DiscoManager extends XmppManagerBase {
|
||||
return state.copyWith(done: true);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
|
||||
return _cacheLock.synchronized(() async {
|
||||
// Complete all futures
|
||||
@ -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<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, 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>(
|
||||
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());
|
||||
}
|
||||
|
@ -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<String> 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<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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +63,11 @@ void main() {
|
||||
StubNegotiator2(),
|
||||
])
|
||||
..registerManagers([
|
||||
PresenceManager('http://moxxmpp.example'),
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager(),
|
||||
DiscoManager([]),
|
||||
PingManager(),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
])
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
|
@ -53,7 +53,7 @@ void main() {
|
||||
ignoreId: true,
|
||||
),
|
||||
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(
|
||||
@ -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(
|
||||
[
|
||||
|
@ -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'),
|
||||
);
|
||||
|
||||
|
@ -246,12 +246,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(
|
||||
[
|
||||
@ -347,7 +348,7 @@ void main() {
|
||||
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
|
||||
),
|
||||
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" />',
|
||||
),
|
||||
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(),
|
||||
]);
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user