18 Commits

Author SHA1 Message Date
098687de45 feat: Bump moxxmpp version to 0.2.0 2023-01-27 21:57:26 +01:00
6da3342f22 feat: Make defining managers better 2023-01-27 21:54:16 +01:00
47337540f5 feat: Factor out "multiple-waiting" into its own thing 2023-01-27 21:14:35 +01:00
7e588f01b0 feat: Add DOAP
Fixes #11.
2023-01-27 19:09:05 +01:00
c7c6c9dae4 feat: Update Message Replies to 0.2.0
Fixes #22.
2023-01-27 19:08:57 +01:00
c77cfc4dcd feat: Change namespaces 2023-01-27 18:34:13 +01:00
1bd61076ea feat: Improve the API provided by the DiscoManager
Fixes #21.
2023-01-27 16:26:01 +01:00
bff4a6f707 feat: Rework how the ReconnectionPolicy system works 2023-01-27 00:14:44 +01:00
1cc266c675 fix: Just use shouldEncryptElement for the envelope "validation" 2023-01-23 13:11:07 +01:00
72099dfde5 feat: Only add envelope elements that should be encrypted 2023-01-23 13:10:07 +01:00
c9c45baabc feat: Allow easier responding to incoming stanzas
Should fix #20.
2023-01-23 12:47:30 +01:00
a01022c217 feat: Bump omemo_dart 2023-01-22 19:25:52 +01:00
c3459e6820 feat: Always set a cancel reason on failure 2023-01-21 20:46:45 +01:00
e031e6d760 feat: Add cancelReason if recipient likely does not support OMEMO:2 2023-01-21 15:42:00 +01:00
6c63b53cf4 fix: Fix crash with direct server IQs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-15 00:52:34 +01:00
1aa50699ad feat: Improve the stanza await system
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This fixes an issue where publishing an avatar would fail, if returned
an error where the "from" attribute is missing from the stanza.
2023-01-14 16:28:37 +01:00
b2c54ae8c0 ci: Add Woodpecker CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-14 15:00:55 +01:00
b16c9f4b30 docs: Add more doc strings 2023-01-14 15:00:02 +01:00
67 changed files with 1957 additions and 1089 deletions

28
.woodpecker.yml Normal file
View File

@@ -0,0 +1,28 @@
pipeline:
# Check moxxmpp
moxxmpp-lint:
image: dart:2.18.1
commands:
- cd packages/moxxmpp
- dart pub get
- dart analyze --fatal-infos --fatal-warnings
moxxmpp-test:
image: dart:2.18.1
commands:
- cd packages/moxxmpp
- dart pub get
- dart test
# Check moxxmpp_socket_tcp
moxxmpp_socket_tcp-lint:
image: dart:2.18.1
commands:
- cd packages/moxxmpp_socket_tcp
- dart pub get
- dart analyze --fatal-infos --fatal-warnings
# moxxmpp-test:
# image: dart:2.18.1
# commands:
# - cd packages/moxxmpp
# - dart pub get
# - dart test

View File

@@ -1,6 +1,7 @@
library moxxmpp; library moxxmpp;
export 'package:moxxmpp/src/connection.dart'; export 'package:moxxmpp/src/connection.dart';
export 'package:moxxmpp/src/connectivity.dart';
export 'package:moxxmpp/src/errors.dart'; export 'package:moxxmpp/src/errors.dart';
export 'package:moxxmpp/src/events.dart'; export 'package:moxxmpp/src/events.dart';
export 'package:moxxmpp/src/iq.dart'; export 'package:moxxmpp/src/iq.dart';

View File

@@ -0,0 +1,94 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:synchronized/synchronized.dart';
/// A surrogate key for awaiting stanzas.
@immutable
class _StanzaSurrogateKey {
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
/// The JID the original stanza was sent to. We expect the result to come from the
/// same JID.
final String sentTo;
/// The ID of the original stanza. We expect the result to have the same ID.
final String id;
/// The tag name of the stanza.
final String tag;
@override
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
@override
bool operator==(Object other) {
return other is _StanzaSurrogateKey &&
other.sentTo == sentTo &&
other.id == id &&
other.tag == tag;
}
}
/// This class handles the await semantics for stanzas. Stanzas are given a "unique"
/// key equal to the tuple (to, id, tag) with which their response is identified.
///
/// That means that when sending ```<iq to="example@some.server.example" id="abc123" />```,
/// the response stanza must be from "example@some.server.example", have id "abc123" and
/// be an iq stanza.
///
/// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
class StanzaAwaiter {
/// The pending stanzas, identified by their surrogate key.
final Map<_StanzaSurrogateKey, Completer<XMLNode>> _pending = {};
/// The critical section for accessing [StanzaAwaiter._pending].
final Lock _lock = Lock();
/// Register a stanza as pending.
/// [to] is the value of the stanza's "to" attribute.
/// [id] is the value of the stanza's "id" attribute.
/// [tag] is the stanza's tag name.
///
/// Returns a future that might resolve to the response to the stanza.
Future<Future<XMLNode>> addPending(String to, String id, String tag) async {
final completer = await _lock.synchronized(() {
final completer = Completer<XMLNode>();
_pending[_StanzaSurrogateKey(to, id, tag)] = completer;
return completer;
});
return completer.future;
}
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of
/// the connection.
/// If [stanza] is awaited, resolves the future and returns true. If not, returns
/// false.
Future<bool> onData(XMLNode stanza, JID bareJid) async {
assert(bareJid.isBare(), 'bareJid must be bare');
final id = stanza.attributes['id'] as String?;
if (id == null) return false;
final key = _StanzaSurrogateKey(
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the
// attribute is implicitly from our own bare JID.
stanza.attributes['from'] as String? ?? bareJid.toString(),
id,
stanza.tag,
);
return _lock.synchronized(() {
final completer = _pending[key];
if (completer != null) {
_pending.remove(key);
completer.complete(stanza);
return true;
}
return false;
});
}
}

View File

@@ -2,7 +2,9 @@ import 'dart:async';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/awaiter.dart';
import 'package:moxxmpp/src/buffer.dart'; import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/iq.dart'; import 'package:moxxmpp/src/iq.dart';
@@ -89,49 +91,28 @@ class XmppConnectionResult {
final XmppError? error; final XmppError? error;
} }
/// A surrogate key for awaiting stanzas.
@immutable
class _StanzaAwaitableData {
const _StanzaAwaitableData(this.sentTo, this.id);
/// The JID the original stanza was sent to. We expect the result to come from the
/// same JID.
final String sentTo;
/// The ID of the original stanza. We expect the result to have the same ID.
final String id;
@override
int get hashCode => sentTo.hashCode ^ id.hashCode;
@override
bool operator==(Object other) {
return other is _StanzaAwaitableData &&
other.sentTo == sentTo &&
other.id == id;
}
}
/// This class is a connection to the server. /// This class is a connection to the server.
class XmppConnection { class XmppConnection {
XmppConnection( XmppConnection(
ReconnectionPolicy reconnectionPolicy, ReconnectionPolicy reconnectionPolicy,
ConnectivityManager connectivityManager,
this._socket, this._socket,
{ {
this.connectionPingDuration = const Duration(minutes: 3), this.connectionPingDuration = const Duration(minutes: 3),
this.connectingTimeout = const Duration(minutes: 2), this.connectingTimeout = const Duration(minutes: 2),
} }
) : _reconnectionPolicy = reconnectionPolicy { ) : _reconnectionPolicy = reconnectionPolicy,
// Allow the reconnection policy to perform reconnections by itself _connectivityManager = connectivityManager {
_reconnectionPolicy.register( // Allow the reconnection policy to perform reconnections by itself
_attemptReconnection, _reconnectionPolicy.register(
_onNetworkConnectionLost, _attemptReconnection,
); _onNetworkConnectionLost,
);
_socketStream = _socket.getDataStream(); _socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done // TODO(Unknown): Handle on done
_socketStream.transform(_streamBuffer).forEach(handleXmlStream); _socketStream.transform(_streamBuffer).forEach(handleXmlStream);
_socket.getEventStream().listen(_handleSocketEvent); _socket.getEventStream().listen(_handleSocketEvent);
} }
@@ -144,16 +125,18 @@ class XmppConnection {
/// The data stream of the socket /// The data stream of the socket
late final Stream<String> _socketStream; late final Stream<String> _socketStream;
/// Connection settings /// Connection settings
late ConnectionSettings _connectionSettings; late ConnectionSettings _connectionSettings;
/// A policy on how to reconnect /// A policy on how to reconnect
final ReconnectionPolicy _reconnectionPolicy; final ReconnectionPolicy _reconnectionPolicy;
/// A list of stanzas we are tracking with its corresponding critical section lock /// The class responsible for preventing errors on initial connection due
final Map<_StanzaAwaitableData, Completer<XMLNode>> _awaitingResponse = {}; /// to no network.
final Lock _awaitingResponseLock = Lock(); final ConnectivityManager _connectivityManager;
/// A helper for handling await semantics with stanzas
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
/// Sorted list of handlers that we call or incoming and outgoing stanzas /// Sorted list of handlers that we call or incoming and outgoing stanzas
final List<StanzaHandler> _incomingStanzaHandlers = List.empty(growable: true); final List<StanzaHandler> _incomingStanzaHandlers = List.empty(growable: true);
@@ -240,63 +223,46 @@ 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.id}');
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,
),
);
_xmppManagers[manager.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.name}');
await manager.postRegisterCallback();
}
}
} }
/// Register a list of negotiator with the connection. /// Register a list of negotiator with the connection.
@@ -401,7 +367,7 @@ class XmppConnection {
// Connect again // Connect again
// ignore: cascade_invocations // ignore: cascade_invocations
_log.finest('Calling connect() from _attemptReconnection'); _log.finest('Calling connect() from _attemptReconnection');
await connect(); await connect(waitForConnection: true);
} }
/// Called when a stream ending error has occurred /// Called when a stream ending error has occurred
@@ -424,7 +390,11 @@ class XmppConnection {
return; return;
} }
await _setConnectionState(XmppConnectionState.error); if (await _connectivityManager.hasConnection()) {
await _setConnectionState(XmppConnectionState.error);
} else {
await _setConnectionState(XmppConnectionState.notConnected);
}
await _reconnectionPolicy.onFailure(); await _reconnectionPolicy.onFailure();
} }
@@ -482,7 +452,7 @@ class XmppConnection {
/// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has /// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has
/// none. /// none.
// TODO(Unknown): if addId = false, the function crashes. // TODO(Unknown): if addId = false, the function crashes.
Future<XMLNode> sendStanza(Stanza stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false }) async { Future<XMLNode> sendStanza(Stanza stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
assert(implies(addId == false && stanza.id == null, !awaitable), 'Cannot await a stanza with no id'); assert(implies(addId == false && stanza.id == null, !awaitable), 'Cannot await a stanza with no id');
// Add extra data in case it was not set // Add extra data in case it was not set
@@ -513,6 +483,7 @@ class XmppConnection {
null, null,
stanza_, stanza_,
encrypted: encrypted, encrypted: encrypted,
forceEncryption: forceEncryption,
), ),
); );
_log.fine('Done'); _log.fine('Done');
@@ -544,43 +515,38 @@ class XmppConnection {
_log.fine('Attempting to acquire lock for ${data.stanza.id}...'); _log.fine('Attempting to acquire lock for ${data.stanza.id}...');
// TODO(PapaTutuWawa): Handle this much more graceful // TODO(PapaTutuWawa): Handle this much more graceful
var future = Future.value(XMLNode(tag: 'not-used')); var future = Future.value(XMLNode(tag: 'not-used'));
await _awaitingResponseLock.synchronized(() async { if (awaitable) {
_log.fine('Lock acquired for ${data.stanza.id}'); future = await _stanzaAwaiter.addPending(
// A stanza with no to attribute is for direct processing by the server. As such,
_StanzaAwaitableData? key; // we can correlate it by just *assuming* we have that attribute
if (awaitable) { // (RFC 6120 Section 8.1.1.1)
key = _StanzaAwaitableData(data.stanza.to!, data.stanza.id!); data.stanza.to ?? _connectionSettings.jid.toBare().toString(),
_awaitingResponse[key] = Completer(); data.stanza.id!,
} data.stanza.tag,
// This uses the StreamManager to behave like a send queue
if (await _canSendData()) {
_socket.write(stanzaString);
// Try to ack every stanza
// NOTE: Here we have send an Ack request nonza. This is now done by StreamManagementManager when receiving the StanzaSentEvent
} else {
_log.fine('_canSendData() returned false.');
}
_log.fine('Running post stanza handlers..');
await _runOutgoingPostStanzaHandlers(
stanza_,
initial: StanzaHandlerData(
false,
false,
null,
stanza_,
),
); );
_log.fine('Done'); }
if (awaitable) { // This uses the StreamManager to behave like a send queue
future = _awaitingResponse[key]!.future; if (await _canSendData()) {
} _socket.write(stanzaString);
_log.fine('Releasing lock for ${data.stanza.id}'); // Try to ack every stanza
}); // NOTE: Here we have send an Ack request nonza. This is now done by StreamManagementManager when receiving the StanzaSentEvent
} else {
_log.fine('_canSendData() returned false.');
}
_log.fine('Running post stanza handlers..');
await _runOutgoingPostStanzaHandlers(
stanza_,
initial: StanzaHandlerData(
false,
false,
null,
stanza_,
),
);
_log.fine('Done');
return future; return future;
} }
@@ -744,21 +710,10 @@ class XmppConnection {
''; '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
// See if we are waiting for this stanza final awaited = await _stanzaAwaiter.onData(
final id = incomingPreHandlers.stanza.attributes['id'] as String?; incomingPreHandlers.stanza,
var awaited = false; _connectionSettings.jid.toBare(),
await _awaitingResponseLock.synchronized(() async { );
if (id != null && incomingPreHandlers.stanza.from != null) {
final key = _StanzaAwaitableData(incomingPreHandlers.stanza.from!, id);
final comp = _awaitingResponse[key];
if (comp != null) {
comp.complete(incomingPreHandlers.stanza);
_awaitingResponse.remove(key);
awaited = true;
}
}
});
if (awaited) { if (awaited) {
return; return;
} }
@@ -776,7 +731,7 @@ class XmppConnection {
), ),
); );
if (!incomingHandlers.done) { if (!incomingHandlers.done) {
handleUnhandledStanza(this, incomingPreHandlers.stanza); await handleUnhandledStanza(this, incomingPreHandlers);
} }
} }
@@ -870,7 +825,6 @@ class XmppConnection {
if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
} else { } else {
@@ -895,7 +849,6 @@ class XmppConnection {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
} else { } else {
@@ -913,7 +866,6 @@ class XmppConnection {
_log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!'); _log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
break; break;
@@ -1025,7 +977,7 @@ class XmppConnection {
} }
Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async { Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async {
_reconnectionPolicy.setShouldReconnect(false); await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) { if (triggeredByUser) {
getPresenceManager().sendUnavailablePresence(); getPresenceManager().sendUnavailablePresence();
@@ -1056,17 +1008,21 @@ class XmppConnection {
/// Like [connect] but the Future resolves when the resource binding is either done or /// Like [connect] but the Future resolves when the resource binding is either done or
/// SASL has failed. /// SASL has failed.
Future<XmppConnectionResult> connectAwaitable({ String? lastResource }) async { Future<XmppConnectionResult> connectAwaitable({ String? lastResource, bool waitForConnection = false }) async {
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
_connectionCompleter = Completer(); _connectionCompleter = Completer();
_log.finest('Calling connect() from connectAwaitable'); _log.finest('Calling connect() from connectAwaitable');
await connect(lastResource: lastResource); await connect(
lastResource: lastResource,
waitForConnection: waitForConnection,
shouldReconnect: false,
);
return _connectionCompleter!.future; return _connectionCompleter!.future;
} }
/// Start the connection process using the provided connection settings. /// Start the connection process using the provided connection settings.
Future<void> connect({ String? lastResource }) async { Future<void> connect({ String? lastResource, bool waitForConnection = false, bool shouldReconnect = true }) async {
if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) { if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) {
_log.fine('Cancelling this connection attempt as one appears to be already running.'); _log.fine('Cancelling this connection attempt as one appears to be already running.');
return; return;
@@ -1074,15 +1030,25 @@ class XmppConnection {
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
_reconnectionPolicy.setShouldReconnect(true);
if (lastResource != null) { if (lastResource != null) {
setResource(lastResource); setResource(lastResource);
} }
if (shouldReconnect) {
await _reconnectionPolicy.setShouldReconnect(true);
}
await _reconnectionPolicy.reset(); await _reconnectionPolicy.reset();
await _sendEvent(ConnectingEvent()); await _sendEvent(ConnectingEvent());
// If requested, wait until we have a network connection
if (waitForConnection) {
_log.info('Waiting for okay from connectivityManager');
await _connectivityManager.waitForConnection();
_log.info('Got okay from connectivityManager');
}
final smManager = getStreamManagementManager(); final smManager = getStreamManagementManager();
String? host; String? host;
int? port; int? port;

View File

@@ -0,0 +1,18 @@
/// This manager class is responsible to tell the moxxmpp XmppConnection
/// when a connection can be established or not, regarding the network availability.
abstract class ConnectivityManager {
/// Returns true if a network connection is available. If not, returns false.
Future<bool> hasConnection();
/// Returns a future that resolves once we have a network connection.
Future<void> waitForConnection();
}
/// An implementation of [ConnectivityManager] that is always connected.
class AlwaysConnectedConnectivityManager extends ConnectivityManager {
@override
Future<bool> hasConnection() async => true;
@override
Future<void> waitForConnection() async {}
}

View File

@@ -1,10 +1,28 @@
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
bool handleUnhandledStanza(XmppConnection conn, Stanza stanza) { /// Bounce a stanza if it was not handled by any manager. [conn] is the connection object
if (stanza.type != 'error' && stanza.type != 'result') { /// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled
conn.sendStanza(stanza.errorReply('cancel', 'feature-not-implemented')); /// stanza.
} Future<void> handleUnhandledStanza(XmppConnection conn, StanzaHandlerData data) async {
if (data.stanza.type != 'error' && data.stanza.type != 'result') {
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: 'error',
children: [
buildErrorElement(
'cancel',
'feature-not-implemented',
),
],
);
return true; await conn.sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
} }

View File

@@ -51,7 +51,11 @@ class JID {
bool isFull() => resource.isNotEmpty; bool isFull() => resource.isNotEmpty;
/// Converts the JID into a bare JID. /// Converts the JID into a bare JID.
JID toBare() => JID(local, domain, ''); JID toBare() {
if (isBare()) return this;
return JID(local, domain, '');
}
/// Converts the JID into one with a resource part of [resource]. /// Converts the JID into one with a resource part of [resource].
JID withResource(String resource) => JID(local, domain, resource); JID withResource(String resource) => JID(local, domain, resource);

View File

@@ -23,7 +23,7 @@ class XmppManagerAttributes {
required this.getNegotiatorById, required this.getNegotiatorById,
}); });
/// Send a stanza whose response can be awaited. /// Send a stanza whose response can be awaited.
final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted}) sendStanza; final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted, bool forceEncryption}) sendStanza;
/// Send a nonza. /// Send a nonza.
final void Function(XMLNode) sendNonza; final void Function(XMLNode) sendNonza;

View File

@@ -1,17 +1,27 @@
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/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 {
XmppManagerBase(this.id);
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;
_log = Logger(getName()); _log = Logger(name);
} }
/// Returns the attributes that are registered with the manager. /// Returns the attributes that are registered with the manager.
@@ -47,13 +57,16 @@ 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(); final String id;
/// Return a name that will be used for logging.
String getName();
/// The name of the manager.
String get name => toString();
/// Return the logger for this manager. /// Return the logger for this manager.
Logger get logger => _log; Logger get logger => _log;
@@ -62,6 +75,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.
@@ -79,4 +110,25 @@ abstract class XmppManagerBase {
return handled; return handled;
} }
/// Sends a reply of the stanza in [data] with [type]. Replaces the original stanza's
/// children with [children].
///
/// Note that this function currently only accepts IQ stanzas.
Future<void> reply(StanzaHandlerData data, String type, List<XMLNode> children) async {
assert(data.stanza.tag == 'iq', 'Reply makes little sense for non-IQ stanzas');
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: type,
children: children,
);
await getAttributes().sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
} }

View File

@@ -50,6 +50,11 @@ class StanzaHandlerData with _$StanzaHandlerData {
String? funCancellation, String? funCancellation,
// Whether the stanza was received encrypted // Whether the stanza was received encrypted
@Default(false) bool encrypted, @Default(false) bool encrypted,
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@Default(false) bool forceEncryption,
// The stated type of encryption used, if any was used // The stated type of encryption used, if any was used
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
// Delayed Delivery // Delayed Delivery

View File

@@ -48,6 +48,11 @@ mixin _$StanzaHandlerData {
String? get funCancellation => String? get funCancellation =>
throw _privateConstructorUsedError; // Whether the stanza was received encrypted throw _privateConstructorUsedError; // Whether the stanza was received encrypted
bool get encrypted => bool get encrypted =>
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption =>
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType => ExplicitEncryptionType? get encryptionType =>
throw _privateConstructorUsedError; // Delayed Delivery throw _privateConstructorUsedError; // Delayed Delivery
@@ -94,6 +99,7 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
String? funReplacement, String? funReplacement,
String? funCancellation, String? funCancellation,
bool encrypted, bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
@@ -132,6 +138,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed, Object? funReplacement = freezed,
Object? funCancellation = freezed, Object? funCancellation = freezed,
Object? encrypted = freezed, Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed, Object? encryptionType = freezed,
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
@@ -213,6 +220,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted ? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable : encrypted // ignore: cast_nullable_to_non_nullable
as bool, as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed encryptionType: encryptionType == freezed
? _value.encryptionType ? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable : encryptionType // ignore: cast_nullable_to_non_nullable
@@ -271,6 +282,7 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
String? funReplacement, String? funReplacement,
String? funCancellation, String? funCancellation,
bool encrypted, bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
@@ -311,6 +323,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed, Object? funReplacement = freezed,
Object? funCancellation = freezed, Object? funCancellation = freezed,
Object? encrypted = freezed, Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed, Object? encryptionType = freezed,
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
@@ -392,6 +405,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted ? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable : encrypted // ignore: cast_nullable_to_non_nullable
as bool, as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed encryptionType: encryptionType == freezed
? _value.encryptionType ? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable : encryptionType // ignore: cast_nullable_to_non_nullable
@@ -442,6 +459,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.funReplacement, this.funReplacement,
this.funCancellation, this.funCancellation,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false,
this.encryptionType, this.encryptionType,
this.delayedDelivery, this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{}, final Map<String, dynamic> other = const <String, dynamic>{},
@@ -506,6 +524,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
@override @override
@JsonKey() @JsonKey()
final bool encrypted; final bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@override
@JsonKey()
final bool forceEncryption;
// The stated type of encryption used, if any was used // The stated type of encryption used, if any was used
@override @override
final ExplicitEncryptionType? encryptionType; final ExplicitEncryptionType? encryptionType;
@@ -540,7 +565,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
@override @override
String toString() { String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)'; return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
} }
@override @override
@@ -572,6 +597,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.funCancellation, funCancellation) && .equals(other.funCancellation, funCancellation) &&
const DeepCollectionEquality().equals(other.encrypted, encrypted) && const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
const DeepCollectionEquality()
.equals(other.forceEncryption, forceEncryption) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.encryptionType, encryptionType) && .equals(other.encryptionType, encryptionType) &&
const DeepCollectionEquality() const DeepCollectionEquality()
@@ -608,6 +635,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(funReplacement), const DeepCollectionEquality().hash(funReplacement),
const DeepCollectionEquality().hash(funCancellation), const DeepCollectionEquality().hash(funCancellation),
const DeepCollectionEquality().hash(encrypted), const DeepCollectionEquality().hash(encrypted),
const DeepCollectionEquality().hash(forceEncryption),
const DeepCollectionEquality().hash(encryptionType), const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery), const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other), const DeepCollectionEquality().hash(_other),
@@ -641,6 +669,7 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final String? funReplacement, final String? funReplacement,
final String? funCancellation, final String? funCancellation,
final bool encrypted, final bool encrypted,
final bool forceEncryption,
final ExplicitEncryptionType? encryptionType, final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery, final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other, final Map<String, dynamic> other,
@@ -690,6 +719,11 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
String? get funCancellation; String? get funCancellation;
@override // Whether the stanza was received encrypted @override // Whether the stanza was received encrypted
bool get encrypted; bool get encrypted;
@override // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption;
@override // The stated type of encryption used, if any was used @override // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType; ExplicitEncryptionType? get encryptionType;
@override // Delayed Delivery @override // Delayed Delivery

View File

@@ -1,25 +1,25 @@
const smManager = 'im.moxxmpp.streammangementmanager'; const smManager = 'org.moxxmpp.streammangementmanager';
const discoManager = 'im.moxxmpp.discomanager'; const discoManager = 'org.moxxmpp.discomanager';
const messageManager = 'im.moxxmpp.messagemanager'; const messageManager = 'org.moxxmpp.messagemanager';
const rosterManager = 'im.moxxmpp.rostermanager'; const rosterManager = 'org.moxxmpp.rostermanager';
const presenceManager = 'im.moxxmpp.presencemanager'; const presenceManager = 'org.moxxmpp.presencemanager';
const csiManager = 'im.moxxmpp.csimanager'; const csiManager = 'org.moxxmpp.csimanager';
const carbonsManager = 'im.moxxmpp.carbonsmanager'; const carbonsManager = 'org.moxxmpp.carbonsmanager';
const vcardManager = 'im.moxxmpp.vcardmanager'; const vcardManager = 'org.moxxmpp.vcardmanager';
const pubsubManager = 'im.moxxmpp.pubsubmanager'; const pubsubManager = 'org.moxxmpp.pubsubmanager';
const userAvatarManager = 'im.moxxmpp.useravatarmanager'; const userAvatarManager = 'org.moxxmpp.useravatarmanager';
const stableIdManager = 'im.moxxmpp.stableidmanager'; const stableIdManager = 'org.moxxmpp.stableidmanager';
const simsManager = 'im.moxxmpp.simsmanager'; const simsManager = 'org.moxxmpp.simsmanager';
const messageDeliveryReceiptManager = 'im.moxxmpp.messagedeliveryreceiptmanager'; const messageDeliveryReceiptManager = 'org.moxxmpp.messagedeliveryreceiptmanager';
const chatMarkerManager = 'im.moxxmpp.chatmarkermanager'; const chatMarkerManager = 'org.moxxmpp.chatmarkermanager';
const oobManager = 'im.moxxmpp.oobmanager'; const oobManager = 'org.moxxmpp.oobmanager';
const sfsManager = 'im.moxxmpp.sfsmanager'; const sfsManager = 'org.moxxmpp.sfsmanager';
const messageRepliesManager = 'im.moxxmpp.messagerepliesmanager'; const messageRepliesManager = 'org.moxxmpp.messagerepliesmanager';
const blockingManager = 'im.moxxmpp.blockingmanager'; const blockingManager = 'org.moxxmpp.blockingmanager';
const httpFileUploadManager = 'im.moxxmpp.httpfileuploadmanager'; const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager';
const chatStateManager = 'im.moxxmpp.chatstatemanager'; const chatStateManager = 'org.moxxmpp.chatstatemanager';
const pingManager = 'im.moxxmpp.ping'; const pingManager = 'org.moxxmpp.ping';
const fileUploadNotificationManager = 'im.moxxmpp.fileuploadnotificationmanager'; const fileUploadNotificationManager = 'org.moxxmpp.fileuploadnotificationmanager';
const omemoManager = 'org.moxxmpp.omemomanager'; const omemoManager = 'org.moxxmpp.omemomanager';
const emeManager = 'org.moxxmpp.ememanager'; const emeManager = 'org.moxxmpp.ememanager';
const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager'; const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager';
@@ -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

@@ -76,11 +76,7 @@ class MessageDetails {
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
@override MessageManager() : super(messageManager);
String getId() => messageManager;
@override
String getName() => 'MessageManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

View File

@@ -4,11 +4,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
class PingManager extends XmppManagerBase { class PingManager extends XmppManagerBase {
@override PingManager() : super(pingManager);
String getId() => pingManager;
@override
String getName() => 'PingManager';
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;

View File

@@ -8,23 +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(presenceManager);
String? _capabilityHash;
final String _capHashNode;
String get capabilityHashNode => _capHashNode; /// The list of pre-send callbacks.
final List<PresencePreSendCallback> _presenceCallbacks = List.empty(growable: true);
@override
String getId() => presenceManager;
@override
String getName() => 'PresenceManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -39,6 +35,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 +64,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

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/util/queue.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// A callback function to be called when the connection to the server has been lost. /// A callback function to be called when the connection to the server has been lost.
@@ -24,10 +25,16 @@ abstract class ReconnectionPolicy {
bool _shouldAttemptReconnection = false; bool _shouldAttemptReconnection = false;
/// Indicate if a reconnection attempt is currently running. /// Indicate if a reconnection attempt is currently running.
bool _isReconnecting = false; @protected
bool isReconnecting = false;
/// And the corresponding lock /// And the corresponding lock
final Lock _isReconnectingLock = Lock(); @protected
final Lock lock = Lock();
/// The lock for accessing [_shouldAttemptReconnection]
@protected
final Lock shouldReconnectLock = Lock();
/// Called by XmppConnection to register the policy. /// Called by XmppConnection to register the policy.
void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) { void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) {
@@ -48,92 +55,122 @@ abstract class ReconnectionPolicy {
/// Caled by the XmppConnection when the reconnection was successful. /// Caled by the XmppConnection when the reconnection was successful.
Future<void> onSuccess(); Future<void> onSuccess();
bool get shouldReconnect => _shouldAttemptReconnection; Future<bool> getShouldReconnect() async {
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection);
}
/// Set whether a reconnection attempt should be made. /// Set whether a reconnection attempt should be made.
void setShouldReconnect(bool value) { Future<void> setShouldReconnect(bool value) async {
_shouldAttemptReconnection = value; return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value);
} }
/// Returns true if the manager is currently triggering a reconnection. If not, returns /// Returns true if the manager is currently triggering a reconnection. If not, returns
/// false. /// false.
Future<bool> isReconnectionRunning() async { Future<bool> isReconnectionRunning() async {
return _isReconnectingLock.synchronized(() => _isReconnecting); return lock.synchronized(() => isReconnecting);
} }
/// Set the _isReconnecting state to [value]. /// Set the isReconnecting state to [value].
@protected @protected
Future<void> setIsReconnecting(bool value) async { Future<void> setIsReconnecting(bool value) async {
await _isReconnectingLock.synchronized(() async { await lock.synchronized(() async {
_isReconnecting = value; isReconnecting = value;
}); });
} }
@protected
Future<bool> testAndSetIsReconnecting() async {
return _isReconnectingLock.synchronized(() {
if (_isReconnecting) {
return false;
} else {
_isReconnecting = true;
return true;
}
});
}
} }
/// A simple reconnection strategy: Make the reconnection delays exponentially longer /// A simple reconnection strategy: Make the reconnection delays exponentially longer
/// for every failed attempt. /// for every failed attempt.
class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy { /// NOTE: This ReconnectionPolicy may be broken
class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
RandomBackoffReconnectionPolicy(
this._minBackoffTime,
this._maxBackoffTime,
) : assert(_minBackoffTime < _maxBackoffTime, '_minBackoffTime must be smaller than _maxBackoffTime'),
super();
ExponentialBackoffReconnectionPolicy(this._maxBackoffTime) /// The maximum time in seconds that a backoff should be.
: _counter = 0,
_log = Logger('ExponentialBackoffReconnectionPolicy'),
super();
final int _maxBackoffTime; final int _maxBackoffTime;
int _counter;
/// The minimum time in seconds that a backoff should be.
final int _minBackoffTime;
/// Backoff timer.
Timer? _timer; Timer? _timer;
final Logger _log;
final Lock _timerLock = Lock();
/// Logger.
final Logger _log = Logger('RandomBackoffReconnectionPolicy');
/// Event queue
final AsyncQueue _eventQueue = AsyncQueue();
/// Called when the backoff expired /// Called when the backoff expired
Future<void> _onTimerElapsed() async { Future<void> _onTimerElapsed() async {
final isReconnecting = await isReconnectionRunning(); _log.fine('Timer elapsed. Waiting for lock');
if (shouldReconnect) { await lock.synchronized(() async {
if (!isReconnecting) { _log.fine('Lock aquired');
await setIsReconnecting(true); if (!(await getShouldReconnect())) {
await performReconnect!(); _log.fine('Backoff timer expired but getShouldReconnect() returned false');
} else { return;
// Should never happen.
_log.fine('Backoff timer expired but reconnection is running, so doing nothing.');
} }
}
if (isReconnecting) {
_log.fine('Backoff timer expired but a reconnection is running, so doing nothing.');
return;
}
_log.fine('Triggering reconnect');
isReconnecting = true;
await performReconnect!();
});
await _timerLock.synchronized(() {
_timer?.cancel();
_timer = null;
});
}
Future<void> _reset() async {
_log.finest('Resetting internal state');
await _timerLock.synchronized(() {
_timer?.cancel();
_timer = null;
});
await setIsReconnecting(false);
} }
@override @override
Future<void> reset() async { Future<void> reset() async {
_log.finest('Resetting internal state'); // ignore: unnecessary_lambdas
_counter = 0; await _eventQueue.addJob(() => _reset());
await setIsReconnecting(false);
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
} }
@override Future<void> _onFailure() async {
Future<void> onFailure() async { final shouldContinue = await _timerLock.synchronized(() {
_log.finest('Failure occured. Starting exponential backoff'); return _timer == null;
_counter++; });
if (!shouldContinue) {
if (_timer != null) { _log.finest('_onFailure: Not backing off since _timer is already running');
_timer!.cancel(); return;
} }
// Wait at max 80 seconds. final seconds = Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime;
final seconds = min(min(pow(2, _counter).toInt(), 80), _maxBackoffTime); _log.finest('Failure occured. Starting random backoff with ${seconds}s');
_timer?.cancel();
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed); _timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
} }
@override
Future<void> onFailure() async {
// ignore: unnecessary_lambdas
await _eventQueue.addJob(() => _onFailure());
}
@override @override
Future<void> onSuccess() async { Future<void> onSuccess() async {
@@ -141,7 +178,7 @@ class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy {
} }
} }
/// A stub reconnection policy for tests /// A stub reconnection policy for tests.
@visibleForTesting @visibleForTesting
class TestingReconnectionPolicy extends ReconnectionPolicy { class TestingReconnectionPolicy extends ReconnectionPolicy {
TestingReconnectionPolicy() : super(); TestingReconnectionPolicy() : super();

View File

@@ -93,7 +93,7 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
/// This manager requires a RosterFeatureNegotiator to be registered. /// This manager requires a RosterFeatureNegotiator to be registered.
class RosterManager extends XmppManagerBase { class RosterManager extends XmppManagerBase {
RosterManager(this._stateManager) : super(); RosterManager(this._stateManager) : super(rosterManager);
/// The class managing the entire roster state. /// The class managing the entire roster state.
final BaseRosterStateManager _stateManager; final BaseRosterStateManager _stateManager;
@@ -104,12 +104,6 @@ class RosterManager extends XmppManagerBase {
_stateManager.register(attributes.sendEvent); _stateManager.register(attributes.sendEvent);
} }
@override
String getId() => rosterManager;
@override
String getName() => 'RosterManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
@@ -161,7 +155,11 @@ class RosterManager extends XmppManagerBase {
), ),
); );
await attrs.sendStanza(stanza.reply()); await reply(
state,
'result',
[],
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }

View File

@@ -114,40 +114,27 @@ class Stanza extends XMLNode {
children: children ?? this.children, children: children ?? this.children,
); );
} }
}
Stanza reply({ List<XMLNode> children = const [] }) {
return copyWith( /// Build an <error /> element with a child <[condition] type="[type]" />. If [text]
from: attributes['to'] as String?, /// is not null, then the condition element will contain a <text /> element with [text]
to: attributes['from'] as String?, /// as the body.
type: tag == 'iq' ? 'result' : attributes['type'] as String?, XMLNode buildErrorElement(String type, String condition, { String? text }) {
children: children, return XMLNode(
); tag: 'error',
} attributes: <String, dynamic>{ 'type': type },
children: [
Stanza errorReply(String type, String condition, { String? text }) { XMLNode.xmlns(
return copyWith( tag: condition,
from: attributes['to'] as String?, xmlns: fullStanzaXmlns,
to: attributes['from'] as String?, children: text != null ? [
type: 'error', XMLNode.xmlns(
children: [ tag: 'text',
XMLNode( xmlns: fullStanzaXmlns,
tag: 'error', text: text,
attributes: <String, dynamic>{ 'type': type }, )
children: [ ] : [],
XMLNode.xmlns( ),
tag: condition, ],
xmlns: fullStanzaXmlns, );
children: text != null ?[
XMLNode.xmlns(
tag: 'text',
xmlns: fullStanzaXmlns,
text: text,
)
] : [],
)
],
)
],
);
}
} }

View File

@@ -1,7 +1,6 @@
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class XMLNode { class XMLNode {
XMLNode({ XMLNode({
required this.tag, required this.tag,
this.attributes = const <String, dynamic>{}, this.attributes = const <String, dynamic>{},

View File

@@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
/// A job to be submitted to an [AsyncQueue].
typedef AsyncQueueJob = Future<void> Function();
/// A (hopefully) async-safe queue that attempts to force
/// in-order execution of its jobs.
class AsyncQueue {
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running].
final Lock _lock = Lock();
/// The actual job queue.
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>();
/// Indicates whether we are currently executing a job.
bool _running = false;
@visibleForTesting
Queue<AsyncQueueJob> get queue => _queue;
@visibleForTesting
bool get isRunning => _running;
/// Adds a job [job] to the queue.
Future<void> addJob(AsyncQueueJob job) async {
await _lock.synchronized(() {
_queue.add(job);
if (!_running && _queue.isNotEmpty) {
_running = true;
unawaited(_popJob());
}
});
}
Future<void> clear() async {
await _lock.synchronized(_queue.clear);
}
Future<void> _popJob() async {
final job = _queue.removeFirst();
final future = job();
await future;
await _lock.synchronized(() {
if (_queue.isNotEmpty) {
unawaited(_popJob());
} else {
_running = false;
}
});
}
}

View File

@@ -0,0 +1,67 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
/// This class allows for multiple asynchronous code places to wait on the
/// same computation of type [V], indentified by a key of type [K].
class WaitForTracker<K, V> {
/// The mapping of key -> Completer for the pending tasks.
final Map<K, List<Completer<V>>> _tracker = {};
/// The lock for accessing _tracker.
final Lock _lock = Lock();
/// Wait for a task with key [key]. If there was no such task already
/// present, returns null. If one or more tasks were already present, returns
/// a future that will resolve to the result of the first task.
Future<Future<V>?> waitFor(K key) async {
final result = await _lock.synchronized(() {
if (_tracker.containsKey(key)) {
// The task already exists. Just append outselves
final completer = Completer<V>();
_tracker[key]!.add(completer);
return completer;
}
// The task does not exist yet
_tracker[key] = List<Completer<V>>.empty(growable: true);
return null;
});
return result?.future;
}
/// Resolve a task with key [key] to [value].
Future<void> resolve(K key, V value) async {
await _lock.synchronized(() {
if (!_tracker.containsKey(key)) return;
for (final completer in _tracker[key]!) {
completer.complete(value);
}
_tracker.remove(key);
});
}
Future<void> resolveAll(V value) async {
await _lock.synchronized(() {
for (final key in _tracker.keys) {
for (final completer in _tracker[key]!) {
completer.complete(value);
}
}
});
}
/// Remove all tasks from the tracker.
Future<void> clear() async {
await _lock.synchronized(_tracker.clear);
}
@visibleForTesting
bool hasTasksRunning() => _tracker.isNotEmpty;
@visibleForTesting
List<Completer<V>> getRunningTasks(K key) => _tracker[key]!;
}

View File

@@ -11,13 +11,7 @@ import 'package:moxxmpp/src/xeps/xep_0446.dart';
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
class FileUploadNotificationManager extends XmppManagerBase { class FileUploadNotificationManager extends XmppManagerBase {
FileUploadNotificationManager() : super(); FileUploadNotificationManager() : super(fileUploadNotificationManager);
@override
String getId() => fileUploadNotificationManager;
@override
String getName() => 'FileUploadNotificationManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

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

@@ -0,0 +1,23 @@
import 'package:meta/meta.dart';
@internal
@immutable
class DiscoCacheKey {
const DiscoCacheKey(this.jid, this.node);
/// The JID we're requesting disco data from.
final String jid;
/// Optionally the node we are requesting from.
final String? node;
@override
bool operator ==(Object other) {
return other is DiscoCacheKey &&
jid == other.jid &&
node == other.node;
}
@override
int get hashCode => jid.hashCode ^ node.hashCode;
}

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

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -7,62 +8,71 @@ 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/util/wait.dart';
import 'package:moxxmpp/src/xeps/xep_0030/cache.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';
@immutable /// Callback that is called when a disco#info requests is received on a given node.
class DiscoCacheKey { typedef DiscoInfoRequestCallback = Future<DiscoInfo> Function();
const DiscoCacheKey(this.jid, this.node); /// Callback that is called when a disco#items requests is received on a given node.
final String jid; typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
final String? node;
@override
bool operator ==(Object other) {
return other is DiscoCacheKey && jid == other.jid && node == other.node;
}
@override
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 {
/// [identities] is a list of disco identities that should be added by default
DiscoManager() /// to a disco#info response.
: _features = List.empty(growable: true), DiscoManager(List<Identity> identities)
_capHashCache = {}, : _identities = List<Identity>.from(identities),
_capHashInfoCache = {}, super(discoManager);
_discoInfoCache = {},
_runningInfoQueries = {},
_cacheLock = Lock(),
super();
/// Our features /// Our features
final List<String> _features; final List<String> _features = List.empty(growable: true);
// Map full JID to Capability hashes /// Disco identities that we advertise
final Map<String, CapabilityHashInfo> _capHashCache; final List<Identity> _identities;
// Map capability hash to the disco info
final Map<String, DiscoInfo> _capHashInfoCache; /// Map full JID to Capability hashes
// Map full JID to Disco Info final Map<String, CapabilityHashInfo> _capHashCache = {};
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache;
// Mapping the full JID to a list of running requests /// Map capability hash to the disco info
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries; final Map<String, DiscoInfo> _capHashInfoCache = {};
// Cache lock
final Lock _cacheLock; /// Map full JID to Disco Info
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> _discoInfoTracker = WaitForTracker();
/// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>> _discoItemsTracker = WaitForTracker();
/// Cache lock
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; WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> get infoTracker => _discoInfoTracker;
@visibleForTesting
List<Completer<Result<DiscoError, DiscoInfo>>> getRunningInfoQueries(DiscoCacheKey key) => _runningInfoQueries[key]!;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -80,12 +90,6 @@ class DiscoManager extends XmppManagerBase {
), ),
]; ];
@override
String getId() => discoManager;
@override
String getName() => 'DiscoManager';
@override @override
List<String> getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ]; List<String> getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ];
@@ -96,17 +100,41 @@ class DiscoManager extends XmppManagerBase {
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PresenceReceivedEvent) { if (event is PresenceReceivedEvent) {
await _onPresence(event.jid, event.presence); await _onPresence(event.jid, event.presence);
} else if (event is StreamResumeFailedEvent) { } else if (event is ConnectionStateChangedEvent) {
// TODO(Unknown): This handling is stupid. We should have an event that is
// triggered when we cannot guarantee that everything is as
// it was before.
if (event.state != XmppConnectionState.connected) return;
if (event.resumed) return;
// Cancel all waiting requests
await _discoInfoTracker.resolveAll(
Result<DiscoError, DiscoInfo>(UnknownDiscoError()),
);
await _discoItemsTracker.resolveAll(
Result<DiscoError, List<DiscoItem>>(UnknownDiscoError()),
);
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
// Clear the cache // Clear the cache
_discoInfoCache.clear(); _discoInfoCache.clear();
}); });
} }
} }
/// 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);
@@ -114,6 +142,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;
@@ -144,77 +182,46 @@ 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')!;
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)) {
await getAttributes().sendStanza(Stanza.iq( // We can now assume that node != null
to: stanza.from, final result = await _discoInfoCallbacks[node]!();
from: stanza.to, await reply(
id: stanza.id, state,
type: 'error', 'result',
children: [ [
XMLNode.xmlns( result.toXml(),
tag: 'query', ],
// TODO(PapaTutuWawa): Why are we copying the xmlns? );
xmlns: query.attributes['xmlns']! as String,
attributes: <String, String>{
'node': node
},
),
XMLNode(
tag: 'error',
attributes: <String, String>{
'type': 'cancel'
},
children: [
XMLNode.xmlns(
tag: 'not-allowed',
xmlns: fullStanzaXmlns,
)
],
)
],
)
,);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
await getAttributes().sendStanza(stanza.reply( await reply(
children: [ state,
XMLNode.xmlns( 'result',
tag: 'query', [
xmlns: discoInfoXmlns, getDiscoInfo(node).toXml(),
attributes: { ],
...!isCapabilityNode ? {} : { );
'node': '${presence.capabilityHashNode}#$capHash'
}
},
children: [
...getIdentities().map((identity) => identity.toXMLNode()),
..._features.map((feat) {
return XMLNode(
tag: 'feature',
attributes: <String, dynamic>{ 'var': feat },
);
}),
],
),
],
),);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
@@ -222,96 +229,63 @@ class DiscoManager extends XmppManagerBase {
Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final query = stanza.firstTag('query')!; 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)) {
await getAttributes().sendStanza( final result = await _discoItemsCallbacks[node]!();
Stanza.iq( await reply(
to: stanza.from, state,
from: stanza.to, 'result',
id: stanza.id, [
type: 'error',
children: [
XMLNode.xmlns(
tag: 'query',
// TODO(PapaTutuWawa): Why copy the xmlns?
xmlns: query.attributes['xmlns']! as String,
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);
}
await getAttributes().sendStanza(
stanza.reply(
children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: <String, String>{
'node': node!,
},
children: result.map((item) => item.toXml()).toList(),
), ),
], ],
), );
);
return state.copyWith(done: true); return state.copyWith(done: true);
}
return state;
} }
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async { Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
return _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
// Complete all futures
for (final completer in _runningInfoQueries[key]!) {
completer.complete(result);
}
// Add to cache if it is a result // Add to cache if it is a result
if (result.isType<DiscoInfo>()) { if (result.isType<DiscoInfo>()) {
_discoInfoCache[key] = result.get<DiscoInfo>(); _discoInfoCache[key] = result.get<DiscoInfo>();
} }
// Remove from the request cache
_runningInfoQueries.remove(key);
}); });
await _discoInfoTracker.resolve(key, result);
} }
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async { Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final cacheKey = DiscoCacheKey(entity, node); final cacheKey = DiscoCacheKey(entity, node);
DiscoInfo? info; DiscoInfo? info;
Completer<Result<DiscoError, DiscoInfo>>? completer; final ffuture = await _cacheLock.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(() async {
await _cacheLock.synchronized(() async {
// Check if we already know what the JID supports // Check if we already know what the JID supports
if (_discoInfoCache.containsKey(cacheKey)) { if (_discoInfoCache.containsKey(cacheKey)) {
info = _discoInfoCache[cacheKey]; info = _discoInfoCache[cacheKey];
return null;
} else { } else {
// Is a request running? return _discoInfoTracker.waitFor(cacheKey);
if (_runningInfoQueries.containsKey(cacheKey)) {
completer = Completer();
_runningInfoQueries[cacheKey]!.add(completer!);
} else {
_runningInfoQueries[cacheKey] = List.from(<Completer<DiscoInfo?>>[]);
}
} }
}); });
if (info != null) { if (info != null) {
return Result<DiscoError, DiscoInfo>(info); return Result<DiscoError, DiscoInfo>(info);
} else if (completer != null) { } else {
return completer!.future; final future = await ffuture;
if (future != null) {
return future;
}
} }
final stanza = await getAttributes().sendStanza( final stanza = await getAttributes().sendStanza(
@@ -325,34 +299,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);
@@ -361,6 +318,12 @@ class DiscoManager extends XmppManagerBase {
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async { Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key);
if (future != null) {
return future;
}
final stanza = await getAttributes() final stanza = await getAttributes()
.sendStanza( .sendStanza(
buildDiscoItemsQueryStanza(entity, node: node), buildDiscoItemsQueryStanza(entity, node: node),
@@ -368,12 +331,18 @@ class DiscoManager extends XmppManagerBase {
) as Stanza; ) as Stanza;
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) return Result(InvalidResponseDiscoError()); if (query == null) {
final result = Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
await _discoItemsTracker.resolve(key, result);
return result;
}
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()); final result = Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
await _discoItemsTracker.resolve(key, result);
return result;
} }
final items = query.findTags('item').map((node) => DiscoItem( final items = query.findTags('item').map((node) => DiscoItem(
@@ -382,7 +351,9 @@ class DiscoManager extends XmppManagerBase {
name: node.attributes['name'] as String?, name: node.attributes['name'] as String?,
),).toList(); ),).toList();
return Result(items); final result = Result<DiscoError, List<DiscoItem>>(items);
await _discoItemsTracker.resolve(key, result);
return result;
} }
/// Queries information about a jid based on its node and capability hash. /// Queries information about a jid based on its node and capability hash.

View File

@@ -28,15 +28,9 @@ class VCard {
} }
class VCardManager extends XmppManagerBase { class VCardManager extends XmppManagerBase {
VCardManager() : _lastHash = {}, super(); VCardManager() : super(vcardManager);
final Map<String, String> _lastHash; final Map<String, String> _lastHash = {};
@override
String getId() => vcardManager;
@override
String getName() => 'vCardManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(

View File

@@ -69,11 +69,7 @@ class PubSubItem {
} }
class PubSubManager extends XmppManagerBase { class PubSubManager extends XmppManagerBase {
@override PubSubManager() : super(pubsubManager);
String getId() => pubsubManager;
@override
String getName() => 'PubsubManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

View File

@@ -8,7 +8,6 @@ import 'package:moxxmpp/src/stringxml.dart';
/// A data class representing the jabber:x:oob tag. /// A data class representing the jabber:x:oob tag.
class OOBData { class OOBData {
const OOBData({ this.url, this.desc }); const OOBData({ this.url, this.desc });
final String? url; final String? url;
final String? desc; final String? desc;
@@ -32,11 +31,7 @@ XMLNode constructOOBNode(OOBData data) {
} }
class OOBManager extends XmppManagerBase { class OOBManager extends XmppManagerBase {
@override OOBManager() : super(oobManager);
String getName() => 'OOBName';
@override
String getId() => oobManager;
@override @override
List<String> getDiscoFeatures() => [ oobDataXmlns ]; List<String> getDiscoFeatures() => [ oobDataXmlns ];

View File

@@ -28,24 +28,24 @@ class UserAvatarMetadata {
this.height, this.height,
this.mime, this.mime,
); );
/// The amount of bytes in the file /// The amount of bytes in the file
final int length; final int length;
/// The identifier of the avatar /// The identifier of the avatar
final String id; final String id;
/// Image proportions /// Image proportions
final int width; final int width;
final int height; final int height;
/// The MIME type of the avatar /// The MIME type of the avatar
final String mime; final String mime;
} }
/// NOTE: This class requires a PubSubManager /// NOTE: This class requires a PubSubManager
class UserAvatarManager extends XmppManagerBase { class UserAvatarManager extends XmppManagerBase {
@override UserAvatarManager() : super(userAvatarManager);
String getId() => userAvatarManager;
@override
String getName() => 'UserAvatarManager';
PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager; PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager;

View File

@@ -39,15 +39,11 @@ ChatState chatStateFromString(String raw) {
String chatStateToString(ChatState state) => state.toString().split('.').last; String chatStateToString(ChatState state) => state.toString().split('.').last;
class ChatStateManager extends XmppManagerBase { class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager);
@override @override
List<String> getDiscoFeatures() => [ chatStateXmlns ]; List<String> getDiscoFeatures() => [ chatStateXmlns ];
@override
String getName() => 'ChatStateManager';
@override
String getId() => chatStateManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(

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,78 @@ 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(entityCapabilitiesManager);
/// 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
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

@@ -24,15 +24,11 @@ XMLNode makeMessageDeliveryResponse(String id) {
} }
class MessageDeliveryReceiptManager extends XmppManagerBase { class MessageDeliveryReceiptManager extends XmppManagerBase {
MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager);
@override @override
List<String> getDiscoFeatures() => [ deliveryXmlns ]; List<String> getDiscoFeatures() => [ deliveryXmlns ];
@override
String getName() => 'MessageDeliveryReceiptManager';
@override
String getId() => messageDeliveryReceiptManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(

View File

@@ -9,16 +9,10 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
class BlockingManager extends XmppManagerBase { class BlockingManager extends XmppManagerBase {
BlockingManager() : _supported = false, _gotSupported = false, super(); BlockingManager() : super(blockingManager);
bool _supported; bool _supported = false;
bool _gotSupported; bool _gotSupported = false;
@override
String getId() => blockingManager;
@override
String getName() => 'BlockingManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

View File

@@ -21,40 +21,41 @@ const xmlUintMax = 4294967296; // 2**32
typedef StanzaAckedCallback = bool Function(Stanza stanza); typedef StanzaAckedCallback = bool Function(Stanza stanza);
class StreamManagementManager extends XmppManagerBase { class StreamManagementManager extends XmppManagerBase {
StreamManagementManager({ StreamManagementManager({
this.ackTimeout = const Duration(seconds: 30), this.ackTimeout = const Duration(seconds: 30),
}) }) : super(smManager);
: _state = StreamManagementState(0, 0),
_unackedStanzas = {},
_stateLock = Lock(),
_streamManagementEnabled = false,
_lastAckTimestamp = -1,
_pendingAcks = 0,
_streamResumed = false,
_ackLock = Lock();
/// The queue of stanzas that are not (yet) acked /// The queue of stanzas that are not (yet) acked
final Map<int, Stanza> _unackedStanzas; final Map<int, Stanza> _unackedStanzas = {};
/// Commitable state of the StreamManagementManager /// Commitable state of the StreamManagementManager
StreamManagementState _state; StreamManagementState _state = StreamManagementState(0, 0);
/// Mutex lock for _state /// Mutex lock for _state
final Lock _stateLock; final Lock _stateLock = Lock();
/// If the have enabled SM on the stream yet /// If the have enabled SM on the stream yet
bool _streamManagementEnabled; bool _streamManagementEnabled = false;
/// If the current stream has been resumed; /// If the current stream has been resumed;
bool _streamResumed; bool _streamResumed = false;
/// The time in which the response to an ack is still valid. Counts as a timeout /// The time in which the response to an ack is still valid. Counts as a timeout
/// otherwise /// otherwise
@internal @internal
final Duration ackTimeout; final Duration ackTimeout;
/// The time at which the last ack has been sent /// The time at which the last ack has been sent
int _lastAckTimestamp; int _lastAckTimestamp = -1;
/// The timer to see if we timed the connection out /// The timer to see if we timed the connection out
Timer? _ackTimer; Timer? _ackTimer;
/// Counts how many acks we're waiting for /// Counts how many acks we're waiting for
int _pendingAcks; int _pendingAcks = 0;
/// Lock for both [_lastAckTimestamp] and [_pendingAcks]. /// Lock for both [_lastAckTimestamp] and [_pendingAcks].
final Lock _ackLock; final Lock _ackLock = Lock();
/// Functions for testing /// Functions for testing
@visibleForTesting @visibleForTesting
@@ -120,12 +121,6 @@ class StreamManagementManager extends XmppManagerBase {
StreamManagementState get state => _state; StreamManagementState get state => _state;
bool get streamResumed => _streamResumed; bool get streamResumed => _streamResumed;
@override
String getId() => smManager;
@override
String getName() => 'StreamManagementManager';
@override @override
List<NonzaHandler> getNonzaHandlers() => [ List<NonzaHandler> getNonzaHandlers() => [

View File

@@ -14,11 +14,7 @@ class DelayedDelivery {
} }
class DelayedDeliveryManager extends XmppManagerBase { class DelayedDeliveryManager extends XmppManagerBase {
@override DelayedDeliveryManager() : super(delayedDeliveryManager);
String getId() => delayedDeliveryManager;
@override
String getName() => 'DelayedDeliveryManager';
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;

View File

@@ -14,7 +14,7 @@ import 'package:moxxmpp/src/xeps/xep_0297.dart';
/// This manager class implements support for XEP-0280. /// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase { class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super(); CarbonsManager() : super(carbonsManager);
/// Indicates that message carbons are enabled. /// Indicates that message carbons are enabled.
bool _isEnabled = false; bool _isEnabled = false;
@@ -25,12 +25,6 @@ class CarbonsManager extends XmppManagerBase {
/// Indicates that we know that [CarbonsManager._supported] is accurate. /// Indicates that we know that [CarbonsManager._supported] is accurate.
bool _gotSupported = false; bool _gotSupported = false;
@override
String getId() => carbonsManager;
@override
String getName() => 'CarbonsManager';
@override @override
List<StanzaHandler> getIncomingPreStanzaHandlers() => [ List<StanzaHandler> getIncomingPreStanzaHandlers() => [
StanzaHandler( StanzaHandler(

View File

@@ -61,11 +61,7 @@ HashFunction hashFunctionFromName(String name) {
} }
class CryptographicHashManager extends XmppManagerBase { class CryptographicHashManager extends XmppManagerBase {
@override CryptographicHashManager() : super(cryptographicHashManager);
String getId() => cryptographicHashManager;
@override
String getName() => 'CryptographicHashManager';
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@@ -81,7 +77,7 @@ class CryptographicHashManager extends XmppManagerBase {
]; ];
static Future<List<int>> hashFromData(List<int> data, HashFunction function) async { static Future<List<int>> hashFromData(List<int> data, HashFunction function) async {
// TODO(PapaTutuWawa): Implemen the others as well // TODO(PapaTutuWawa): Implement the others as well
HashAlgorithm algo; HashAlgorithm algo;
switch (function) { switch (function) {
case HashFunction.sha256: case HashFunction.sha256:

View File

@@ -17,11 +17,7 @@ XMLNode makeLastMessageCorrectionEdit(String id) {
} }
class LastMessageCorrectionManager extends XmppManagerBase { class LastMessageCorrectionManager extends XmppManagerBase {
@override LastMessageCorrectionManager() : super(lastMessageCorrectionManager);
String getName() => 'LastMessageCorrectionManager';
@override
String getId() => lastMessageCorrectionManager;
@override @override
List<String> getDiscoFeatures() => [ lmcXmlns ]; List<String> getDiscoFeatures() => [ lmcXmlns ];

View File

@@ -25,11 +25,7 @@ XMLNode makeChatMarker(String tag, String id) {
} }
class ChatMarkerManager extends XmppManagerBase { class ChatMarkerManager extends XmppManagerBase {
@override ChatMarkerManager() : super(chatMarkerManager);
String getName() => 'ChatMarkerManager';
@override
String getId() => chatMarkerManager;
@override @override
List<String> getDiscoFeatures() => [ chatMarkersXmlns ]; List<String> getDiscoFeatures() => [ chatMarkersXmlns ];

View File

@@ -26,10 +26,10 @@ class CSIInactiveNonza extends XMLNode {
/// A Stub negotiator that is just for "intercepting" the stream feature. /// A Stub negotiator that is just for "intercepting" the stream feature.
class CSINegotiator extends XmppFeatureNegotiatorBase { class CSINegotiator extends XmppFeatureNegotiatorBase {
CSINegotiator() : _supported = false, super(11, false, csiXmlns, csiNegotiator); CSINegotiator() : super(11, false, csiXmlns, csiNegotiator);
/// True if CSI is supported. False otherwise. /// True if CSI is supported. False otherwise.
bool _supported; bool _supported = false;
bool get isSupported => _supported; bool get isSupported => _supported;
@override @override
@@ -50,15 +50,9 @@ class CSINegotiator extends XmppFeatureNegotiatorBase {
/// The manager requires a CSINegotiator to be registered as a feature negotiator. /// The manager requires a CSINegotiator to be registered as a feature negotiator.
class CSIManager extends XmppManagerBase { class CSIManager extends XmppManagerBase {
CSIManager() : super(csiManager);
CSIManager() : _isActive = true, super(); bool _isActive = true;
bool _isActive;
@override
String getId() => csiManager;
@override
String getName() => 'CSIManager';
@override @override
Future<bool> isSupported() async { Future<bool> isSupported() async {

View File

@@ -13,7 +13,6 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
/// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of /// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of
/// the message stanza. /// the message stanza.
class StableStanzaId { class StableStanzaId {
const StableStanzaId({ this.originId, this.stanzaId, this.stanzaIdBy }); const StableStanzaId({ this.originId, this.stanzaId, this.stanzaIdBy });
final String? originId; final String? originId;
final String? stanzaId; final String? stanzaId;
@@ -29,11 +28,7 @@ XMLNode makeOriginIdElement(String id) {
} }
class StableIdManager extends XmppManagerBase { class StableIdManager extends XmppManagerBase {
@override StableIdManager() : super(stableIdManager);
String getName() => 'StableIdManager';
@override
String getId() => stableIdManager;
@override @override
List<String> getDiscoFeatures() => [ stableIdXmlns ]; List<String> getDiscoFeatures() => [ stableIdXmlns ];

View File

@@ -41,17 +41,19 @@ Map<String, String> prepareHeaders(Map<String, String> headers) {
} }
class HttpFileUploadManager extends XmppManagerBase { class HttpFileUploadManager extends XmppManagerBase {
HttpFileUploadManager() : _gotSupported = false, _supported = false, super(); HttpFileUploadManager() : super(httpFileUploadManager);
/// The entity that we will request file uploads from, if discovered.
JID? _entityJid; JID? _entityJid;
/// The maximum file upload file size, if advertised and discovered.
int? _maxUploadSize; int? _maxUploadSize;
bool _gotSupported;
bool _supported;
@override /// Flag, if we every tried to discover the upload entity.
String getId() => httpFileUploadManager; bool _gotSupported = false;
@override /// Flag, if we can use HTTP File Upload
String getName() => 'HttpFileUploadManager'; bool _supported = false;
/// Returns whether the entity provided an identity that tells us that we can ask it /// Returns whether the entity provided an identity that tells us that we can ask it
/// for an HTTP upload slot. /// for an HTTP upload slot.

View File

@@ -53,20 +53,12 @@ XMLNode buildEmeElement(ExplicitEncryptionType type) {
} }
class EmeManager extends XmppManagerBase { class EmeManager extends XmppManagerBase {
EmeManager() : super(emeManager);
EmeManager() : super();
@override
String getId() => emeManager;
@override
String getName() => 'EmeManager';
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@override @override
List<String> getDiscoFeatures() => [emeXmlns]; List<String> getDiscoFeatures() => [ emeXmlns ];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

View File

@@ -42,12 +42,9 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns), DoNotEncrypt('stanza-id', stableIdXmlns),
]; ];
@mustCallSuper
abstract class BaseOmemoManager extends XmppManagerBase { abstract class BaseOmemoManager extends XmppManagerBase {
@override BaseOmemoManager() : super(omemoManager);
String getId() => omemoManager;
@override
String getName() => 'OmemoManager';
// TODO(Unknown): Technically, this is not always true // TODO(Unknown): Technically, this is not always true
@override @override
@@ -318,11 +315,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
final toJid = JID.fromString(stanza.to!).toBare(); final toJid = JID.fromString(stanza.to!).toBare();
if (!(await shouldEncryptStanza(toJid, stanza))) { final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza);
logger.finest('shouldEncryptStanza returned false for message to $toJid. Not encrypting.'); if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest('Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.');
return state; return state;
} else { } else {
logger.finest('shouldEncryptStanza returned true for message to $toJid.'); logger.finest('Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}');
} }
final toEncrypt = List<XMLNode>.empty(growable: true); final toEncrypt = List<XMLNode>.empty(growable: true);
@@ -357,6 +355,11 @@ abstract class BaseOmemoManager extends XmppManagerBase {
other['encryption_error_devices'] = result.deviceEncryptionErrors; other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith( return state.copyWith(
other: other, other: other,
// If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()] is NoKeyMaterialAvailableException ?
OmemoNotSupportedForContactException() :
UnknownOmemoError(),
cancel: true, cancel: true,
); );
} }
@@ -453,9 +456,15 @@ abstract class BaseOmemoManager extends XmppManagerBase {
); );
} }
children.addAll( final envelopeChildren = envelope.firstTag('content')?.children;
envelope.firstTag('content')!.children, if (envelopeChildren != null) {
); children.addAll(
// Do not add forbidden elements from the envelope
envelopeChildren.where(shouldEncryptElement),
);
} else {
logger.warning('Invalid envelope element: No <content /> element');
}
if (!checkAffixElements(envelope, stanza.from!, ourJid)) { if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException(); other['encryption_error'] = InvalidAffixElementsException();

View File

@@ -8,7 +8,6 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
class StatelessMediaSharingData { class StatelessMediaSharingData {
const StatelessMediaSharingData({ required this.mediaType, required this.size, required this.description, required this.hashes, required this.url, required this.thumbnails }); const StatelessMediaSharingData({ required this.mediaType, required this.size, required this.description, required this.hashes, required this.url, required this.thumbnails });
final String mediaType; final String mediaType;
final int size; final int size;
@@ -63,11 +62,7 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
} }
class SIMSManager extends XmppManagerBase { class SIMSManager extends XmppManagerBase {
@override SIMSManager() : super(simsManager);
String getName() => 'SIMSManager';
@override
String getId() => simsManager;
@override @override
List<String> getDiscoFeatures() => [ simsXmlns ]; List<String> getDiscoFeatures() => [ simsXmlns ];

View File

@@ -12,11 +12,7 @@ class MessageRetractionData {
} }
class MessageRetractionManager extends XmppManagerBase { class MessageRetractionManager extends XmppManagerBase {
@override MessageRetractionManager() : super(messageRetractionManager);
String getName() => 'MessageRetractionManager';
@override
String getId() => messageRetractionManager;
@override @override
List<String> getDiscoFeatures() => [ messageRetractionXmlns ]; List<String> getDiscoFeatures() => [ messageRetractionXmlns ];

View File

@@ -29,15 +29,11 @@ class MessageReactions {
} }
class MessageReactionsManager extends XmppManagerBase { class MessageReactionsManager extends XmppManagerBase {
MessageReactionsManager() : super(messageReactionsManager);
@override @override
List<String> getDiscoFeatures() => [ messageReactionsXmlns ]; List<String> getDiscoFeatures() => [ messageReactionsXmlns ];
@override
String getName() => 'MessageReactionsManager';
@override
String getId() => messageReactionsManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(

View File

@@ -105,11 +105,7 @@ class StatelessFileSharingData {
} }
class SFSManager extends XmppManagerBase { class SFSManager extends XmppManagerBase {
@override SFSManager() : super(sfsManager);
String getName() => 'SFSManager';
@override
String getId() => sfsManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [

View File

@@ -225,11 +225,7 @@ class StickerPack {
} }
class StickersManager extends XmppManagerBase { class StickersManager extends XmppManagerBase {
@override StickersManager() : super(stickersManager);
String getId() => stickersManager;
@override
String getName() => 'StickersManager';
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;

View File

@@ -9,14 +9,14 @@ import 'package:moxxmpp/src/stanza.dart';
/// Data summarizing the XEP-0461 data. /// Data summarizing the XEP-0461 data.
class ReplyData { class ReplyData {
const ReplyData({ const ReplyData({
required this.to,
required this.id, required this.id,
this.to,
this.start, this.start,
this.end, this.end,
}); });
/// The bare JID to whom the reply applies to /// The bare JID to whom the reply applies to
final String to; final String? to;
/// The stanza ID of the message that is replied to /// The stanza ID of the message that is replied to
final String id; final String id;
@@ -66,12 +66,13 @@ class QuoteData {
/// A manager implementing support for parsing XEP-0461 metadata. The /// A manager implementing support for parsing XEP-0461 metadata. The
/// MessageRepliesManager itself does not modify the body of the message. /// MessageRepliesManager itself does not modify the body of the message.
class MessageRepliesManager extends XmppManagerBase { class MessageRepliesManager extends XmppManagerBase {
@override MessageRepliesManager() : super(messageRepliesManager);
String getName() => 'MessageRepliesManager';
@override @override
String getId() => messageRepliesManager; List<String> getDiscoFeatures() => [
replyXmlns,
];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
@@ -90,7 +91,7 @@ class MessageRepliesManager extends XmppManagerBase {
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async {
final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; final reply = stanza.firstTag('reply', xmlns: replyXmlns)!;
final id = reply.attributes['id']! as String; final id = reply.attributes['id']! as String;
final to = reply.attributes['to']! as String; final to = reply.attributes['to'] as String?;
int? start; int? start;
int? end; int? end;
@@ -102,11 +103,13 @@ class MessageRepliesManager extends XmppManagerBase {
end = int.parse(body.attributes['end']! as String); end = int.parse(body.attributes['end']! as String);
} }
return state.copyWith(reply: ReplyData( return state.copyWith(
reply: ReplyData(
id: id, id: id,
to: to, to: to,
start: start, start: start,
end: end, end: end,
),); ),
);
} }
} }

View File

@@ -0,0 +1,276 @@
<?xml version='1.0' encoding='UTF-8'?>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
xmlns='http://usefulinc.com/ns/doap#'
xmlns:foaf='http://xmlns.com/foaf/0.1/'
xmlns:xmpp='https://linkmauve.fr/ns/xmpp-doap#'>
<Project xml:lang='en'>
<name>moxxmpp</name>
<created>2021-12-26</created>
<homepage rdf:resource='https://codeberg.org/moxxy/moxxmpp'/>
<os>Linux</os>
<os>Windows</os>
<os>macOS</os>
<os>Android</os>
<os>iOS</os>
<programming-language>Dart</programming-language>
<maintainer>
<foaf:Person>
<foaf:name>Alexander "Polynomdivision"</foaf:name>
<foaf:homepage rdf:resource="https://blog.polynom.me" />
</foaf:Person>
</maintainer>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html" />
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.13.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.5rc3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.24.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Only jabber:x:oob</xmpp:note>
<xmpp:version>1.5</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Receiving data</xmpp:note>
<xmpp:version>1.1.4</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.5.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.3.0</xmpp:version>
<xmpp:note xml:lang="en">Not plugged into the UI</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.6</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Exists only as part of support for XEP-0280</xmpp:note>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Supports only Sha256, Sha512 and blake2b512</xmpp:note>
<xmpp:version>1.0.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Read-only support</xmpp:note>
<xmpp:version>0.4</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:note xml:lang="en">Write-only support</xmpp:note>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.6.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
<xmpp:note xml:lang="en">Only handles the success case; not accessible via the App</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.8.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0420.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.4.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0444.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0447.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0448.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0449.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-extensible-file-thumbnails.md" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
<xmpp:note xml:lang="en">Only Blurhash is implemented</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-file-upload-notification.md" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.0.5</xmpp:version>
<xmpp:note xml:lang="en">Sending and receiving implemented; cancellation not implemented</xmpp:note>
</xmpp:SupportedXep>
</implements>
</Project>
</rdf:RDF>

View File

@@ -1,6 +1,6 @@
name: moxxmpp name: moxxmpp
description: A pure-Dart XMPP library description: A pure-Dart XMPP library
version: 0.1.6+1 version: 0.2.0
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -21,7 +21,7 @@ dependencies:
version: ^0.1.5 version: ^0.1.5
omemo_dart: omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.4.2 version: ^0.4.3
random_string: ^2.3.1 random_string: ^2.3.1
saslprep: ^1.0.2 saslprep: ^1.0.2
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2

View File

@@ -0,0 +1,43 @@
import 'package:moxxmpp/src/util/queue.dart';
import 'package:test/test.dart';
void main() {
test('Test the async queue', () async {
final queue = AsyncQueue();
int future1Finish = 0;
int future2Finish = 0;
int future3Finish = 0;
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future1Finish = DateTime.now().millisecondsSinceEpoch));
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future2Finish = DateTime.now().millisecondsSinceEpoch));
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future3Finish = DateTime.now().millisecondsSinceEpoch));
await Future<void>.delayed(const Duration(seconds: 12));
// The three futures must be done
expect(future1Finish != 0, true);
expect(future2Finish != 0, true);
expect(future3Finish != 0, true);
// The end times of the futures must be ordered (on a timeline)
// |-- future1Finish -- future2Finish -- future3Finish --|
expect(
future1Finish < future2Finish && future1Finish < future3Finish,
true,
);
expect(
future2Finish < future3Finish && future2Finish > future1Finish,
true,
);
expect(
future3Finish > future1Finish && future3Finish > future2Finish,
true,
);
// The queue must be empty at the end
expect(queue.queue.isEmpty, true);
// The queue must not be executing anything at the end
expect(queue.isRunning, false);
});
}

View File

@@ -0,0 +1,104 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/awaiter.dart';
import 'package:test/test.dart';
void main() {
final bareJid = JID('moxxmpp', 'server3.example', '');
test('Test awaiting an awaited stanza with a from attribute', () async {
final awaiter = StanzaAwaiter();
// "Send" a stanza
final future = await awaiter.addPending('user1@server.example', 'abc123', 'iq');
// Receive the wrong answer
final result1 = await awaiter.onData(
XMLNode.fromString('<iq from="user3@server.example" id="abc123" type="result" />'),
bareJid,
);
expect(result1, false);
final result2 = await awaiter.onData(
XMLNode.fromString('<iq from="user1@server.example" id="lol" type="result" />'),
bareJid,
);
expect(result2, false);
// Receive the correct answer
final stanza = XMLNode.fromString('<iq from="user1@server.example" id="abc123" type="result" />');
final result3 = await awaiter.onData(
stanza,
bareJid,
);
expect(result3, true);
expect(await future, stanza);
});
test('Test awaiting an awaited stanza without a from attribute', () async {
final awaiter = StanzaAwaiter();
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
// Receive the wrong answer
final result1 = await awaiter.onData(
XMLNode.fromString('<iq id="lol" type="result" />'),
bareJid,
);
expect(result1, false);
// Receive the correct answer
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, true);
expect(await future, stanza);
});
test('Test awaiting a stanza that was already awaited', () async {
final awaiter = StanzaAwaiter();
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
// Receive the correct answer
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result1 = await awaiter.onData(
stanza,
bareJid,
);
expect(result1, true);
expect(await future, stanza);
// Receive it again
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, false);
});
test('Test ignoring a stanza that has the wrong tag', () async {
final awaiter = StanzaAwaiter();
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
// Receive the wrong answer
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result1 = await awaiter.onData(
XMLNode.fromString('<message id="abc123" type="result" />'),
bareJid,
);
expect(result1, false);
// Receive the correct answer
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, true);
expect(await future, stanza);
});
}

View File

@@ -54,16 +54,20 @@ void main() {
], ],
); );
final connection = XmppConnection(TestingReconnectionPolicy(), stubSocket) final connection = XmppConnection(
..registerFeatureNegotiators([ TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
stubSocket,
)..registerFeatureNegotiators([
StubNegotiator1(), StubNegotiator1(),
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

@@ -1,50 +0,0 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Make sure reply does not copy the children', () {
final stanza = Stanza.iq(
to: 'hallo',
from: 'world',
id: 'abc123',
type: 'get',
children: [
XMLNode(tag: 'test-tag'),
XMLNode(tag: 'test-tag2')
],
);
final reply = stanza.reply();
expect(reply.children, []);
expect(reply.type, 'result');
expect(reply.from, stanza.to);
expect(reply.to, stanza.from);
expect(reply.id, stanza.id);
});
test('Make sure reply includes the new children', () {
final stanza = Stanza.iq(
to: 'hallo',
from: 'world',
id: 'abc123',
type: 'get',
children: [
XMLNode(tag: 'test-tag'),
XMLNode(tag: 'test-tag2')
],
);
final reply = stanza.reply(
children: [
XMLNode.xmlns(
tag: 'test',
xmlns: 'test',
)
],
);
expect(reply.children.length, 1);
expect(reply.firstTag('test') != null, true);
});
}

View File

@@ -0,0 +1,40 @@
import 'package:test/test.dart';
import 'package:moxxmpp/src/util/wait.dart';
void main() {
test('Test adding and resolving', () async {
// ID -> Milliseconds since epoch
final tracker = WaitForTracker<int, int>();
int r2 = 0;
int r3 = 0;
// Queue some jobs
final r1 = await tracker.waitFor(0);
expect(r1, null);
tracker
.waitFor(0)
.then((result) async {
expect(result != null, true);
r2 = await result!;
});
tracker
.waitFor(0)
.then((result) async {
expect(result != null, true);
r3 = await result!;
});
final c = await tracker.waitFor(1);
expect(c, null);
// Resolve jobs
await tracker.resolve(0, 42);
await tracker.resolve(1, 25);
await tracker.resolve(2, -1);
expect(r2, 42);
expect(r3, 42);
});
}

View File

@@ -3,14 +3,14 @@ import 'package:test/test.dart';
void main() { void main() {
test('Parsing', () { test('Parsing', () {
const testData = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>"; const testData = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
final form = parseDataForm(XMLNode.fromString(testData)); final form = parseDataForm(XMLNode.fromString(testData));
expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo'); expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo');
expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]); expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]);
expect(form.getFieldByVar('os')?.values.first, 'Mac'); expect(form.getFieldByVar('os')?.values.first, 'Mac');
expect(form.getFieldByVar('os_version')?.values.first, '10.5.1'); expect(form.getFieldByVar('os_version')?.values.first, '10.5.1');
expect(form.getFieldByVar('software')?.values.first, 'Psi'); expect(form.getFieldByVar('software')?.values.first, 'Psi');
expect(form.getFieldByVar('software_version')?.values.first, '0.11'); expect(form.getFieldByVar('software_version')?.values.first, '0.11');
}); });
} }

View File

@@ -1,9 +1,13 @@
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';
void main() { void main() {
initLogger();
test('Test having multiple disco requests for the same JID', () async { test('Test having multiple disco requests for the same JID', () async {
final fakeSocket = StubTCPSocket( final fakeSocket = StubTCPSocket(
play: [ play: [
@@ -53,7 +57,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(
@@ -65,7 +69,11 @@ void main() {
], ],
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -73,10 +81,11 @@ void main() {
allowPlainAuth: true, allowPlainAuth: true,
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager(),
RosterManager(), RosterManager(TestingRosterStateManager(null, [])),
DiscoManager(), DiscoManager([]),
PingManager(), PingManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators( conn.registerFeatureNegotiators(
[ [
@@ -97,7 +106,7 @@ void main() {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
expect( expect(
disco.getRunningInfoQueries(DiscoCacheKey(jid.toString(), null)).length, disco.infoTracker.getRunningTasks(DiscoCacheKey(jid.toString(), null)).length,
1, 1,
); );
fakeSocket.injectRawXml("<iq type='result' id='${fakeSocket.lastId!}' from='romeo@montague.lit/orchard' to='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>"); fakeSocket.injectRawXml("<iq type='result' id='${fakeSocket.lastId!}' from='romeo@montague.lit/orchard' to='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>");
@@ -106,6 +115,6 @@ void main() {
expect(fakeSocket.getState(), 6); expect(fakeSocket.getState(), 6);
expect(await result1, await result2); expect(await result1, await result2);
expect(disco.hasInfoQueriesRunning(), false); expect(disco.infoTracker.hasTasksRunning(), false);
}); });
} }

View File

@@ -4,164 +4,167 @@ import 'package:test/test.dart';
void main() { void main() {
test('Test XEP example', () async { test('Test XEP example', () async {
final data = DiscoInfo( final data = DiscoInfo(
[ [
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc'
], ],
[ [
Identity( Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Exodus 0.9.1', name: 'Exodus 0.9.1',
) )
], ],
[], [],
JID.fromString('some@user.local/test'), null,
); JID.fromString('some@user.local/test'),
);
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0='); expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0=');
}); });
test('Test complex generation example', () async { test('Test complex generation example', () async {
const extDiscoDataString = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>"; const extDiscoDataString = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
final data = DiscoInfo( final data = DiscoInfo(
[ [
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc'
], ],
[ [
const Identity( const Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Psi 0.11', name: 'Psi 0.11',
lang: 'en', lang: 'en',
), ),
const Identity( const Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Ψ 0.11', name: 'Ψ 0.11',
lang: 'el', lang: 'el',
), ),
], ],
[ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ],
JID.fromString('some@user.local/test'), null,
); JID.fromString('some@user.local/test'),
);
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w='); expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w=');
}); });
test('Test Gajim capability hash computation', () async { test('Test Gajim capability hash computation', () async {
// TODO: This one fails // TODO: This one fails
/* /*
final data = DiscoInfo( final data = DiscoInfo(
features: [ features: [
"http://jabber.org/protocol/bytestreams", "http://jabber.org/protocol/bytestreams",
"http://jabber.org/protocol/muc", "http://jabber.org/protocol/muc",
"http://jabber.org/protocol/commands", "http://jabber.org/protocol/commands",
"http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/disco#info",
"jabber:iq:last", "jabber:iq:last",
"jabber:x:data", "jabber:x:data",
"jabber:x:encrypted", "jabber:x:encrypted",
"urn:xmpp:ping", "urn:xmpp:ping",
"http://jabber.org/protocol/chatstates", "http://jabber.org/protocol/chatstates",
"urn:xmpp:receipts", "urn:xmpp:receipts",
"urn:xmpp:time", "urn:xmpp:time",
"jabber:iq:version", "jabber:iq:version",
"http://jabber.org/protocol/rosterx", "http://jabber.org/protocol/rosterx",
"urn:xmpp:sec-label:0", "urn:xmpp:sec-label:0",
"jabber:x:conference", "jabber:x:conference",
"urn:xmpp:message-correct:0", "urn:xmpp:message-correct:0",
"urn:xmpp:chat-markers:0", "urn:xmpp:chat-markers:0",
"urn:xmpp:eme:0", "urn:xmpp:eme:0",
"http://jabber.org/protocol/xhtml-im", "http://jabber.org/protocol/xhtml-im",
"urn:xmpp:hashes:2", "urn:xmpp:hashes:2",
"urn:xmpp:hash-function-text-names:md5", "urn:xmpp:hash-function-text-names:md5",
"urn:xmpp:hash-function-text-names:sha-1", "urn:xmpp:hash-function-text-names:sha-1",
"urn:xmpp:hash-function-text-names:sha-256", "urn:xmpp:hash-function-text-names:sha-256",
"urn:xmpp:hash-function-text-names:sha-512", "urn:xmpp:hash-function-text-names:sha-512",
"urn:xmpp:hash-function-text-names:sha3-256", "urn:xmpp:hash-function-text-names:sha3-256",
"urn:xmpp:hash-function-text-names:sha3-512", "urn:xmpp:hash-function-text-names:sha3-512",
"urn:xmpp:hash-function-text-names:id-blake2b256", "urn:xmpp:hash-function-text-names:id-blake2b256",
"urn:xmpp:hash-function-text-names:id-blake2b512", "urn:xmpp:hash-function-text-names:id-blake2b512",
"urn:xmpp:jingle:1", "urn:xmpp:jingle:1",
"urn:xmpp:jingle:apps:file-transfer:5", "urn:xmpp:jingle:apps:file-transfer:5",
"urn:xmpp:jingle:security:xtls:0", "urn:xmpp:jingle:security:xtls:0",
"urn:xmpp:jingle:transports:s5b:1", "urn:xmpp:jingle:transports:s5b:1",
"urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:jingle:transports:ibb:1",
"urn:xmpp:avatar:metadata+notify", "urn:xmpp:avatar:metadata+notify",
"urn:xmpp:message-moderate:0", "urn:xmpp:message-moderate:0",
"http://jabber.org/protocol/tune+notify", "http://jabber.org/protocol/tune+notify",
"http://jabber.org/protocol/geoloc+notify", "http://jabber.org/protocol/geoloc+notify",
"http://jabber.org/protocol/nick+notify", "http://jabber.org/protocol/nick+notify",
"eu.siacs.conversations.axolotl.devicelist+notify", "eu.siacs.conversations.axolotl.devicelist+notify",
], ],
identities: [ identities: [
Identity( Identity(
category: "client", category: "client",
type: "pc", type: "pc",
name: "Gajim" name: "Gajim"
) )
] ]
); );
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs="); expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs=");
*/ */
}); });
test('Test Conversations hash computation', () async { test('Test Conversations hash computation', () async {
final data = DiscoInfo( final data = DiscoInfo(
[ [
'eu.siacs.conversations.axolotl.devicelist+notify', 'eu.siacs.conversations.axolotl.devicelist+notify',
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/chatstates', 'http://jabber.org/protocol/chatstates',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/muc', 'http://jabber.org/protocol/muc',
'http://jabber.org/protocol/nick+notify', 'http://jabber.org/protocol/nick+notify',
'jabber:iq:version', 'jabber:iq:version',
'jabber:x:conference', 'jabber:x:conference',
'jabber:x:oob', 'jabber:x:oob',
'storage:bookmarks+notify', 'storage:bookmarks+notify',
'urn:xmpp:avatar:metadata+notify', 'urn:xmpp:avatar:metadata+notify',
'urn:xmpp:chat-markers:0', 'urn:xmpp:chat-markers:0',
'urn:xmpp:jingle-message:0', 'urn:xmpp:jingle-message:0',
'urn:xmpp:jingle:1', 'urn:xmpp:jingle:1',
'urn:xmpp:jingle:apps:dtls:0', 'urn:xmpp:jingle:apps:dtls:0',
'urn:xmpp:jingle:apps:file-transfer:3', 'urn:xmpp:jingle:apps:file-transfer:3',
'urn:xmpp:jingle:apps:file-transfer:4', 'urn:xmpp:jingle:apps:file-transfer:4',
'urn:xmpp:jingle:apps:file-transfer:5', 'urn:xmpp:jingle:apps:file-transfer:5',
'urn:xmpp:jingle:apps:rtp:1', 'urn:xmpp:jingle:apps:rtp:1',
'urn:xmpp:jingle:apps:rtp:audio', 'urn:xmpp:jingle:apps:rtp:audio',
'urn:xmpp:jingle:apps:rtp:video', 'urn:xmpp:jingle:apps:rtp:video',
'urn:xmpp:jingle:jet-omemo:0', 'urn:xmpp:jingle:jet-omemo:0',
'urn:xmpp:jingle:jet:0', 'urn:xmpp:jingle:jet:0',
'urn:xmpp:jingle:transports:ibb:1', 'urn:xmpp:jingle:transports:ibb:1',
'urn:xmpp:jingle:transports:ice-udp:1', 'urn:xmpp:jingle:transports:ice-udp:1',
'urn:xmpp:jingle:transports:s5b:1', 'urn:xmpp:jingle:transports:s5b:1',
'urn:xmpp:message-correct:0', 'urn:xmpp:message-correct:0',
'urn:xmpp:ping', 'urn:xmpp:ping',
'urn:xmpp:receipts', 'urn:xmpp:receipts',
'urn:xmpp:time' 'urn:xmpp:time'
], ],
[ [
Identity( Identity(
category: 'client', category: 'client',
type: 'phone', type: 'phone',
name: 'Conversations', name: 'Conversations',
) )
], ],
[], [],
JID.fromString('user@server.local/test'), null,
); JID.fromString('user@server.local/test'),
);
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA='); expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA=');
}); });
} }

View File

@@ -17,7 +17,7 @@ Future<void> runOutgoingStanzaHandlers(StreamManagementManager man, Stanza stanz
XmppManagerAttributes mkAttributes(void Function(Stanza) callback) { XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
return XmppManagerAttributes( return XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false }) async { sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
callback(stanza); callback(stanza);
return Stanza.message(); return Stanza.message();
@@ -34,7 +34,7 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('hallo@example.server/uwu'), getFullJID: () => JID.fromString('hallo@example.server/uwu'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub, getNegotiatorById: getNegotiatorNullStub,
); );
} }
@@ -233,7 +233,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -242,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(
[ [
@@ -343,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(
@@ -355,7 +360,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -364,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(
[ [
@@ -510,7 +520,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -518,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(),
]); ]);
@@ -602,7 +616,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -610,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(),
]); ]);
@@ -694,7 +712,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -702,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

@@ -5,7 +5,7 @@ import '../helpers/xmpp.dart';
void main() { void main() {
test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities", () async { test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities", () async {
final attributes = XmppManagerAttributes( final attributes = XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async { sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
// ignore: avoid_print // ignore: avoid_print
print('==> ${stanza.toXml()}'); print('==> ${stanza.toXml()}');
return XMLNode(tag: 'iq', attributes: { 'type': 'result' }); return XMLNode(tag: 'iq', attributes: { 'type': 'result' });
@@ -22,7 +22,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('bob@xmpp.example/uwu'), getFullJID: () => JID.fromString('bob@xmpp.example/uwu'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub, getNegotiatorById: getNegotiatorNullStub,
); );
final manager = CarbonsManager(); final manager = CarbonsManager();

View File

@@ -29,59 +29,62 @@ T? getUnsupportedCSINegotiator<T extends XmppFeatureNegotiatorBase>(String id) {
void main() { void main() {
group('Test the XEP-0352 implementation', () { group('Test the XEP-0352 implementation', () {
test('Test setting the CSI state when CSI is unsupported', () { test('Test setting the CSI state when CSI is unsupported', () {
var nonzaSent = false; var nonzaSent = false;
final csi = CSIManager(); final csi = CSIManager();
csi.register(XmppManagerAttributes( csi.register(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), XmppManagerAttributes(
sendEvent: (event) {}, sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendNonza: (nonza) { sendEvent: (event) {},
nonzaSent = true; sendNonza: (nonza) {
}, nonzaSent = true;
getConnectionSettings: () => ConnectionSettings( },
jid: JID.fromString('some.user@example.server'), getConnectionSettings: () => ConnectionSettings(
password: 'password', jid: JID.fromString('some.user@example.server'),
useDirectTLS: true, password: 'password',
allowPlainAuth: false, useDirectTLS: true,
), allowPlainAuth: false,
getManagerById: getManagerNullStub, ),
getNegotiatorById: getUnsupportedCSINegotiator, getManagerById: getManagerNullStub,
isFeatureSupported: (_) => false, getNegotiatorById: getUnsupportedCSINegotiator,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), isFeatureSupported: (_) => false,
getSocket: () => StubTCPSocket(play: []), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getSocket: () => StubTCPSocket(play: []),
), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
); ),
);
csi.setActive(); csi.setActive();
csi.setInactive(); csi.setInactive();
expect(nonzaSent, false, reason: 'Expected that no nonza is sent'); expect(nonzaSent, false, reason: 'Expected that no nonza is sent');
}); });
test('Test setting the CSI state when CSI is supported', () { test('Test setting the CSI state when CSI is supported', () {
final csi = CSIManager(); final csi = CSIManager();
csi.register(XmppManagerAttributes( csi.register(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), XmppManagerAttributes(
sendEvent: (event) {}, sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendNonza: (nonza) { sendEvent: (event) {},
expect(nonza.attributes['xmlns'] == csiXmlns, true, reason: "Expected only nonzas with XMLNS '$csiXmlns'"); sendNonza: (nonza) {
}, expect(nonza.attributes['xmlns'] == csiXmlns, true, reason: "Expected only nonzas with XMLNS '$csiXmlns'");
getConnectionSettings: () => ConnectionSettings( },
jid: JID.fromString('some.user@example.server'), getConnectionSettings: () => ConnectionSettings(
password: 'password', jid: JID.fromString('some.user@example.server'),
useDirectTLS: true, password: 'password',
allowPlainAuth: false, useDirectTLS: true,
), allowPlainAuth: false,
getManagerById: getManagerNullStub, ),
getNegotiatorById: getSupportedCSINegotiator, getManagerById: getManagerNullStub,
isFeatureSupported: (_) => false, getNegotiatorById: getSupportedCSINegotiator,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), isFeatureSupported: (_) => false,
getSocket: () => StubTCPSocket(play: []), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getSocket: () => StubTCPSocket(play: []),
),); getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),
);
csi.setActive(); csi.setActive();
csi.setInactive(); csi.setInactive();
}); });
}); });
} }

View File

@@ -3,52 +3,52 @@ import 'package:test/test.dart';
void main() { void main() {
group('Test the XEP-0363 header preparation', () { group('Test the XEP-0363 header preparation', () {
test('invariance', () { test('invariance', () {
final headers = { final headers = {
'authorization': 'Basic Base64String==', 'authorization': 'Basic Base64String==',
'cookie': 'foo=bar; user=romeo' 'cookie': 'foo=bar; user=romeo'
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
headers, headers,
); );
}); });
test('invariance through uppercase', () { test('invariance through uppercase', () {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo' 'Cookie': 'foo=bar; user=romeo'
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
headers, headers,
); );
}); });
test('remove unspecified headers', () { test('remove unspecified headers', () {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
'X-Tracking': 'Base64String==' 'X-Tracking': 'Base64String=='
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
{ {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
} }
); );
}); });
test('remove newlines', () { test('remove newlines', () {
final headers = { final headers = {
'Authorization': '\n\nBasic Base64String==\n\n', 'Authorization': '\n\nBasic Base64String==\n\n',
'\nCookie\r\n': 'foo=bar; user=romeo', '\nCookie\r\n': 'foo=bar; user=romeo',
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
{ {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
} }
); );
}); });
}); });
} }

View File

@@ -9,7 +9,7 @@ Future<bool> testRosterManager(String bareJid, String resource, String stanzaStr
var eventTriggered = false; var eventTriggered = false;
final roster = RosterManager(TestingRosterStateManager('', [])); final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes( roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) { sendEvent: (event) {
eventTriggered = true; eventTriggered = true;
}, },
@@ -25,7 +25,7 @@ Future<bool> testRosterManager(String bareJid, String resource, String stanzaStr
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('$bareJid/$resource'), getFullJID: () => JID.fromString('$bareJid/$resource'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),); ),);
final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString)); final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString));
@@ -118,7 +118,10 @@ void main() {
], ],
); );
// TODO: This test is broken since we query the server and enable carbons // TODO: This test is broken since we query the server and enable carbons
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -126,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(
[ [
@@ -172,7 +176,11 @@ void main() {
], ],
); );
var receivedEvent = false; var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -180,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()
@@ -226,7 +235,11 @@ void main() {
], ],
); );
var receivedEvent = false; var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@@ -234,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()
@@ -310,7 +324,7 @@ void main() {
var eventTriggered = false; var eventTriggered = false;
final roster = RosterManager(TestingRosterStateManager('', [])); final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes( roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) { sendEvent: (event) {
eventTriggered = true; eventTriggered = true;
}, },
@@ -326,7 +340,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),); ),);
// NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html // NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html