diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..cd9de33 --- /dev/null +++ b/.gitlint @@ -0,0 +1,14 @@ +[general] +ignore=B5,B6,B7,B8 + +[title-max-length] +line-length=72 + +[title-trailing-punctuation] +[title-hard-tab] +[title-match-regex] +regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core)+(,(meta|tests|style|docs|xep|core))*\)|release): [A-Z0-9].*$ + + +[body-trailing-whitespace] +[body-first-line-empty] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a44921d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contribution Guide + +Thanks for your interest in the moxxmpp XMPP library! This document contains guidelines and guides for working on the moxxmpp codebase. + +## Contributing + +If you want to fix a small issue, you can just fork, create a new branch, and start working right away. However, if you want to work +on a bigger feature, please first create an issue (if an issue does not already exist) or join the [development chat](xmpp:moxxy@muc.moxxy.org?join) (xmpp:moxxy@muc.moxxy.org?join) +to discuss the feature first. + +Before creating a pull request, please make sure you checked every item on the following checklist: + +- [ ] I formatted the code with the dart formatter (`dart format`) before running the linter +- [ ] I ran the linter (`dart analyze`) and introduced no new linter warnings +- [ ] I ran the tests (`dart test`) and introduced no new failing tests +- [ ] I used [gitlint](https://github.com/jorisroovers/gitlint) to ensure propper formatting of my commig messages + +If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion +can happen there. If you think your PR is ready for review, remove the "WIP: " prefix. diff --git a/flake.lock b/flake.lock index 842552d..e80d64b 100644 --- a/flake.lock +++ b/flake.lock @@ -17,16 +17,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1667610399, - "narHash": "sha256-XZd0f4ZWAY0QOoUSdiNWj/eFiKb4B9CJPtl9uO9SYY4=", - "owner": "NixOS", + "lastModified": 1676076353, + "narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=", + "owner": "AtaraxiaSjel", "repo": "nixpkgs", - "rev": "1dd8696f96db47156e1424a49578fe7dd4ce99a4", + "rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", + "owner": "AtaraxiaSjel", + "ref": "update/flutter", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index e7fe485..27bc663 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "moxxmpp"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter"; flake-utils.url = "github:numtide/flake-utils"; }; diff --git a/packages/moxxmpp/lib/src/awaiter.dart b/packages/moxxmpp/lib/src/awaiter.dart index 786da64..c730038 100644 --- a/packages/moxxmpp/lib/src/awaiter.dart +++ b/packages/moxxmpp/lib/src/awaiter.dart @@ -18,16 +18,16 @@ class _StanzaSurrogateKey { /// The tag name of the stanza. final String tag; - + @override int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode; @override - bool operator==(Object other) { + bool operator ==(Object other) { return other is _StanzaSurrogateKey && - other.sentTo == sentTo && - other.id == id && - other.tag == tag; + other.sentTo == sentTo && + other.id == id && + other.tag == tag; } } @@ -71,7 +71,7 @@ class StanzaAwaiter { 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. diff --git a/packages/moxxmpp/lib/src/buffer.dart b/packages/moxxmpp/lib/src/buffer.dart index 0379cc6..ab0e497 100644 --- a/packages/moxxmpp/lib/src/buffer.dart +++ b/packages/moxxmpp/lib/src/buffer.dart @@ -6,22 +6,27 @@ import 'package:xml/xml.dart'; import 'package:xml/xml_events.dart'; class XmlStreamBuffer extends StreamTransformerBase { - - XmlStreamBuffer() : _streamController = StreamController(), _decoder = const XmlNodeDecoder(); + XmlStreamBuffer() + : _streamController = StreamController(), + _decoder = const XmlNodeDecoder(); final StreamController _streamController; final XmlNodeDecoder _decoder; @override Stream bind(Stream stream) { - stream.toXmlEvents().selectSubtreeEvents((event) { - return event.qualifiedName != 'stream:stream'; - }).transform(_decoder).listen((nodes) { - for (final node in nodes) { - if (node.nodeType == XmlNodeType.ELEMENT) { - _streamController.add(XMLNode.fromXmlElement(node as XmlElement)); - } - } - }); + stream + .toXmlEvents() + .selectSubtreeEvents((event) { + return event.qualifiedName != 'stream:stream'; + }) + .transform(_decoder) + .listen((nodes) { + for (final node in nodes) { + if (node.nodeType == XmlNodeType.ELEMENT) { + _streamController.add(XMLNode.fromXmlElement(node as XmlElement)); + } + } + }); return _streamController.stream; } } diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 3b5f740..7aa731d 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -61,27 +61,26 @@ enum StanzaFromType { /// Nonza describing the XMPP stream header. class StreamHeaderNonza extends XMLNode { - StreamHeaderNonza(String serverDomain) : super( - tag: 'stream:stream', - attributes: { - 'xmlns': stanzaXmlns, - 'version': '1.0', - 'xmlns:stream': streamXmlns, - 'to': serverDomain, - 'xml:lang': 'en', - }, - closeTag: false, - ); + StreamHeaderNonza(String serverDomain) + : super( + tag: 'stream:stream', + attributes: { + 'xmlns': stanzaXmlns, + 'version': '1.0', + 'xmlns:stream': streamXmlns, + 'to': serverDomain, + 'xml:lang': 'en', + }, + closeTag: false, + ); } /// The result of an awaited connection. class XmppConnectionResult { const XmppConnectionResult( - this.success, - { - this.error, - } - ); + this.success, { + this.error, + }); /// True if the connection was successful. False if it failed for any reason. final bool success; @@ -96,13 +95,11 @@ class XmppConnection { XmppConnection( ReconnectionPolicy reconnectionPolicy, ConnectivityManager connectivityManager, - this._socket, - { - this.connectionPingDuration = const Duration(minutes: 3), - this.connectingTimeout = const Duration(minutes: 2), - } - ) : _reconnectionPolicy = reconnectionPolicy, - _connectivityManager = connectivityManager { + this._socket, { + this.connectionPingDuration = const Duration(minutes: 3), + this.connectingTimeout = const Duration(minutes: 2), + }) : _reconnectionPolicy = reconnectionPolicy, + _connectivityManager = connectivityManager { // Allow the reconnection policy to perform reconnections by itself _reconnectionPolicy.register( _attemptReconnection, @@ -115,7 +112,6 @@ class XmppConnection { _socket.getEventStream().listen(_handleSocketEvent); } - /// The state that the connection is currently in XmppConnectionState _connectionState = XmppConnectionState.notConnected; @@ -128,7 +124,7 @@ class XmppConnection { /// Connection settings late ConnectionSettings _connectionSettings; - /// A policy on how to reconnect + /// A policy on how to reconnect final ReconnectionPolicy _reconnectionPolicy; /// The class responsible for preventing errors on initial connection due @@ -137,15 +133,20 @@ class XmppConnection { /// A helper for handling await semantics with stanzas final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter(); - + /// Sorted list of handlers that we call or incoming and outgoing stanzas - final List _incomingStanzaHandlers = List.empty(growable: true); - final List _incomingPreStanzaHandlers = List.empty(growable: true); - final List _outgoingPreStanzaHandlers = List.empty(growable: true); - final List _outgoingPostStanzaHandlers = List.empty(growable: true); - final StreamController _eventStreamController = StreamController.broadcast(); + final List _incomingStanzaHandlers = + List.empty(growable: true); + final List _incomingPreStanzaHandlers = + List.empty(growable: true); + final List _outgoingPreStanzaHandlers = + List.empty(growable: true); + final List _outgoingPostStanzaHandlers = + List.empty(growable: true); + final StreamController _eventStreamController = + StreamController.broadcast(); final Map _xmppManagers = {}; - + /// Disco info we got after binding a resource (xmlns) final List _serverFeatures = List.empty(growable: true); @@ -185,10 +186,11 @@ class XmppConnection { final Map _featureNegotiators = {}; XmppFeatureNegotiatorBase? _currentNegotiator; final List _streamFeatures = List.empty(growable: true); + /// Prevent data from being passed to _currentNegotiator.negotiator while the negotiator /// is still running. final Lock _negotiationLock = Lock(); - + /// The logger for the class final Logger _log = Logger('XmppConnection'); @@ -200,29 +202,32 @@ class XmppConnection { /// and does the following: /// - if _isConnectionRunning is false, set it to true and return false. /// - if _isConnectionRunning is true, return true. - Future _testAndSetIsConnectionRunning() async => _connectionRunningLock.synchronized(() { - if (!_isConnectionRunning) { - _isConnectionRunning = true; - return false; - } + Future _testAndSetIsConnectionRunning() async => + _connectionRunningLock.synchronized(() { + if (!_isConnectionRunning) { + _isConnectionRunning = true; + return false; + } - return true; - }); + return true; + }); /// Enters the critical section for accessing [XmppConnection._isConnectionRunning] /// and sets it to false. - Future _resetIsConnectionRunning() async => _connectionRunningLock.synchronized(() => _isConnectionRunning = false); - + Future _resetIsConnectionRunning() async => + _connectionRunningLock.synchronized(() => _isConnectionRunning = false); + ReconnectionPolicy get reconnectionPolicy => _reconnectionPolicy; - + List get serverFeatures => _serverFeatures; bool get isAuthenticated => _isAuthenticated; - + /// Return the registered feature negotiator that has id [id]. Returns null if /// none can be found. - T? getNegotiatorById(String id) => _featureNegotiators[id] as T?; - + T? getNegotiatorById(String id) => + _featureNegotiators[id] as T?; + /// Registers a list of [XmppManagerBase] sub-classes as managers on this connection. Future registerManagers(List managers) async { for (final manager in managers) { @@ -247,7 +252,8 @@ class XmppConnection { _incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); _incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers()); _outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers()); - _outgoingPostStanzaHandlers.addAll(manager.getOutgoingPostStanzaHandlers()); + _outgoingPostStanzaHandlers + .addAll(manager.getOutgoingPostStanzaHandlers()); } // Sort them @@ -264,7 +270,7 @@ class XmppConnection { } } } - + /// Register a list of negotiator with the connection. void registerFeatureNegotiators(List negotiators) { for (final negotiator in negotiators) { @@ -296,19 +302,23 @@ class XmppConnection { // Prevent leaking the last active negotiator _currentNegotiator = null; } - + /// Generate an Id suitable for an origin-id or stanza id String generateId() { return _uuid.v4(); } - + /// Returns the Manager with id [id] or null if such a manager is not registered. - T? getManagerById(String id) => _xmppManagers[id] as T?; + T? getManagerById(String id) => + _xmppManagers[id] as T?; /// A [PresenceManager] is required, so have a wrapper for getting it. /// Returns the registered [PresenceManager]. PresenceManager getPresenceManager() { - assert(_xmppManagers.containsKey(presenceManager), 'A PresenceManager is mandatory'); + assert( + _xmppManagers.containsKey(presenceManager), + 'A PresenceManager is mandatory', + ); return getManagerById(presenceManager)!; } @@ -316,7 +326,10 @@ class XmppConnection { /// A [DiscoManager] is required so, have a wrapper for getting it. /// Returns the registered [DiscoManager]. DiscoManager getDiscoManager() { - assert(_xmppManagers.containsKey(discoManager), 'A DiscoManager is mandatory'); + assert( + _xmppManagers.containsKey(discoManager), + 'A DiscoManager is mandatory', + ); return getManagerById(discoManager)!; } @@ -324,11 +337,14 @@ class XmppConnection { /// A [RosterManager] is required, so have a wrapper for getting it. /// Returns the registered [RosterManager]. RosterManager getRosterManager() { - assert(_xmppManagers.containsKey(rosterManager), 'A RosterManager is mandatory'); + assert( + _xmppManagers.containsKey(rosterManager), + 'A RosterManager is mandatory', + ); return getManagerById(rosterManager)!; } - + /// Returns the registered [StreamManagementManager], if one is registered. StreamManagementManager? getStreamManagementManager() { return getManagerById(smManager); @@ -338,7 +354,7 @@ class XmppConnection { CSIManager? getCSIManager() { return getManagerById(csiManager); } - + /// Set the connection settings of this connection. void setConnectionSettings(ConnectionSettings settings) { _connectionSettings = settings; @@ -352,7 +368,9 @@ class XmppConnection { /// Attempts to reconnect to the server by following an exponential backoff. Future _attemptReconnection() async { if (await _testAndSetIsConnectionRunning()) { - _log.warning('_attemptReconnection is called but connection attempt is already running. Ignoring...'); + _log.warning( + '_attemptReconnection is called but connection attempt is already running. Ignoring...', + ); return; } @@ -369,17 +387,22 @@ class XmppConnection { _log.finest('Calling connect() from _attemptReconnection'); await connect(waitForConnection: true); } - + /// Called when a stream ending error has occurred Future handleError(XmppError error) async { _log.severe('handleError called with ${error.toString()}'); - + // Whenever we encounter an error that would trigger a reconnection attempt while // the connection result is being awaited, don't attempt a reconnection but instead // try to gracefully disconnect. if (_connectionCompleter != null) { - _log.info('Not triggering reconnection since connection result is being awaited'); - await _disconnect(triggeredByUser: false, state: XmppConnectionState.error); + _log.info( + 'Not triggering reconnection since connection result is being awaited', + ); + await _disconnect( + triggeredByUser: false, + state: XmppConnectionState.error, + ); _connectionCompleter?.complete( XmppConnectionResult( false, @@ -392,7 +415,9 @@ class XmppConnection { if (!error.isRecoverable()) { // We cannot recover this error - _log.severe('Since a $error is not recoverable, not attempting a reconnection'); + _log.severe( + 'Since a $error is not recoverable, not attempting a reconnection', + ); await _setConnectionState(XmppConnectionState.error); await _sendEvent( NonRecoverableErrorEvent(error), @@ -411,10 +436,14 @@ class XmppConnection { await handleError(SocketError(event)); } else if (event is XmppSocketClosureEvent) { if (!event.expected) { - _log.fine('Received unexpected XmppSocketClosureEvent. Reconnecting...'); + _log.fine( + 'Received unexpected XmppSocketClosureEvent. Reconnecting...', + ); await handleError(SocketError(XmppSocketErrorEvent(event))); } else { - _log.fine('Received XmppSocketClosureEvent. No reconnection attempt since _socketClosureTriggersReconnect is false...'); + _log.fine( + 'Received XmppSocketClosureEvent. No reconnection attempt since _socketClosureTriggersReconnect is false...', + ); } } } @@ -429,9 +458,9 @@ class XmppConnection { Future getConnectionState() async { return _connectionState; } - + /// Sends an [XMLNode] without any further processing to the server. - void sendRawXML(XMLNode node, { String? redact }) { + void sendRawXML(XMLNode node, {String? redact}) { final string = node.toXml(); _log.finest('==> $string'); _socket.write(string, redact: redact); @@ -441,15 +470,13 @@ class XmppConnection { void sendRawString(String raw) { _socket.write(raw); } - + /// Returns true if we can send data through the socket. Future _canSendData() async { - return [ - XmppConnectionState.connected, - XmppConnectionState.connecting - ].contains(await getConnectionState()); + return [XmppConnectionState.connected, XmppConnectionState.connecting] + .contains(await getConnectionState()); } - + /// Sends a [stanza] to the server. If stream management is enabled, then keeping track /// of the stanza is taken care of. Returns a Future that resolves when we receive a /// response to the stanza. @@ -459,25 +486,43 @@ class XmppConnection { /// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has /// none. // TODO(Unknown): if addId = false, the function crashes. - Future 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'); + Future 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', + ); // Add extra data in case it was not set var stanza_ = stanza; if (addId && (stanza_.id == null || stanza_.id == '')) { stanza_ = stanza.copyWith(id: generateId()); } - if (addFrom != StanzaFromType.none && (stanza_.from == null || stanza_.from == '')) { + if (addFrom != StanzaFromType.none && + (stanza_.from == null || stanza_.from == '')) { switch (addFrom) { - case StanzaFromType.full: { - stanza_ = stanza_.copyWith(from: _connectionSettings.jid.withResource(_resource).toString()); - } - break; - case StanzaFromType.bare: { - stanza_ = stanza_.copyWith(from: _connectionSettings.jid.toBare().toString()); - } - break; - case StanzaFromType.none: break; + case StanzaFromType.full: + { + stanza_ = stanza_.copyWith( + from: _connectionSettings.jid.withResource(_resource).toString(), + ); + } + break; + case StanzaFromType.bare: + { + stanza_ = stanza_.copyWith( + from: _connectionSettings.jid.toBare().toString(), + ); + } + break; + case StanzaFromType.none: + break; } } @@ -504,16 +549,16 @@ class XmppConnection { from: data.stanza.to, attributes: { 'type': 'error', - ...data.stanza.id != null ? { - 'id': data.stanza.id!, - } : {}, + ...data.stanza.id != null + ? { + 'id': data.stanza.id!, + } + : {}, }, ); } - final prefix = data.encrypted ? - '(Encrypted) ' : - ''; + final prefix = data.encrypted ? '(Encrypted) ' : ''; _log.finest('==> $prefix${stanza_.toXml()}'); final stanzaString = data.stanza.toXml(); @@ -572,18 +617,20 @@ class XmppConnection { _log.finest('Destroying connecting timeout timer...'); } } - + /// Sets the connection state to [state] and triggers an event of type /// [ConnectionStateChangedEvent]. Future _setConnectionState(XmppConnectionState state) async { // Ignore changes that are not really changes. if (state == _connectionState) return; - + _log.finest('Updating _connectionState from $_connectionState to $state'); final oldState = _connectionState; _connectionState = state; - final sm = getNegotiatorById(streamManagementNegotiator); + final sm = getNegotiatorById( + streamManagementNegotiator, + ); await _sendEvent( ConnectionStateChangedEvent( state, @@ -591,10 +638,11 @@ class XmppConnection { sm?.isResumed ?? false, ), ); - + if (state == XmppConnectionState.connected) { _log.finest('Starting _pingConnectionTimer'); - _connectionPingTimer = Timer.periodic(connectionPingDuration, _pingConnectionOpen); + _connectionPingTimer = + Timer.periodic(connectionPingDuration, _pingConnectionOpen); // We are connected, so the timer can stop. _destroyConnectingTimer(); @@ -617,7 +665,7 @@ class XmppConnection { } } } - + /// Sets the routing state and logs the change void _updateRoutingState(RoutingState state) { _log.finest('Updating _routingState from $_routingState to $state'); @@ -629,12 +677,12 @@ class XmppConnection { _log.finest('Updating _resource to $resource'); _resource = resource; } - + /// Returns the connection's events as a stream. Stream asBroadcastStream() { return _eventStreamController.stream.asBroadcastStream(); - } - + } + /// Timer callback to prevent the connection from timing out. Future _pingConnectionOpen(Timer timer) async { // Follow the recommendation of XEP-0198 and just request an ack. If SM is not enabled, @@ -645,14 +693,20 @@ class XmppConnection { _log.finest('_pingConnectionTimer: Connected. Triggering a ping event.'); unawaited(_sendEvent(SendPingEvent())); } else { - _log.finest('_pingConnectionTimer: Not connected. Not triggering an event.'); + _log.finest( + '_pingConnectionTimer: Not connected. Not triggering an event.', + ); } } /// Iterate over [handlers] and check if the handler matches [stanza]. If it does, /// call its callback and end the processing if the callback returned true; continue /// if it returned false. - Future _runStanzaHandlers(List handlers, Stanza stanza, { StanzaHandlerData? initial }) async { + Future _runStanzaHandlers( + List handlers, + Stanza stanza, { + StanzaHandlerData? initial, + }) async { var state = initial ?? StanzaHandlerData(false, false, null, stanza); for (final handler in handlers) { if (handler.matches(state.stanza)) { @@ -664,19 +718,36 @@ class XmppConnection { return state; } - Future _runIncomingStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { - return _runStanzaHandlers(_incomingStanzaHandlers, stanza, initial: initial); + Future _runIncomingStanzaHandlers( + Stanza stanza, { + StanzaHandlerData? initial, + }) async { + return _runStanzaHandlers( + _incomingStanzaHandlers, + stanza, + initial: initial, + ); } Future _runIncomingPreStanzaHandlers(Stanza stanza) async { return _runStanzaHandlers(_incomingPreStanzaHandlers, stanza); } - Future _runOutgoingPreStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { - return _runStanzaHandlers(_outgoingPreStanzaHandlers, stanza, initial: initial); + Future _runOutgoingPreStanzaHandlers( + Stanza stanza, { + StanzaHandlerData? initial, + }) async { + return _runStanzaHandlers( + _outgoingPreStanzaHandlers, + stanza, + initial: initial, + ); } - Future _runOutgoingPostStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { + Future _runOutgoingPostStanzaHandlers( + Stanza stanza, { + StanzaHandlerData? initial, + }) async { final data = await _runStanzaHandlers( _outgoingPostStanzaHandlers, stanza, @@ -684,7 +755,7 @@ class XmppConnection { ); return data.done; } - + /// Called whenever we receive a stanza after resource binding or stream resumption. Future _handleStanza(XMLNode nonza) async { // Process nonzas separately @@ -692,14 +763,12 @@ class XmppConnection { _log.finest('<== ${nonza.toXml()}'); var nonzaHandled = false; - await Future.forEach( - _xmppManagers.values, - (XmppManagerBase manager) async { - final handled = await manager.runNonzaHandlers(nonza); + await Future.forEach(_xmppManagers.values, + (XmppManagerBase manager) async { + final handled = await manager.runNonzaHandlers(nonza); - if (!nonzaHandled && handled) nonzaHandled = true; - } - ); + if (!nonzaHandled && handled) nonzaHandled = true; + }); if (!nonzaHandled) { _log.warning('Unhandled nonza received: ${nonza.toXml()}'); @@ -712,9 +781,10 @@ class XmppConnection { // Run the incoming stanza handlers and bounce with an error if no manager handled // it. final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza); - final prefix = incomingPreHandlers.encrypted && incomingPreHandlers.other['encryption_error'] == null ? - '(Encrypted) ' : - ''; + final prefix = incomingPreHandlers.encrypted && + incomingPreHandlers.other['encryption_error'] == null + ? '(Encrypted) ' + : ''; _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); final awaited = await _stanzaAwaiter.onData( @@ -745,11 +815,10 @@ class XmppConnection { /// Returns true if all mandatory features in [features] have been negotiated. /// Otherwise returns false. bool _isMandatoryNegotiationDone(List features) { - return features.every( - (XMLNode feature) { - return feature.firstTag('required') == null && feature.tag != 'mechanisms'; - } - ); + return features.every((XMLNode feature) { + return feature.firstTag('required') == null && + feature.tag != 'mechanisms'; + }); } /// Returns true if we can still negotiate. Returns false if no negotiator is @@ -761,20 +830,23 @@ class XmppConnection { /// Returns the next negotiator that matches [features]. Returns null if none can be /// picked. If [log] is true, then the list of matching negotiators will be logged. @visibleForTesting - XmppFeatureNegotiatorBase? getNextNegotiator(List features, {bool log = true}) { + XmppFeatureNegotiatorBase? getNextNegotiator( + List features, { + bool log = true, + }) { final matchingNegotiators = _featureNegotiators.values - .where( - (XmppFeatureNegotiatorBase negotiator) { - return negotiator.state == NegotiatorState.ready && negotiator.matchesFeature(features); - } - ) - .toList() + .where((XmppFeatureNegotiatorBase negotiator) { + return negotiator.state == NegotiatorState.ready && + negotiator.matchesFeature(features); + }).toList() ..sort((a, b) => b.priority.compareTo(a.priority)); if (log) { - _log.finest('List of matching negotiators: ${matchingNegotiators.map((a) => a.id)}'); + _log.finest( + 'List of matching negotiators: ${matchingNegotiators.map((a) => a.id)}', + ); } - + if (matchingNegotiators.isEmpty) return null; return matchingNegotiators.first; @@ -794,7 +866,7 @@ class XmppConnection { // Tell consumers of the event stream that we're done with stream feature // negotiations await _sendEvent(StreamNegotiationsDoneEvent()); - + // Send out initial presence await getPresenceManager().sendInitialPresence(); } @@ -802,7 +874,9 @@ class XmppConnection { Future _executeCurrentNegotiator(XMLNode nonza) async { // If we don't have a negotiator get one _currentNegotiator ??= getNextNegotiator(_streamFeatures); - if (_currentNegotiator == null && _isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { + if (_currentNegotiator == null && + _isMandatoryNegotiationDone(_streamFeatures) && + !_isNegotiationPossible(_streamFeatures)) { _log.finest('Negotiations done!'); _updateRoutingState(RoutingState.handleStanzas); await _onNegotiationsDone(); @@ -820,69 +894,73 @@ class XmppConnection { final state = result.get(); _currentNegotiator!.state = state; switch (state) { - case NegotiatorState.ready: return; + case NegotiatorState.ready: + return; case NegotiatorState.done: - if (_currentNegotiator!.sendStreamHeaderWhenDone) { - _currentNegotiator = null; - _streamFeatures.clear(); - _sendStreamHeader(); - } else { - _streamFeatures - .removeWhere((node) { - return node.attributes['xmlns'] == _currentNegotiator!.negotiatingXmlns; + if (_currentNegotiator!.sendStreamHeaderWhenDone) { + _currentNegotiator = null; + _streamFeatures.clear(); + _sendStreamHeader(); + } else { + _streamFeatures.removeWhere((node) { + return node.attributes['xmlns'] == + _currentNegotiator!.negotiatingXmlns; }); - _currentNegotiator = null; + _currentNegotiator = null; - if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { + if (_isMandatoryNegotiationDone(_streamFeatures) && + !_isNegotiationPossible(_streamFeatures)) { + _log.finest('Negotiations done!'); + _updateRoutingState(RoutingState.handleStanzas); + await _resetIsConnectionRunning(); + await _onNegotiationsDone(); + } else { + _currentNegotiator = getNextNegotiator(_streamFeatures); + _log.finest('Chose ${_currentNegotiator!.id} as next negotiator'); + + final fakeStanza = XMLNode( + tag: 'stream:features', + children: _streamFeatures, + ); + + await _executeCurrentNegotiator(fakeStanza); + } + } + break; + case NegotiatorState.retryLater: + _log.finest('Negotiator wants to continue later. Picking new one...'); + _currentNegotiator!.state = NegotiatorState.ready; + + if (_isMandatoryNegotiationDone(_streamFeatures) && + !_isNegotiationPossible(_streamFeatures)) { _log.finest('Negotiations done!'); + _updateRoutingState(RoutingState.handleStanzas); await _resetIsConnectionRunning(); await _onNegotiationsDone(); } else { + _log.finest('Picking new negotiator...'); _currentNegotiator = getNextNegotiator(_streamFeatures); - _log.finest('Chose ${_currentNegotiator!.id} as next negotiator'); - + _log.finest('Chose $_currentNegotiator as next negotiator'); final fakeStanza = XMLNode( tag: 'stream:features', children: _streamFeatures, ); - await _executeCurrentNegotiator(fakeStanza); } - } - break; - case NegotiatorState.retryLater: - _log.finest('Negotiator wants to continue later. Picking new one...'); - _currentNegotiator!.state = NegotiatorState.ready; - - - if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { - _log.finest('Negotiations done!'); + break; + case NegotiatorState.skipRest: + _log.finest( + 'Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!', + ); _updateRoutingState(RoutingState.handleStanzas); await _resetIsConnectionRunning(); await _onNegotiationsDone(); - } else { - _log.finest('Picking new negotiator...'); - _currentNegotiator = getNextNegotiator(_streamFeatures); - _log.finest('Chose $_currentNegotiator as next negotiator'); - final fakeStanza = XMLNode( - tag: 'stream:features', - children: _streamFeatures, - ); - await _executeCurrentNegotiator(fakeStanza); - } - break; - case NegotiatorState.skipRest: - _log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!'); - - _updateRoutingState(RoutingState.handleStanzas); - await _resetIsConnectionRunning(); - await _onNegotiationsDone(); - break; + break; } } - + /// Called whenever we receive data that has been parsed as XML. Future handleXmlStream(XMLNode node) async { // Check if we received a stream error @@ -891,7 +969,7 @@ class XmppConnection { ..finest('<== ${node.toXml()}') ..severe('Received a stream error! Attempting reconnection'); await handleError(StreamError()); - + return; } @@ -920,7 +998,7 @@ class XmppConnection { await _executeCurrentNegotiator(node); }); break; - case RoutingState.handleStanzas: + case RoutingState.handleStanzas: await _handleStanza(node); break; case RoutingState.preConnection: @@ -934,23 +1012,27 @@ class XmppConnection { void sendWhitespacePing() { _socket.write(''); } - + /// Sends an event to the connection's event stream. Future _sendEvent(XmppEvent event) async { _log.finest('Event: ${event.toString()}'); // Specific event handling if (event is ResourceBindingSuccessEvent) { - _log.finest('Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}'); + _log.finest( + 'Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}', + ); setResource(event.resource); _log.finest('Resetting _serverFeatures'); _serverFeatures.clear(); } else if (event is AuthenticationSuccessEvent) { - _log.finest('Received AuthenticationSuccessEvent. Setting _isAuthenticated to true'); + _log.finest( + 'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true', + ); _isAuthenticated = true; } - + for (final manager in _xmppManagers.values) { await manager.onXmppEvent(event); } @@ -963,9 +1045,7 @@ class XmppConnection { _socket.write( XMLNode( tag: 'xml', - attributes: { - 'version': '1.0' - }, + attributes: {'version': '1.0'}, closeTag: false, isDeclaration: true, children: [ @@ -987,7 +1067,10 @@ class XmppConnection { await _disconnect(state: XmppConnectionState.notConnected); } - Future _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async { + Future _disconnect({ + required XmppConnectionState state, + bool triggeredByUser = true, + }) async { await _reconnectionPolicy.setShouldReconnect(false); if (triggeredByUser) { @@ -1008,18 +1091,33 @@ class XmppConnection { await getStreamManagementManager()?.resetState(); } } - + /// Make sure that all required managers are registered void _runPreConnectionAssertions() { - assert(_xmppManagers.containsKey(presenceManager), 'A PresenceManager is mandatory'); - assert(_xmppManagers.containsKey(rosterManager), 'A RosterManager is mandatory'); - assert(_xmppManagers.containsKey(discoManager), 'A DiscoManager is mandatory'); - assert(_xmppManagers.containsKey(pingManager), 'A PingManager is mandatory'); + assert( + _xmppManagers.containsKey(presenceManager), + 'A PresenceManager is mandatory', + ); + assert( + _xmppManagers.containsKey(rosterManager), + 'A RosterManager is mandatory', + ); + assert( + _xmppManagers.containsKey(discoManager), + 'A DiscoManager is mandatory', + ); + assert( + _xmppManagers.containsKey(pingManager), + 'A PingManager is mandatory', + ); } - + /// Like [connect] but the Future resolves when the resource binding is either done or /// SASL has failed. - Future connectAwaitable({ String? lastResource, bool waitForConnection = false }) async { + Future connectAwaitable({ + String? lastResource, + bool waitForConnection = false, + }) async { _runPreConnectionAssertions(); await _resetIsConnectionRunning(); _connectionCompleter = Completer(); @@ -1031,17 +1129,24 @@ class XmppConnection { ); return _connectionCompleter!.future; } - + /// Start the connection process using the provided connection settings. - Future connect({ String? lastResource, bool waitForConnection = false, bool shouldReconnect = true }) async { - if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) { - _log.fine('Cancelling this connection attempt as one appears to be already running.'); + Future connect({ + String? lastResource, + bool waitForConnection = false, + bool shouldReconnect = true, + }) async { + if (_connectionState != XmppConnectionState.notConnected && + _connectionState != XmppConnectionState.error) { + _log.fine( + 'Cancelling this connection attempt as one appears to be already running.', + ); return; } - + _runPreConnectionAssertions(); await _resetIsConnectionRunning(); - + if (lastResource != null) { setResource(lastResource); } @@ -1059,7 +1164,7 @@ class XmppConnection { await _connectivityManager.waitForConnection(); _log.info('Got okay from connectivityManager'); } - + final smManager = getStreamManagementManager(); String? host; int? port; diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 180a6f9..54b4c30 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -30,7 +30,7 @@ class ConnectionStateChangedEvent extends XmppEvent { /// Triggered when we encounter a stream error. class StreamErrorEvent extends XmppEvent { - StreamErrorEvent({ required this.error }); + StreamErrorEvent({required this.error}); final String error; } @@ -48,7 +48,7 @@ class SendPingEvent extends XmppEvent {} /// Triggered when the stream resumption was successful class StreamResumedEvent extends XmppEvent { - StreamResumedEvent({ required this.h }); + StreamResumedEvent({required this.h}); final int h; } @@ -128,7 +128,7 @@ class MessageEvent extends XmppEvent { /// Triggered when a client responds to our delivery receipt request class DeliveryReceiptReceivedEvent extends XmppEvent { - DeliveryReceiptReceivedEvent({ required this.from, required this.id }); + DeliveryReceiptReceivedEvent({required this.from, required this.id}); final JID from; final String id; } @@ -147,9 +147,9 @@ class ChatMarkerEvent extends XmppEvent { // Triggered when we received a Stream resumption ID class StreamManagementEnabledEvent extends XmppEvent { StreamManagementEnabledEvent({ - required this.resource, - this.id, - this.location, + required this.resource, + this.id, + this.location, }); final String resource; final String? id; @@ -158,7 +158,7 @@ class StreamManagementEnabledEvent extends XmppEvent { /// Triggered when we bound a resource class ResourceBindingSuccessEvent extends XmppEvent { - ResourceBindingSuccessEvent({ required this.resource }); + ResourceBindingSuccessEvent({required this.resource}); final String resource; } @@ -182,13 +182,17 @@ class ServerItemDiscoEvent extends XmppEvent { /// Triggered when we receive a subscription request class SubscriptionRequestReceivedEvent extends XmppEvent { - SubscriptionRequestReceivedEvent({ required this.from }); + SubscriptionRequestReceivedEvent({required this.from}); final JID from; } /// Triggered when we receive a new or updated avatar class AvatarUpdatedEvent extends XmppEvent { - AvatarUpdatedEvent({ required this.jid, required this.base64, required this.hash }); + AvatarUpdatedEvent({ + required this.jid, + required this.base64, + required this.hash, + }); final String jid; final String base64; final String hash; @@ -196,7 +200,7 @@ class AvatarUpdatedEvent extends XmppEvent { /// Triggered when a PubSub notification has been received class PubSubNotificationEvent extends XmppEvent { - PubSubNotificationEvent({ required this.item, required this.from }); + PubSubNotificationEvent({required this.item, required this.from}); final PubSubItem item; final String from; } @@ -209,13 +213,13 @@ class StanzaAckedEvent extends XmppEvent { /// Triggered when receiving a push of the blocklist class BlocklistBlockPushEvent extends XmppEvent { - BlocklistBlockPushEvent({ required this.items }); + BlocklistBlockPushEvent({required this.items}); final List items; } /// Triggered when receiving a push of the blocklist class BlocklistUnblockPushEvent extends XmppEvent { - BlocklistUnblockPushEvent({ required this.items }); + BlocklistUnblockPushEvent({required this.items}); final List items; } @@ -242,7 +246,7 @@ class OmemoDeviceListUpdatedEvent extends XmppEvent { /// error. class NonRecoverableErrorEvent extends XmppEvent { NonRecoverableErrorEvent(this.error); - + /// The error in question. final XmppError error; } diff --git a/packages/moxxmpp/lib/src/iq.dart b/packages/moxxmpp/lib/src/iq.dart index 47cce7c..c20b6fe 100644 --- a/packages/moxxmpp/lib/src/iq.dart +++ b/packages/moxxmpp/lib/src/iq.dart @@ -5,7 +5,10 @@ import 'package:moxxmpp/src/stanza.dart'; /// Bounce a stanza if it was not handled by any manager. [conn] is the connection object /// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled /// stanza. -Future handleUnhandledStanza(XmppConnection conn, StanzaHandlerData data) async { +Future handleUnhandledStanza( + XmppConnection conn, + StanzaHandlerData data, +) async { if (data.stanza.type != 'error' && data.stanza.type != 'result') { final stanza = data.stanza.copyWith( to: data.stanza.from, diff --git a/packages/moxxmpp/lib/src/jid.dart b/packages/moxxmpp/lib/src/jid.dart index 24100d2..b3daf56 100644 --- a/packages/moxxmpp/lib/src/jid.dart +++ b/packages/moxxmpp/lib/src/jid.dart @@ -18,7 +18,10 @@ class JID { } else { resourcePart = slashParts.sublist(1).join('/'); - assert(resourcePart.isNotEmpty, 'Resource part cannot be there and empty'); + assert( + resourcePart.isNotEmpty, + 'Resource part cannot be there and empty', + ); } final atParts = slashParts.first.split('@'); @@ -34,9 +37,9 @@ class JID { return JID( localPart, - domainPart.endsWith('.') ? - domainPart.substring(0, domainPart.length - 1) : - domainPart, + domainPart.endsWith('.') + ? domainPart.substring(0, domainPart.length - 1) + : domainPart, resourcePart, ); } @@ -53,7 +56,7 @@ class JID { /// Converts the JID into a bare JID. JID toBare() { if (isBare()) return this; - + return JID(local, domain, ''); } @@ -63,12 +66,12 @@ class JID { /// Compares the JID with [other]. This function assumes that JID and [other] /// are bare, i.e. only the domain- and localparts are compared. If [ensureBare] /// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned. - bool bareCompare(JID other, { bool ensureBare = false }) { + bool bareCompare(JID other, {bool ensureBare = false}) { if (ensureBare && !other.isBare()) return false; return local == other.local && domain == other.domain; } - + /// Converts to JID instance into its string representation of /// localpart@domainpart/resource. @override @@ -90,7 +93,9 @@ class JID { @override bool operator ==(Object other) { if (other is JID) { - return other.local == local && other.domain == domain && other.resource == resource; + return other.local == local && + other.domain == domain && + other.resource == resource; } return false; diff --git a/packages/moxxmpp/lib/src/managers/attributes.dart b/packages/moxxmpp/lib/src/managers/attributes.dart index c4525be..9143d58 100644 --- a/packages/moxxmpp/lib/src/managers/attributes.dart +++ b/packages/moxxmpp/lib/src/managers/attributes.dart @@ -22,8 +22,16 @@ class XmppManagerAttributes { required this.getConnection, required this.getNegotiatorById, }); + /// Send a stanza whose response can be awaited. - final Future Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted, bool forceEncryption}) sendStanza; + final Future Function( + Stanza stanza, { + StanzaFromType addFrom, + bool addId, + bool awaitable, + bool encrypted, + bool forceEncryption, + }) sendStanza; /// Send a nonza. final void Function(XMLNode) sendNonza; @@ -39,7 +47,7 @@ class XmppManagerAttributes { /// Returns true if a server feature is supported final bool Function(String) isFeatureSupported; - + /// Returns the full JID of the current account final JID Function() getFullJID; @@ -49,5 +57,6 @@ class XmppManagerAttributes { /// Return the [XmppConnection] the manager is registered against. final XmppConnection Function() getConnection; - final T? Function(String) getNegotiatorById; + final T? Function(String) + getNegotiatorById; } diff --git a/packages/moxxmpp/lib/src/managers/base.dart b/packages/moxxmpp/lib/src/managers/base.dart index b01b0b2..8d0ebb1 100644 --- a/packages/moxxmpp/lib/src/managers/base.dart +++ b/packages/moxxmpp/lib/src/managers/base.dart @@ -18,13 +18,13 @@ abstract class XmppManagerBase { /// Flag indicating that the post registration callback has been called once. bool initialized = false; - + /// Registers the callbacks from XmppConnection with the manager void register(XmppManagerAttributes attributes) { _managerAttributes = attributes; _log = Logger(name); } - + /// Returns the attributes that are registered with the manager. /// Must only be called after register has been called on it. XmppManagerAttributes getAttributes() { @@ -40,7 +40,7 @@ abstract class XmppManagerBase { /// send. These are run after the stanza is sent. The higher the value of the /// handler's priority, the earlier it is run. List getOutgoingPostStanzaHandlers() => []; - + /// Return the StanzaHandlers associated with this manager that deal with stanzas we /// receive. The higher the value of the /// handler's priority, the earlier it is run. @@ -51,7 +51,7 @@ abstract class XmppManagerBase { /// as we have to decrypt the stanza before we do anything else. The higher the value /// of the handler's priority, the earlier it is run. List getIncomingPreStanzaHandlers() => []; - + /// Return the NonzaHandlers associated with this manager. The higher the value of the /// handler's priority, the earlier it is run. List getNonzaHandlers() => []; @@ -61,16 +61,16 @@ abstract class XmppManagerBase { /// Return a list of identities that should be included in a disco response. List getDiscoIdentities() => []; - + /// Return the Id (akin to xmlns) of this manager. final String id; /// The name of the manager. String get name => toString(); - + /// Return the logger for this manager. Logger get logger => _log; - + /// Called when XmppConnection triggers an event Future onXmppEvent(XmppEvent event) async {} @@ -94,20 +94,17 @@ abstract class XmppManagerBase { } } } - + /// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if /// the nonza has been handled by one of the handlers. Resolves to false otherwise. Future runNonzaHandlers(XMLNode nonza) async { var handled = false; - await Future.forEach( - getNonzaHandlers(), - (NonzaHandler handler) async { - if (handler.matches(nonza)) { - handled = true; - await handler.callback(nonza); - } + await Future.forEach(getNonzaHandlers(), (NonzaHandler handler) async { + if (handler.matches(nonza)) { + handled = true; + await handler.callback(nonza); } - ); + }); return handled; } @@ -116,17 +113,25 @@ abstract class XmppManagerBase { /// for plugins to reset their cache in case of a new stream. /// The value only makes sense after receiving a StreamNegotiationsDoneEvent. Future isNewStream() async { - final sm = getAttributes().getManagerById(smManager); + final sm = + getAttributes().getManagerById(smManager); return sm?.streamResumed == false; } - + /// 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 reply(StanzaHandlerData data, String type, List children) async { - assert(data.stanza.tag == 'iq', 'Reply makes little sense for non-IQ stanzas'); + Future reply( + StanzaHandlerData data, + String type, + List children, + ) async { + assert( + data.stanza.tag == 'iq', + 'Reply makes little sense for non-IQ stanzas', + ); final stanza = data.stanza.copyWith( to: data.stanza.from, diff --git a/packages/moxxmpp/lib/src/managers/data.dart b/packages/moxxmpp/lib/src/managers/data.dart index 0a0b466..4bb1cc1 100644 --- a/packages/moxxmpp/lib/src/managers/data.dart +++ b/packages/moxxmpp/lib/src/managers/data.dart @@ -27,50 +27,48 @@ class StanzaHandlerData with _$StanzaHandlerData { dynamic cancelReason, // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely // necessary, e.g. with Message Carbons or OMEMO - Stanza stanza, - { - // Whether the stanza is retransmitted. Only useful in the context of outgoing - // stanza handlers. MUST NOT be overwritten. - @Default(false) bool retransmitted, - StatelessMediaSharingData? sims, - StatelessFileSharingData? sfs, - OOBData? oob, - StableStanzaId? stableId, - ReplyData? reply, - ChatState? chatState, - @Default(false) bool isCarbon, - @Default(false) bool deliveryReceiptRequested, - @Default(false) bool isMarkable, - // File Upload Notifications - // A notification - FileMetadataData? fun, - // The stanza id this replaces - String? funReplacement, - // The stanza id this cancels - String? funCancellation, - // Whether the stanza was received 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 - ExplicitEncryptionType? encryptionType, - // Delayed Delivery - DelayedDelivery? delayedDelivery, - // This is for stanza handlers that are not part of the XMPP library but still need - // pass data around. - @Default({}) Map other, - // If non-null, then it indicates the origin Id of the message that should be - // retracted - MessageRetractionData? messageRetraction, - // If non-null, then the message is a correction for the specified stanza Id - String? lastMessageCorrectionSid, - // Reactions data - MessageReactions? messageReactions, - // The Id of the sticker pack this sticker belongs to - String? stickerPackId, - } - ) = _StanzaHandlerData; + Stanza stanza, { + // Whether the stanza is retransmitted. Only useful in the context of outgoing + // stanza handlers. MUST NOT be overwritten. + @Default(false) bool retransmitted, + StatelessMediaSharingData? sims, + StatelessFileSharingData? sfs, + OOBData? oob, + StableStanzaId? stableId, + ReplyData? reply, + ChatState? chatState, + @Default(false) bool isCarbon, + @Default(false) bool deliveryReceiptRequested, + @Default(false) bool isMarkable, + // File Upload Notifications + // A notification + FileMetadataData? fun, + // The stanza id this replaces + String? funReplacement, + // The stanza id this cancels + String? funCancellation, + // Whether the stanza was received 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 + ExplicitEncryptionType? encryptionType, + // Delayed Delivery + DelayedDelivery? delayedDelivery, + // This is for stanza handlers that are not part of the XMPP library but still need + // pass data around. + @Default({}) Map other, + // If non-null, then it indicates the origin Id of the message that should be + // retracted + MessageRetractionData? messageRetraction, + // If non-null, then the message is a correction for the specified stanza Id + String? lastMessageCorrectionSid, + // Reactions data + MessageReactions? messageReactions, + // The Id of the sticker pack this sticker belongs to + String? stickerPackId, + }) = _StanzaHandlerData; } diff --git a/packages/moxxmpp/lib/src/managers/handlers.dart b/packages/moxxmpp/lib/src/managers/handlers.dart index 892e98e..32eef43 100644 --- a/packages/moxxmpp/lib/src/managers/handlers.dart +++ b/packages/moxxmpp/lib/src/managers/handlers.dart @@ -5,7 +5,7 @@ import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; abstract class Handler { - const Handler(this.matchStanzas, { this.nonzaTag, this.nonzaXmlns }); + const Handler(this.matchStanzas, {this.nonzaTag, this.nonzaXmlns}); final String? nonzaTag; final String? nonzaXmlns; final bool matchStanzas; @@ -19,11 +19,12 @@ abstract class Handler { } if (nonzaXmlns != null && nonzaTag != null) { - matches = (node.attributes['xmlns'] ?? '') == nonzaXmlns! && node.tag == nonzaTag!; + matches = (node.attributes['xmlns'] ?? '') == nonzaXmlns! && + node.tag == nonzaTag!; } - + if (matchStanzas && nonzaTag == null) { - matches = [ 'iq', 'presence', 'message' ].contains(node.tag); + matches = ['iq', 'presence', 'message'].contains(node.tag); } return matches; @@ -32,42 +33,42 @@ abstract class Handler { class NonzaHandler extends Handler { NonzaHandler({ - required this.callback, - String? nonzaTag, - String? nonzaXmlns, + required this.callback, + String? nonzaTag, + String? nonzaXmlns, }) : super( - false, - nonzaTag: nonzaTag, - nonzaXmlns: nonzaXmlns, - ); + false, + nonzaTag: nonzaTag, + nonzaXmlns: nonzaXmlns, + ); final Future Function(XMLNode) callback; } class StanzaHandler extends Handler { StanzaHandler({ - required this.callback, - this.tagXmlns, - this.tagName, - this.priority = 0, - String? stanzaTag, + required this.callback, + this.tagXmlns, + this.tagName, + this.priority = 0, + String? stanzaTag, }) : super( - true, - nonzaTag: stanzaTag, - nonzaXmlns: stanzaXmlns, - ); + true, + nonzaTag: stanzaTag, + nonzaXmlns: stanzaXmlns, + ); final String? tagName; final String? tagXmlns; final int priority; final Future Function(Stanza, StanzaHandlerData) callback; - + @override bool matches(XMLNode node) { var matches = super.matches(node); - + if (matches == false) { return false; } - + if (tagName != null) { final firstTag = node.firstTag(tagName!, xmlns: tagXmlns); @@ -75,16 +76,19 @@ class StanzaHandler extends Handler { } else if (tagXmlns != null) { return listContains( node.children, - (XMLNode node_) => node_.attributes.containsKey('xmlns') && node_.attributes['xmlns'] == tagXmlns, + (XMLNode node_) => + node_.attributes.containsKey('xmlns') && + node_.attributes['xmlns'] == tagXmlns, ); } if (tagName == null && tagXmlns == null) { matches = true; } - + return matches; } } -int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => b.priority.compareTo(a.priority); +int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => + b.priority.compareTo(a.priority); diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index 2a4ff16..03e40e0 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -10,7 +10,8 @@ const pubsubManager = 'org.moxxmpp.pubsubmanager'; const userAvatarManager = 'org.moxxmpp.useravatarmanager'; const stableIdManager = 'org.moxxmpp.stableidmanager'; const simsManager = 'org.moxxmpp.simsmanager'; -const messageDeliveryReceiptManager = 'org.moxxmpp.messagedeliveryreceiptmanager'; +const messageDeliveryReceiptManager = + 'org.moxxmpp.messagedeliveryreceiptmanager'; const chatMarkerManager = 'org.moxxmpp.chatmarkermanager'; const oobManager = 'org.moxxmpp.oobmanager'; const sfsManager = 'org.moxxmpp.sfsmanager'; @@ -19,7 +20,8 @@ const blockingManager = 'org.moxxmpp.blockingmanager'; const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager'; const chatStateManager = 'org.moxxmpp.chatstatemanager'; const pingManager = 'org.moxxmpp.ping'; -const fileUploadNotificationManager = 'org.moxxmpp.fileuploadnotificationmanager'; +const fileUploadNotificationManager = + 'org.moxxmpp.fileuploadnotificationmanager'; const omemoManager = 'org.moxxmpp.omemomanager'; const emeManager = 'org.moxxmpp.ememanager'; const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager'; diff --git a/packages/moxxmpp/lib/src/managers/priorities.dart b/packages/moxxmpp/lib/src/managers/priorities.dart index e69de29..8b13789 100644 --- a/packages/moxxmpp/lib/src/managers/priorities.dart +++ b/packages/moxxmpp/lib/src/managers/priorities.dart @@ -0,0 +1 @@ + diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index e4ec0c8..075ece0 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -80,54 +80,58 @@ class MessageManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - callback: _onMessage, - priority: -100, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + callback: _onMessage, + priority: -100, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza _, StanzaHandlerData state) async { + + Future _onMessage( + Stanza _, + StanzaHandlerData state, + ) async { final message = state.stanza; final body = message.firstTag('body'); final hints = List.empty(growable: true); - for (final element in message.findTagsByXmlns(messageProcessingHintsXmlns)) { - hints.add(messageProcessingHintFromXml(element)); + for (final element + in message.findTagsByXmlns(messageProcessingHintsXmlns)) { + hints.add(messageProcessingHintFromXml(element)); } - - getAttributes().sendEvent(MessageEvent( - body: body != null ? body.innerText() : '', - fromJid: JID.fromString(message.attributes['from']! as String), - toJid: JID.fromString(message.attributes['to']! as String), - sid: message.attributes['id']! as String, - stanzaId: state.stableId ?? const StableStanzaId(), - isCarbon: state.isCarbon, - deliveryReceiptRequested: state.deliveryReceiptRequested, - isMarkable: state.isMarkable, - type: message.attributes['type'] as String?, - oob: state.oob, - sfs: state.sfs, - sims: state.sims, - reply: state.reply, - chatState: state.chatState, - fun: state.fun, - funReplacement: state.funReplacement, - funCancellation: state.funCancellation, - encrypted: state.encrypted, - messageRetraction: state.messageRetraction, - messageCorrectionId: state.lastMessageCorrectionSid, - messageReactions: state.messageReactions, - messageProcessingHints: hints.isEmpty ? - null : - hints, - stickerPackId: state.stickerPackId, - other: state.other, - error: StanzaError.fromStanza(message), - ),); + + getAttributes().sendEvent( + MessageEvent( + body: body != null ? body.innerText() : '', + fromJid: JID.fromString(message.attributes['from']! as String), + toJid: JID.fromString(message.attributes['to']! as String), + sid: message.attributes['id']! as String, + stanzaId: state.stableId ?? const StableStanzaId(), + isCarbon: state.isCarbon, + deliveryReceiptRequested: state.deliveryReceiptRequested, + isMarkable: state.isMarkable, + type: message.attributes['type'] as String?, + oob: state.oob, + sfs: state.sfs, + sims: state.sims, + reply: state.reply, + chatState: state.chatState, + fun: state.fun, + funReplacement: state.funReplacement, + funCancellation: state.funCancellation, + encrypted: state.encrypted, + messageRetraction: state.messageRetraction, + messageCorrectionId: state.lastMessageCorrectionSid, + messageReactions: state.messageReactions, + messageProcessingHints: hints.isEmpty ? null : hints, + stickerPackId: state.stickerPackId, + other: state.other, + error: StanzaError.fromStanza(message), + ), + ); return state.copyWith(done: true); } @@ -139,7 +143,10 @@ class MessageManager extends XmppManagerBase { /// child in the message stanza and set its id to originId. void sendMessage(MessageDetails details) { assert( - implies(details.quoteBody != null, details.quoteFrom != null && details.quoteId != null), + implies( + details.quoteBody != null, + details.quoteFrom != null && details.quoteId != null, + ), 'When quoting a message, then quoteFrom and quoteId must also be non-null', ); @@ -152,7 +159,7 @@ class MessageManager extends XmppManagerBase { if (details.quoteBody != null) { final quote = QuoteData.fromBodies(details.quoteBody!, details.body!); - + stanza ..addChild( XMLNode(tag: 'body', text: quote.body), @@ -161,19 +168,14 @@ class MessageManager extends XmppManagerBase { XMLNode.xmlns( tag: 'reply', xmlns: replyXmlns, - attributes: { - 'to': details.quoteFrom!, - 'id': details.quoteId! - }, + attributes: {'to': details.quoteFrom!, 'id': details.quoteId!}, ), ) ..addChild( XMLNode.xmlns( tag: 'fallback', xmlns: fallbackXmlns, - attributes: { - 'for': replyXmlns - }, + attributes: {'for': replyXmlns}, children: [ XMLNode( tag: 'body', @@ -220,16 +222,20 @@ class MessageManager extends XmppManagerBase { stanza.addChild(details.sfs!.toXML()); final source = details.sfs!.sources.first; - if (source is StatelessFileSharingUrlSource && details.setOOBFallbackBody) { + if (source is StatelessFileSharingUrlSource && + details.setOOBFallbackBody) { // SFS recommends OOB as a fallback stanza.addChild(constructOOBNode(OOBData(url: source.url))); } } - + if (details.chatState != null) { stanza.addChild( // TODO(Unknown): Move this into xep_0085.dart - XMLNode.xmlns(tag: chatStateToString(details.chatState!), xmlns: chatStateXmlns), + XMLNode.xmlns( + tag: chatStateToString(details.chatState!), + xmlns: chatStateXmlns, + ), ); } @@ -295,7 +301,7 @@ class MessageManager extends XmppManagerBase { if (details.messageReactions != null) { stanza.addChild(details.messageReactions!.toXml()); } - + if (details.messageProcessingHints != null) { for (final hint in details.messageProcessingHints!) { stanza.addChild(hint.toXml()); @@ -313,7 +319,7 @@ class MessageManager extends XmppManagerBase { ), ); } - + getAttributes().sendStanza(stanza, awaitable: false); } } diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index 08de65e..d49330b 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -28,9 +28,11 @@ const vCardTempUpdate = 'vcard-temp:x:update'; const pubsubXmlns = 'http://jabber.org/protocol/pubsub'; const pubsubEventXmlns = 'http://jabber.org/protocol/pubsub#event'; const pubsubOwnerXmlns = 'http://jabber.org/protocol/pubsub#owner'; -const pubsubPublishOptionsXmlns = 'http://jabber.org/protocol/pubsub#publish-options'; +const pubsubPublishOptionsXmlns = + 'http://jabber.org/protocol/pubsub#publish-options'; const pubsubNodeConfigMax = 'http://jabber.org/protocol/pubsub#config-node-max'; -const pubsubNodeConfigMultiItems = 'http://jabber.org/protocol/pubsub#multi-items'; +const pubsubNodeConfigMultiItems = + 'http://jabber.org/protocol/pubsub#multi-items'; // XEP-0066 const oobDataXmlns = 'jabber:x:oob'; @@ -137,8 +139,10 @@ const sfsXmlns = 'urn:xmpp:sfs:0'; // XEP-0448 const sfsEncryptionXmlns = 'urn:xmpp:esfs:0'; -const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0'; -const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; +const sfsEncryptionAes128GcmNoPaddingXmlns = + 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0'; +const sfsEncryptionAes256GcmNoPaddingXmlns = + 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'; // XEP-0449 diff --git a/packages/moxxmpp/lib/src/negotiators/manager.dart b/packages/moxxmpp/lib/src/negotiators/manager.dart index e69de29..8b13789 100644 --- a/packages/moxxmpp/lib/src/negotiators/manager.dart +++ b/packages/moxxmpp/lib/src/negotiators/manager.dart @@ -0,0 +1 @@ + diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 57ba230..3594403 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -35,28 +35,41 @@ class NegotiatorAttributes { this.getSocket, this.isAuthenticated, ); + /// Sends the nonza nonza and optionally redacts it in logs if redact is not null. final void Function(XMLNode nonza, {String? redact}) sendNonza; + /// Returns the connection settings. final ConnectionSettings Function() getConnectionSettings; + /// Send an event event to the connection's event bus final Future Function(XmppEvent event) sendEvent; + /// Returns the negotiator with id id of the connection or null. - final T? Function(String) getNegotiatorById; + final T? Function(String) + getNegotiatorById; + /// Returns the manager with id id of the connection or null. final T? Function(String) getManagerById; + /// Returns the full JID of the current account final JID Function() getFullJID; + /// Returns the socket the negotiator is attached to final BaseSocketWrapper Function() getSocket; + /// Returns true if the stream is authenticated. Returns false if not. final bool Function() isAuthenticated; } abstract class XmppFeatureNegotiatorBase { + XmppFeatureNegotiatorBase( + this.priority, + this.sendStreamHeaderWhenDone, + this.negotiatingXmlns, + this.id, + ) : state = NegotiatorState.ready; - XmppFeatureNegotiatorBase(this.priority, this.sendStreamHeaderWhenDone, this.negotiatingXmlns, this.id) - : state = NegotiatorState.ready; /// The priority regarding other negotiators. The higher, the earlier will the /// negotiator be used final int priority; @@ -70,24 +83,25 @@ abstract class XmppFeatureNegotiatorBase { /// The Id of the negotiator final String id; - + /// The state the negotiator is currently in NegotiatorState state; - + late NegotiatorAttributes _attributes; /// Register the negotiator against a connection class by means of [attributes]. void register(NegotiatorAttributes attributes) { _attributes = attributes; } - + /// Returns true if a feature in [features], which are the children of the /// nonza, can be negotiated. Otherwise, returns false. bool matchesFeature(List features) { return firstWhereOrNull( - features, - (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, - ) != null; + features, + (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, + ) != + null; } /// Called with the currently received nonza [nonza] when the negotiator is active. @@ -105,6 +119,6 @@ abstract class XmppFeatureNegotiatorBase { void reset() { state = NegotiatorState.ready; } - + NegotiatorAttributes get attributes => _attributes; } diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart index ecc177a..e60eee0 100644 --- a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart +++ b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart @@ -16,7 +16,8 @@ class ResourceBindingFailedError extends NegotiatorError { /// A negotiator that implements resource binding against a random server-provided /// resource. class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { - ResourceBindingNegotiator() : super(0, false, bindXmlns, resourceBindingNegotiator); + ResourceBindingNegotiator() + : super(0, false, bindXmlns, resourceBindingNegotiator); /// Flag indicating the state of the negotiator: /// - True: We sent a binding request @@ -27,14 +28,18 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { bool matchesFeature(List features) { final sm = attributes.getManagerById(smManager); if (sm != null) { - return super.matchesFeature(features) && !sm.streamResumed && attributes.isAuthenticated(); + return super.matchesFeature(features) && + !sm.streamResumed && + attributes.isAuthenticated(); } return super.matchesFeature(features) && attributes.isAuthenticated(); } - + @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { if (!_requestSent) { final stanza = XMLNode.xmlns( tag: 'iq', @@ -63,11 +68,12 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { final jid = bind.firstTag('jid')!; final resource = jid.innerText().split('/')[1]; - await attributes.sendEvent(ResourceBindingSuccessEvent(resource: resource)); + await attributes + .sendEvent(ResourceBindingSuccessEvent(resource: resource)); return const Result(NegotiatorState.done); } } - + @override void reset() { _requestSent = false; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart b/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart index 1ffd652..e4b2176 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart @@ -12,9 +12,12 @@ abstract class SaslError extends NegotiatorError { } switch (error?.tag) { - case 'credentials-expired': return SaslCredentialsExpiredError(); - case 'not-authorized': return SaslNotAuthorizedError(); - case 'account-disabled': return SaslAccountDisabledError(); + case 'credentials-expired': + return SaslCredentialsExpiredError(); + case 'not-authorized': + return SaslNotAuthorizedError(); + case 'account-disabled': + return SaslAccountDisabledError(); } return SaslUnspecifiedError(); diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/kv.dart b/packages/moxxmpp/lib/src/negotiators/sasl/kv.dart index 1636760..7f6eab3 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/kv.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/kv.dart @@ -1,7 +1,4 @@ -enum ParserState { - variableName, - variableValue -} +enum ParserState { variableName, variableValue } /// Parse a string like "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" into /// { "n": "user", "r": "fyko+d2lbbFgONRv9qkxdawL"}. @@ -14,31 +11,33 @@ Map parseKeyValue(String keyValueString) { for (var i = 0; i < keyValueString.length; i++) { final char = keyValueString[i]; switch (state) { - case ParserState.variableName: { - if (char == '=') { - state = ParserState.variableValue; - } else if (char == ',') { - name = ''; - } else { - name += char; + case ParserState.variableName: + { + if (char == '=') { + state = ParserState.variableValue; + } else if (char == ',') { + name = ''; + } else { + name += char; + } } - } - break; - case ParserState.variableValue: { - if (char == ',' || i == keyValueString.length - 1) { - if (char != ',') { + break; + case ParserState.variableValue: + { + if (char == ',' || i == keyValueString.length - 1) { + if (char != ',') { + value += char; + } + + values[name] = value; + value = ''; + name = ''; + state = ParserState.variableName; + } else { value += char; } - - values[name] = value; - value = ''; - name = ''; - state = ParserState.variableName; - } else { - value += char; } - } - break; + break; } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart index ebb1a99..e0cef18 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart @@ -4,11 +4,12 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; abstract class SaslNegotiator extends XmppFeatureNegotiatorBase { + SaslNegotiator(int priority, String id, this.mechanismName) + : super(priority, true, saslXmlns, id); - SaslNegotiator(int priority, String id, this.mechanismName) : super(priority, true, saslXmlns, id); /// The name inside the element final String mechanismName; - + @override bool matchesFeature(List features) { // Is SASL advertised? @@ -20,8 +21,9 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase { // Is SASL PLAIN advertised? return firstWhereOrNull( - mechanisms.children, - (XMLNode mechanism) => mechanism.text == mechanismName, - ) != null; + mechanisms.children, + (XMLNode mechanism) => mechanism.text == mechanismName, + ) != + null; } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart b/packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart index 0010ca2..1b76aa1 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart @@ -2,12 +2,13 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; class SaslAuthNonza extends XMLNode { - SaslAuthNonza(String mechanism, String body) : super( - tag: 'auth', - attributes: { - 'xmlns': saslXmlns, - 'mechanism': mechanism , - }, - text: body, - ); + SaslAuthNonza(String mechanism, String body) + : super( + tag: 'auth', + attributes: { + 'xmlns': saslXmlns, + 'mechanism': mechanism, + }, + text: body, + ); } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 9e29daa..dc56728 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -10,16 +10,18 @@ import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; class SaslPlainAuthNonza extends SaslAuthNonza { - SaslPlainAuthNonza(String username, String password) : super( - 'PLAIN', base64.encode(utf8.encode('\u0000$username\u0000$password')), - ); + SaslPlainAuthNonza(String username, String password) + : super( + 'PLAIN', + base64.encode(utf8.encode('\u0000$username\u0000$password')), + ); } class SaslPlainNegotiator extends SaslNegotiator { SaslPlainNegotiator() - : _authSent = false, - _log = Logger('SaslPlainNegotiator'), - super(0, saslPlainNegotiator, 'PLAIN'); + : _authSent = false, + _log = Logger('SaslPlainNegotiator'), + super(0, saslPlainNegotiator, 'PLAIN'); bool _authSent; final Logger _log; @@ -27,10 +29,12 @@ class SaslPlainNegotiator extends SaslNegotiator { @override bool matchesFeature(List features) { if (!attributes.getConnectionSettings().allowPlainAuth) return false; - + if (super.matchesFeature(features)) { if (!attributes.getSocket().isSecure()) { - _log.warning('Refusing to match SASL feature due to unsecured connection'); + _log.warning( + 'Refusing to match SASL feature due to unsecured connection', + ); return false; } @@ -41,7 +45,9 @@ class SaslPlainNegotiator extends SaslNegotiator { } @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { if (!_authSent) { final settings = attributes.getConnectionSettings(); attributes.sendNonza( diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index baafec7..78d64c8 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -17,28 +17,30 @@ import 'package:saslprep/saslprep.dart'; // NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart -enum ScramHashType { - sha1, - sha256, - sha512 -} +enum ScramHashType { sha1, sha256, sha512 } HashAlgorithm hashFromType(ScramHashType type) { switch (type) { - case ScramHashType.sha1: return Sha1(); - case ScramHashType.sha256: return Sha256(); - case ScramHashType.sha512: return Sha512(); + case ScramHashType.sha1: + return Sha1(); + case ScramHashType.sha256: + return Sha256(); + case ScramHashType.sha512: + return Sha512(); } } int pbkdfBitsFromHash(ScramHashType type) { switch (type) { // NOTE: SHA1 is 20 octets long => 20 octets * 8 bits/octet - case ScramHashType.sha1: return 160; + case ScramHashType.sha1: + return 160; // NOTE: SHA256 is 32 octets long => 32 octets * 8 bits/octet - case ScramHashType.sha256: return 256; + case ScramHashType.sha256: + return 256; // NOTE: SHA512 is 64 octets long => 64 octets * 8 bits/octet - case ScramHashType.sha512: return 512; + case ScramHashType.sha512: + return 512; } } @@ -48,44 +50,48 @@ const scramSha512Mechanism = 'SCRAM-SHA-512'; String mechanismNameFromType(ScramHashType type) { switch (type) { - case ScramHashType.sha1: return scramSha1Mechanism; - case ScramHashType.sha256: return scramSha256Mechanism; - case ScramHashType.sha512: return scramSha512Mechanism; + case ScramHashType.sha1: + return scramSha1Mechanism; + case ScramHashType.sha256: + return scramSha256Mechanism; + case ScramHashType.sha512: + return scramSha512Mechanism; } } String namespaceFromType(ScramHashType type) { switch (type) { - case ScramHashType.sha1: return saslScramSha1Negotiator; - case ScramHashType.sha256: return saslScramSha256Negotiator; - case ScramHashType.sha512: return saslScramSha512Negotiator; + case ScramHashType.sha1: + return saslScramSha1Negotiator; + case ScramHashType.sha256: + return saslScramSha256Negotiator; + case ScramHashType.sha512: + return saslScramSha512Negotiator; } } class SaslScramAuthNonza extends SaslAuthNonza { // This subclassing makes less sense here, but this is since the auth nonza here // requires knowledge of the inner state of the Negotiator. - SaslScramAuthNonza({ required ScramHashType type, required String body }) : super( - mechanismNameFromType(type), body, - ); + SaslScramAuthNonza({required ScramHashType type, required String body}) + : super( + mechanismNameFromType(type), + body, + ); } class SaslScramResponseNonza extends XMLNode { - SaslScramResponseNonza({ required String body }) : super( - tag: 'response', - attributes: { - 'xmlns': saslXmlns, - }, - text: body, - ); + SaslScramResponseNonza({required String body}) + : super( + tag: 'response', + attributes: { + 'xmlns': saslXmlns, + }, + text: body, + ); } -enum ScramState { - preSent, - initialMessageSent, - challengeResponseSent, - error -} +enum ScramState { preSent, initialMessageSent, challengeResponseSent, error } const gs2Header = 'n,,'; @@ -96,12 +102,16 @@ class SaslScramNegotiator extends SaslNegotiator { this.initialMessageNoGS2, this.clientNonce, this.hashType, - ) : - _hash = hashFromType(hashType), - _serverSignature = '', - _scramState = ScramState.preSent, - _log = Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'), - super(priority, namespaceFromType(hashType), mechanismNameFromType(hashType)); + ) : _hash = hashFromType(hashType), + _serverSignature = '', + _scramState = ScramState.preSent, + _log = + Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'), + super( + priority, + namespaceFromType(hashType), + mechanismNameFromType(hashType), + ); String? clientNonce; String initialMessageNoGS2; final ScramHashType hashType; @@ -122,7 +132,9 @@ class SaslScramNegotiator extends SaslNegotiator { final saltedPasswordRaw = await pbkdf2.deriveKey( secretKey: SecretKey( - utf8.encode(Saslprep.saslprep(attributes.getConnectionSettings().password)), + utf8.encode( + Saslprep.saslprep(attributes.getConnectionSettings().password), + ), ), nonce: base64.decode(salt), ); @@ -131,32 +143,46 @@ class SaslScramNegotiator extends SaslNegotiator { Future> calculateClientKey(List saltedPassword) async { return (await Hmac(_hash).calculateMac( - utf8.encode('Client Key'), secretKey: SecretKey(saltedPassword), - )).bytes; + utf8.encode('Client Key'), + secretKey: SecretKey(saltedPassword), + )) + .bytes; } - Future> calculateClientSignature(String authMessage, List storedKey) async { + Future> calculateClientSignature( + String authMessage, + List storedKey, + ) async { return (await Hmac(_hash).calculateMac( - utf8.encode(authMessage), - secretKey: SecretKey(storedKey), - )).bytes; + utf8.encode(authMessage), + secretKey: SecretKey(storedKey), + )) + .bytes; } Future> calculateServerKey(List saltedPassword) async { return (await Hmac(_hash).calculateMac( - utf8.encode('Server Key'), - secretKey: SecretKey(saltedPassword), - )).bytes; + utf8.encode('Server Key'), + secretKey: SecretKey(saltedPassword), + )) + .bytes; } - Future> calculateServerSignature(String authMessage, List serverKey) async { + Future> calculateServerSignature( + String authMessage, + List serverKey, + ) async { return (await Hmac(_hash).calculateMac( - utf8.encode(authMessage), - secretKey: SecretKey(serverKey), - )).bytes; + utf8.encode(authMessage), + secretKey: SecretKey(serverKey), + )) + .bytes; } - List calculateClientProof(List clientKey, List clientSignature) { + List calculateClientProof( + List clientKey, + List clientSignature, + ) { final clientProof = List.filled(clientKey.length, 0); for (var i = 0; i < clientKey.length; i++) { clientProof[i] = clientKey[i] ^ clientSignature[i]; @@ -164,20 +190,26 @@ class SaslScramNegotiator extends SaslNegotiator { return clientProof; } - + Future calculateChallengeResponse(String base64Challenge) async { final challengeString = utf8.decode(base64.decode(base64Challenge)); final challenge = parseKeyValue(challengeString); final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}'; - - final saltedPassword = await calculateSaltedPassword(challenge['s']!, int.parse(challenge['i']!)); + + final saltedPassword = await calculateSaltedPassword( + challenge['s']!, + int.parse(challenge['i']!), + ); final clientKey = await calculateClientKey(saltedPassword); final storedKey = (await _hash.hash(clientKey)).bytes; - final authMessage = '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare'; - final clientSignature = await calculateClientSignature(authMessage, storedKey); + final authMessage = + '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare'; + final clientSignature = + await calculateClientSignature(authMessage, storedKey); final clientProof = calculateClientProof(clientKey, clientSignature); final serverKey = await calculateServerKey(saltedPassword); - _serverSignature = base64.encode(await calculateServerSignature(authMessage, serverKey)); + _serverSignature = + base64.encode(await calculateServerSignature(authMessage, serverKey)); return '$clientFinalMessageBare,p=${base64.encode(clientProof)}'; } @@ -186,7 +218,9 @@ class SaslScramNegotiator extends SaslNegotiator { bool matchesFeature(List features) { if (super.matchesFeature(features)) { if (!attributes.getSocket().isSecure()) { - _log.warning('Refusing to match SASL feature due to unsecured connection'); + _log.warning( + 'Refusing to match SASL feature due to unsecured connection', + ); return false; } @@ -195,20 +229,29 @@ class SaslScramNegotiator extends SaslNegotiator { return false; } - + @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { switch (_scramState) { case ScramState.preSent: if (clientNonce == null || clientNonce == '') { - clientNonce = randomAlphaNumeric(40, provider: CoreRandomProvider.from(Random.secure())); + clientNonce = randomAlphaNumeric( + 40, + provider: CoreRandomProvider.from(Random.secure()), + ); } - - initialMessageNoGS2 = 'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce'; + + initialMessageNoGS2 = + 'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce'; _scramState = ScramState.initialMessageSent; attributes.sendNonza( - SaslScramAuthNonza(body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), type: hashType), + SaslScramAuthNonza( + body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), + type: hashType, + ), redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(), ); return const Result(NegotiatorState.ready); @@ -244,7 +287,8 @@ class SaslScramNegotiator extends SaslNegotiator { } // NOTE: This assumes that the string is always "v=..." and contains no other parameters - final signature = parseKeyValue(utf8.decode(base64.decode(nonza.innerText()))); + final signature = + parseKeyValue(utf8.decode(base64.decode(nonza.innerText()))); if (signature['v']! != _serverSignature) { // TODO(Unknown): Notify of a signature mismatch //final error = nonza.children.first.tag; diff --git a/packages/moxxmpp/lib/src/negotiators/starttls.dart b/packages/moxxmpp/lib/src/negotiators/starttls.dart index 5f8735a..b8e07d1 100644 --- a/packages/moxxmpp/lib/src/negotiators/starttls.dart +++ b/packages/moxxmpp/lib/src/negotiators/starttls.dart @@ -5,10 +5,7 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; -enum _StartTlsState { - ready, - requested -} +enum _StartTlsState { ready, requested } class StartTLSFailedError extends NegotiatorError { @override @@ -16,10 +13,11 @@ class StartTLSFailedError extends NegotiatorError { } class StartTLSNonza extends XMLNode { - StartTLSNonza() : super.xmlns( - tag: 'starttls', - xmlns: startTlsXmlns, - ); + StartTLSNonza() + : super.xmlns( + tag: 'starttls', + xmlns: startTlsXmlns, + ); } /// A negotiator implementing StartTLS. @@ -33,7 +31,9 @@ class StartTlsNegotiator extends XmppFeatureNegotiatorBase { final Logger _log = Logger('StartTlsNegotiator'); @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { switch (_state) { case _StartTlsState.ready: _log.fine('StartTLS is available. Performing StartTLS upgrade...'); @@ -41,14 +41,16 @@ class StartTlsNegotiator extends XmppFeatureNegotiatorBase { attributes.sendNonza(StartTLSNonza()); return const Result(NegotiatorState.ready); case _StartTlsState.requested: - if (nonza.tag != 'proceed' || nonza.attributes['xmlns'] != startTlsXmlns) { + if (nonza.tag != 'proceed' || + nonza.attributes['xmlns'] != startTlsXmlns) { _log.severe('Failed to perform StartTLS negotiation'); return Result(StartTLSFailedError()); } _log.fine('Securing socket'); - final result = await attributes.getSocket() - .secure(attributes.getConnectionSettings().jid.domain); + final result = await attributes + .getSocket() + .secure(attributes.getConnectionSettings().jid.domain); if (!result) { _log.severe('Failed to secure stream'); return Result(StartTLSFailedError()); diff --git a/packages/moxxmpp/lib/src/ping.dart b/packages/moxxmpp/lib/src/ping.dart index edc103f..422ae8c 100644 --- a/packages/moxxmpp/lib/src/ping.dart +++ b/packages/moxxmpp/lib/src/ping.dart @@ -8,11 +8,13 @@ class PingManager extends XmppManagerBase { @override Future isSupported() async => true; - + void _logWarning() { - logger.warning('Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.'); + logger.warning( + 'Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.', + ); } - + @override Future onXmppEvent(XmppEvent event) async { if (event is SendPingEvent) { @@ -24,14 +26,18 @@ class PingManager extends XmppManagerBase { logger.finest('Not sending ping as the socket manages it.'); return; } - - final stream = attrs.getManagerById(smManager) as StreamManagementManager?; + + final stream = + attrs.getManagerById(smManager) as StreamManagementManager?; if (stream != null) { - if (stream.isStreamManagementEnabled() /*&& stream.getUnackedStanzaCount() > 0*/) { + if (stream + .isStreamManagementEnabled() /*&& stream.getUnackedStanzaCount() > 0*/) { logger.finest('Sending an ack ping as Stream Management is enabled'); stream.sendAckRequestPing(); } else if (attrs.getSocket().whitespacePingAllowed()) { - logger.finest('Sending a whitespace ping as Stream Management is not enabled'); + logger.finest( + 'Sending a whitespace ping as Stream Management is not enabled', + ); attrs.getConnection().sendWhitespacePing(); } else { _logWarning(); diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index d878f0e..a461a85 100644 --- a/packages/moxxmpp/lib/src/presence.dart +++ b/packages/moxxmpp/lib/src/presence.dart @@ -20,18 +20,19 @@ class PresenceManager extends XmppManagerBase { PresenceManager() : super(presenceManager); /// The list of pre-send callbacks. - final List _presenceCallbacks = List.empty(growable: true); + final List _presenceCallbacks = + List.empty(growable: true); @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'presence', - callback: _onPresence, - ) - ]; + StanzaHandler( + stanzaTag: 'presence', + callback: _onPresence, + ) + ]; @override - List getDiscoFeatures() => [ capsXmlns ]; + List getDiscoFeatures() => [capsXmlns]; @override Future isSupported() async => true; @@ -40,26 +41,35 @@ class PresenceManager extends XmppManagerBase { void registerPreSendCallback(PresencePreSendCallback callback) { _presenceCallbacks.add(callback); } - - Future _onPresence(Stanza presence, StanzaHandlerData state) async { + + Future _onPresence( + Stanza presence, + StanzaHandlerData state, + ) async { final attrs = getAttributes(); switch (presence.type) { case 'subscribe': - case 'subscribed': { - attrs.sendEvent( - SubscriptionRequestReceivedEvent(from: JID.fromString(presence.from!)), - ); - return state.copyWith(done: true); - } - default: break; + case 'subscribed': + { + attrs.sendEvent( + SubscriptionRequestReceivedEvent( + from: JID.fromString(presence.from!), + ), + ); + return state.copyWith(done: true); + } + default: + break; } if (presence.from != null) { logger.finest("Received presence from '${presence.from}'"); - getAttributes().sendEvent(PresenceReceivedEvent(JID.fromString(presence.from!), presence)); + getAttributes().sendEvent( + PresenceReceivedEvent(JID.fromString(presence.from!), presence), + ); return state.copyWith(done: true); - } + } return state; } @@ -97,7 +107,7 @@ class PresenceManager extends XmppManagerBase { addFrom: StanzaFromType.full, ); } - + /// Sends a subscription request to [to]. void sendSubscriptionRequest(String to) { getAttributes().sendStanza( diff --git a/packages/moxxmpp/lib/src/reconnect.dart b/packages/moxxmpp/lib/src/reconnect.dart index 895d6df..9782f8b 100644 --- a/packages/moxxmpp/lib/src/reconnect.dart +++ b/packages/moxxmpp/lib/src/reconnect.dart @@ -35,15 +35,18 @@ abstract class ReconnectionPolicy { /// The lock for accessing [_shouldAttemptReconnection] @protected final Lock shouldReconnectLock = Lock(); - + /// Called by XmppConnection to register the policy. - void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) { + void register( + PerformReconnectFunction performReconnect, + ConnectionLostCallback triggerConnectionLost, + ) { this.performReconnect = performReconnect; this.triggerConnectionLost = triggerConnectionLost; unawaited(reset()); } - + /// In case the policy depends on some internal state, this state must be reset /// to an initial state when reset is called. In case timers run, they must be /// terminated. @@ -61,7 +64,8 @@ abstract class ReconnectionPolicy { /// Set whether a reconnection attempt should be made. Future setShouldReconnect(bool value) async { - return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value); + return shouldReconnectLock + .synchronized(() => _shouldAttemptReconnection = value); } /// Returns true if the manager is currently triggering a reconnection. If not, returns @@ -77,7 +81,6 @@ abstract class ReconnectionPolicy { isReconnecting = value; }); } - } /// A simple reconnection strategy: Make the reconnection delays exponentially longer @@ -87,8 +90,11 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { RandomBackoffReconnectionPolicy( this._minBackoffTime, this._maxBackoffTime, - ) : assert(_minBackoffTime < _maxBackoffTime, '_minBackoffTime must be smaller than _maxBackoffTime'), - super(); + ) : assert( + _minBackoffTime < _maxBackoffTime, + '_minBackoffTime must be smaller than _maxBackoffTime', + ), + super(); /// The maximum time in seconds that a backoff should be. final int _maxBackoffTime; @@ -113,12 +119,16 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { await lock.synchronized(() async { _log.fine('Lock aquired'); if (!(await getShouldReconnect())) { - _log.fine('Backoff timer expired but getShouldReconnect() returned false'); + _log.fine( + 'Backoff timer expired but getShouldReconnect() returned false', + ); return; } if (isReconnecting) { - _log.fine('Backoff timer expired but a reconnection is running, so doing nothing.'); + _log.fine( + 'Backoff timer expired but a reconnection is running, so doing nothing.', + ); return; } @@ -143,7 +153,7 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { await setIsReconnecting(false); } - + @override Future reset() async { // ignore: unnecessary_lambdas @@ -155,17 +165,20 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { return _timer == null; }); if (!shouldContinue) { - _log.finest('_onFailure: Not backing off since _timer is already running'); + _log.finest( + '_onFailure: Not backing off since _timer is already running', + ); return; } - final seconds = Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime; + final seconds = + Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime; _log.finest('Failure occured. Starting random backoff with ${seconds}s'); _timer?.cancel(); _timer = Timer(Duration(seconds: seconds), _onTimerElapsed); } - + @override Future onFailure() async { // ignore: unnecessary_lambdas @@ -199,7 +212,7 @@ class TestingReconnectionPolicy extends ReconnectionPolicy { class TestingSleepReconnectionPolicy extends ReconnectionPolicy { TestingSleepReconnectionPolicy(this._sleepAmount) : super(); final int _sleepAmount; - + @override Future onSuccess() async {} diff --git a/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart b/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart index 1235c1e..23473ec 100644 --- a/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart @@ -21,7 +21,7 @@ int ioctetSortComparator(String a, String b) { if (a.codeUnitAt(0) < b.codeUnitAt(0)) { return -1; } - + return 1; } @@ -46,6 +46,6 @@ int ioctetSortComparatorRaw(List a, List b) { if (a[0] < b[0]) { return -1; } - + return 1; } diff --git a/packages/moxxmpp/lib/src/roster/roster.dart b/packages/moxxmpp/lib/src/roster/roster.dart index 0313c08..d8c3f9d 100644 --- a/packages/moxxmpp/lib/src/roster/roster.dart +++ b/packages/moxxmpp/lib/src/roster/roster.dart @@ -18,7 +18,13 @@ import 'package:moxxmpp/src/types/result.dart'; @immutable class XmppRosterItem { - const XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] }); + const XmppRosterItem({ + required this.jid, + required this.subscription, + this.ask, + this.name, + this.groups = const [], + }); final String jid; final String? name; final String subscription; @@ -26,34 +32,35 @@ class XmppRosterItem { final List groups; @override - bool operator==(Object other) { + bool operator ==(Object other) { return other is XmppRosterItem && - other.jid == jid && - other.name == name && - other.subscription == subscription && - other.ask == ask && - const ListEquality().equals(other.groups, groups); + other.jid == jid && + other.name == name && + other.subscription == subscription && + other.ask == ask && + const ListEquality().equals(other.groups, groups); } @override - int get hashCode => jid.hashCode ^ name.hashCode ^ subscription.hashCode ^ ask.hashCode ^ groups.hashCode; - + int get hashCode => + jid.hashCode ^ + name.hashCode ^ + subscription.hashCode ^ + ask.hashCode ^ + groups.hashCode; + @override String toString() { return 'XmppRosterItem(' - 'jid: $jid, ' - 'name: $name, ' - 'subscription: $subscription, ' - 'ask: $ask, ' - 'groups: $groups)'; + 'jid: $jid, ' + 'name: $name, ' + 'subscription: $subscription, ' + 'ask: $ask, ' + 'groups: $groups)'; } } -enum RosterRemovalResult { - okay, - error, - itemNotFound -} +enum RosterRemovalResult { okay, error, itemNotFound } class RosterRequestResult { RosterRequestResult(this.items, this.ver); @@ -69,14 +76,18 @@ class RosterPushResult { /// A Stub feature negotiator for finding out whether roster versioning is supported. class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase { - RosterFeatureNegotiator() : _supported = false, super(11, false, rosterVersioningXmlns, rosterNegotiator); + RosterFeatureNegotiator() + : _supported = false, + super(11, false, rosterVersioningXmlns, rosterNegotiator); /// True if rosterVersioning is supported. False otherwise. bool _supported; bool get isSupported => _supported; - + @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { // negotiate is only called when the negotiator matched, meaning the server // advertises roster versioning. _supported = true; @@ -101,23 +112,26 @@ class RosterManager extends XmppManagerBase { @override void register(XmppManagerAttributes attributes) { super.register(attributes); - _stateManager.register(attributes.sendEvent); + _stateManager.register(attributes.sendEvent); } - + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'iq', - tagName: 'query', - tagXmlns: rosterXmlns, - callback: _onRosterPush, - ) - ]; + StanzaHandler( + stanzaTag: 'iq', + tagName: 'query', + tagXmlns: rosterXmlns, + callback: _onRosterPush, + ) + ]; @override - Future isSupported() async => true; + Future isSupported() async => true; - Future _onRosterPush(Stanza stanza, StanzaHandlerData state) async { + Future _onRosterPush( + Stanza stanza, + StanzaHandlerData state, + ) async { final attrs = getAttributes(); final from = stanza.attributes['from'] as String?; final selfJid = attrs.getConnectionSettings().jid; @@ -128,7 +142,9 @@ class RosterManager extends XmppManagerBase { // - empty, i.e. not set // - a full JID of our own if (from != null && JID.fromString(from).toBare() != selfJid) { - logger.warning('Roster push invalid! Unexpected from attribute: ${stanza.toXml()}'); + logger.warning( + 'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}', + ); return state.copyWith(done: true); } @@ -148,13 +164,13 @@ class RosterManager extends XmppManagerBase { jid: item.attributes['jid']! as String, subscription: item.attributes['subscription']! as String, ask: item.attributes['ask'] as String?, - name: item.attributes['name'] as String?, + name: item.attributes['name'] as String?, ), query.attributes['ver'] as String?, ), ), ); - + await reply( state, 'result', @@ -166,23 +182,32 @@ class RosterManager extends XmppManagerBase { /// Shared code between requesting rosters without and with roster versioning, if /// the server deems a regular roster response more efficient than n roster pushes. - Future> _handleRosterResponse(XMLNode? query) async { + Future> _handleRosterResponse( + XMLNode? query, + ) async { final List items; String? rosterVersion; if (query != null) { - items = query.children.map( - (item) => XmppRosterItem( - name: item.attributes['name'] as String?, - jid: item.attributes['jid']! as String, - subscription: item.attributes['subscription']! as String, - ask: item.attributes['ask'] as String?, - groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(), - ), - ).toList(); + items = query.children + .map( + (item) => XmppRosterItem( + name: item.attributes['name'] as String?, + jid: item.attributes['jid']! as String, + subscription: item.attributes['subscription']! as String, + ask: item.attributes['ask'] as String?, + groups: item + .findTags('group') + .map((groupNode) => groupNode.innerText()) + .toList(), + ), + ) + .toList(); rosterVersion = query.attributes['ver'] as String?; } else { - logger.warning('Server response to roster request without roster versioning does not contain a element, while the type is not error. This violates RFC6121'); + logger.warning( + 'Server response to roster request without roster versioning does not contain a element, while the type is not error. This violates RFC6121', + ); return Result(NoQueryError()); } @@ -197,7 +222,7 @@ class RosterManager extends XmppManagerBase { return Result(result); } - + /// Requests the roster following RFC 6121. Future> requestRoster() async { final attrs = getAttributes(); @@ -230,7 +255,8 @@ class RosterManager extends XmppManagerBase { /// Requests a series of roster pushes according to RFC6121. Requires that the server /// advertises urn:xmpp:features:rosterver in the stream features. - Future> requestRosterPushes() async { + Future> + requestRosterPushes() async { final attrs = getAttributes(); final result = await attrs.sendStanza( Stanza.iq( @@ -257,12 +283,18 @@ class RosterManager extends XmppManagerBase { } bool rosterVersioningAvailable() { - return getAttributes().getNegotiatorById(rosterNegotiator)!.isSupported; + return getAttributes() + .getNegotiatorById(rosterNegotiator)! + .isSupported; } - + /// Attempts to add [jid] with a title of [title] and groups [groups] to the roster. /// Returns true if the process was successful, false otherwise. - Future addToRoster(String jid, String title, { List? groups }) async { + Future addToRoster( + String jid, + String title, { + List? groups, + }) async { final attrs = getAttributes(); final response = await attrs.sendStanza( Stanza.iq( @@ -276,9 +308,13 @@ class RosterManager extends XmppManagerBase { tag: 'item', attributes: { 'jid': jid, - ...title == jid.split('@')[0] ? {} : { 'name': title } + ...title == jid.split('@')[0] + ? {} + : {'name': title} }, - children: (groups ?? []).map((group) => XMLNode(tag: 'group', text: group)).toList(), + children: (groups ?? []) + .map((group) => XMLNode(tag: 'group', text: group)) + .toList(), ) ], ) diff --git a/packages/moxxmpp/lib/src/roster/state.dart b/packages/moxxmpp/lib/src/roster/state.dart index edf8046..a71270a 100644 --- a/packages/moxxmpp/lib/src/roster/state.dart +++ b/packages/moxxmpp/lib/src/roster/state.dart @@ -30,7 +30,7 @@ abstract class BaseRosterStateManager { /// A function to send an XmppEvent to moxxmpp's main event bus late void Function(XmppEvent) _sendEvent; - + /// Overrideable function /// Loads the old cached version of the roster and optionally that roster version /// from persistent storage into a RosterCacheLoadResult object. @@ -50,14 +50,19 @@ abstract class BaseRosterStateManager { /// /// [added] is a (possibly empty) list of XmppRosterItems that are added by the /// roster push or roster fetch request. - Future commitRoster(String? version, List removed, List modified, List added); + Future commitRoster( + String? version, + List removed, + List modified, + List added, + ); /// Internal function. Registers functions from the RosterManger against this /// instance. void register(void Function(XmppEvent) sendEvent) { _sendEvent = sendEvent; } - + /// Load and cache or return the cached roster version. Future getRosterVersion() async { return _lock.synchronized(() async { @@ -69,7 +74,12 @@ abstract class BaseRosterStateManager { /// A wrapper around _commitRoster that also sends an event to moxxmpp's event /// bus. - Future _commitRoster(String? version, List removed, List modified, List added) async { + Future _commitRoster( + String? version, + List removed, + List modified, + List added, + ) async { _sendEvent( RosterUpdatedEvent( removed, @@ -77,10 +87,10 @@ abstract class BaseRosterStateManager { added, ), ); - + await commitRoster(version, removed, modified, added); } - + /// Loads the cached roster data into memory, if that has not already happened. /// NOTE: Must be called from within the _lock critical section. Future _loadRosterCache() async { @@ -104,7 +114,7 @@ abstract class BaseRosterStateManager { null, ); } - + final index = _currentRoster!.indexWhere((i) => i.jid == item.jid); if (index == -1) { // The item does not exist @@ -173,7 +183,7 @@ abstract class BaseRosterStateManager { final added = List.empty(growable: true); await _loadRosterCache(); - + _currentVersion = result.ver; for (final item in result.items) { final result = _handleRosterItem(item); @@ -216,5 +226,10 @@ class TestingRosterStateManager extends BaseRosterStateManager { } @override - Future commitRoster(String? version, List removed, List modified, List added) async {} + Future commitRoster( + String? version, + List removed, + List modified, + List added, + ) async {} } diff --git a/packages/moxxmpp/lib/src/routing.dart b/packages/moxxmpp/lib/src/routing.dart index 7e9573f..5f279e7 100644 --- a/packages/moxxmpp/lib/src/routing.dart +++ b/packages/moxxmpp/lib/src/routing.dart @@ -1,6 +1 @@ -enum RoutingState { - error, - preConnection, - negotiating, - handleStanzas -} +enum RoutingState { error, preConnection, negotiating, handleStanzas } diff --git a/packages/moxxmpp/lib/src/settings.dart b/packages/moxxmpp/lib/src/settings.dart index 8479065..9547e02 100644 --- a/packages/moxxmpp/lib/src/settings.dart +++ b/packages/moxxmpp/lib/src/settings.dart @@ -1,8 +1,12 @@ import 'package:moxxmpp/src/jid.dart'; class ConnectionSettings { - - ConnectionSettings({ required this.jid, required this.password, required this.useDirectTLS, required this.allowPlainAuth }); + ConnectionSettings({ + required this.jid, + required this.password, + required this.useDirectTLS, + required this.allowPlainAuth, + }); final JID jid; final String password; final bool useDirectTLS; diff --git a/packages/moxxmpp/lib/src/socket.dart b/packages/moxxmpp/lib/src/socket.dart index f7dcd7a..4999087 100644 --- a/packages/moxxmpp/lib/src/socket.dart +++ b/packages/moxxmpp/lib/src/socket.dart @@ -25,20 +25,20 @@ abstract class BaseSocketWrapper { /// This must return events generated by the socket. /// See sub-classes of [XmppSocketEvent] for possible events. Stream getEventStream(); - + /// This must close the socket but not the streams so that the same class can be /// reused by calling [this.connect] again. void close(); /// Write [data] into the socket. If [redact] is not null, then [redact] will be /// logged instead of [data]. - void write(String data, { String? redact }); - + void write(String data, {String? redact}); + /// This must connect to [host]:[port] and initialize the streams accordingly. /// [domain] is the domain that TLS should be validated against, in case the Socket /// provides TLS encryption. Returns true if the connection has been successfully /// established. Returns false if the connection has failed. - Future connect(String domain, { String? host, int? port }); + Future connect(String domain, {String? host, int? port}); /// Returns true if the socket is secured, e.g. using TLS. bool isSecure(); diff --git a/packages/moxxmpp/lib/src/stanza.dart b/packages/moxxmpp/lib/src/stanza.dart index 37059f9..1e7bb1d 100644 --- a/packages/moxxmpp/lib/src/stanza.dart +++ b/packages/moxxmpp/lib/src/stanza.dart @@ -25,59 +25,83 @@ class StanzaError { class Stanza extends XMLNode { // ignore: use_super_parameters - Stanza({ this.to, this.from, this.type, this.id, List children = const [], required String tag, Map attributes = const {} }) : super( - tag: tag, - attributes: { - ...attributes, - ...type != null ? { 'type': type } : {}, - ...id != null ? { 'id': id } : {}, - ...to != null ? { 'to': to } : {}, - ...from != null ? { 'from': from } : {}, - 'xmlns': stanzaXmlns - }, - children: children, - ); + Stanza({ + this.to, + this.from, + this.type, + this.id, + List children = const [], + required String tag, + Map attributes = const {}, + }) : super( + tag: tag, + attributes: { + ...attributes, + ...type != null + ? {'type': type} + : {}, + ...id != null ? {'id': id} : {}, + ...to != null ? {'to': to} : {}, + ...from != null + ? {'from': from} + : {}, + 'xmlns': stanzaXmlns + }, + children: children, + ); - factory Stanza.iq({ String? to, String? from, String? type, String? id, List children = const [], Map? attributes = const {} }) { + factory Stanza.iq({ + String? to, + String? from, + String? type, + String? id, + List children = const [], + Map? attributes = const {}, + }) { return Stanza( tag: 'iq', from: from, to: to, id: id, type: type, - attributes: { - ...attributes!, - 'xmlns': stanzaXmlns - }, + attributes: {...attributes!, 'xmlns': stanzaXmlns}, children: children, ); } - factory Stanza.presence({ String? to, String? from, String? type, String? id, List children = const [], Map? attributes = const {} }) { + factory Stanza.presence({ + String? to, + String? from, + String? type, + String? id, + List children = const [], + Map? attributes = const {}, + }) { return Stanza( tag: 'presence', from: from, to: to, id: id, type: type, - attributes: { - ...attributes!, - 'xmlns': stanzaXmlns - }, + attributes: {...attributes!, 'xmlns': stanzaXmlns}, children: children, ); } - factory Stanza.message({ String? to, String? from, String? type, String? id, List children = const [], Map? attributes = const {} }) { + factory Stanza.message({ + String? to, + String? from, + String? type, + String? id, + List children = const [], + Map? attributes = const {}, + }) { return Stanza( tag: 'message', from: from, to: to, id: id, type: type, - attributes: { - ...attributes!, - 'xmlns': stanzaXmlns - }, + attributes: {...attributes!, 'xmlns': stanzaXmlns}, children: children, ); } @@ -92,10 +116,10 @@ class Stanza extends XMLNode { children: node.children, // TODO(Unknown): Remove to, from, id, and type // TODO(Unknown): Not sure if this is the correct way to approach this - attributes: node.attributes - .map((String key, dynamic value) { - return MapEntry(key, value.toString()); - }), + attributes: + node.attributes.map((String key, dynamic value) { + return MapEntry(key, value.toString()); + }), ); } @@ -104,7 +128,13 @@ class Stanza extends XMLNode { String? type; String? id; - Stanza copyWith({ String? id, String? from, String? to, String? type, List? children }) { + Stanza copyWith({ + String? id, + String? from, + String? to, + String? type, + List? children, + }) { return Stanza( tag: tag, to: to ?? this.to, @@ -119,21 +149,23 @@ class Stanza extends XMLNode { /// Build an element with a child <[condition] type="[type]" />. If [text] /// is not null, then the condition element will contain a element with [text] /// as the body. -XMLNode buildErrorElement(String type, String condition, { String? text }) { +XMLNode buildErrorElement(String type, String condition, {String? text}) { return XMLNode( tag: 'error', - attributes: { 'type': type }, + attributes: {'type': type}, children: [ XMLNode.xmlns( tag: condition, xmlns: fullStanzaXmlns, - children: text != null ? [ - XMLNode.xmlns( - tag: 'text', - xmlns: fullStanzaXmlns, - text: text, - ) - ] : [], + children: text != null + ? [ + XMLNode.xmlns( + tag: 'text', + xmlns: fullStanzaXmlns, + text: text, + ) + ] + : [], ), ], ); diff --git a/packages/moxxmpp/lib/src/stringxml.dart b/packages/moxxmpp/lib/src/stringxml.dart index fda7fe6..7fda088 100644 --- a/packages/moxxmpp/lib/src/stringxml.dart +++ b/packages/moxxmpp/lib/src/stringxml.dart @@ -10,13 +10,15 @@ class XMLNode { this.isDeclaration = false, }); XMLNode.xmlns({ - required this.tag, - required String xmlns, - Map attributes = const {}, - this.children = const [], - this.closeTag = true, - this.text, - }) : attributes = { 'xmlns': xmlns, ...attributes }, isDeclaration = false; + required this.tag, + required String xmlns, + Map attributes = const {}, + this.children = const [], + this.closeTag = true, + this.text, + }) : attributes = {'xmlns': xmlns, ...attributes}, + isDeclaration = false; + /// Because this API is better ;) /// Don't use in production. Just for testing factory XMLNode.fromXmlElement(XmlElement element) { @@ -36,10 +38,12 @@ class XMLNode { return XMLNode( tag: element.name.qualified, attributes: attributes, - children: element.childElements.toList().map(XMLNode.fromXmlElement).toList(), + children: + element.childElements.toList().map(XMLNode.fromXmlElement).toList(), ); } } + /// Just for testing purposes factory XMLNode.fromString(String str) { return XMLNode.fromXmlElement( @@ -61,13 +65,16 @@ class XMLNode { /// Renders the attributes of the node into "attr1=\"value\" attr2=...". String renderAttributes() { return attributes.keys.map((String key) { - final dynamic value = attributes[key]; - assert(value is String || value is int, 'XML values must either be string or int'); - if (value is String) { - return "$key='$value'"; - } else { - return '$key=$value'; - } + final dynamic value = attributes[key]; + assert( + value is String || value is int, + 'XML values must either be string or int', + ); + if (value is String) { + return "$key='$value'"; + } else { + return '$key=$value'; + } }).join(' '); } @@ -80,8 +87,8 @@ class XMLNode { return '<$tag$attrString>$text'; } else { return '<$decl$tag ${renderAttributes()}${closeTag ? " />" : "$decl>"}'; - } - } else { + } + } else { final childXml = children.map((child) => child.toXml()).join(); final xml = '<$decl$tag ${renderAttributes()}$decl>$childXml'; return xml + (closeTag ? '' : ''); @@ -93,16 +100,16 @@ class XMLNode { XMLNode? _firstTag(bool Function(XMLNode) test) { try { return children.firstWhere(test); - } catch(e) { + } catch (e) { return null; } } - + /// Returns the first xml node that matches the description: /// - node's tag is equal to [tag] /// - (optional) node's xmlns attribute is equal to [xmlns] /// Returns null if none is found. - XMLNode? firstTag(String tag, { String? xmlns}) { + XMLNode? firstTag(String tag, {String? xmlns}) { return _firstTag((node) { if (xmlns != null) { return node.tag == tag && node.attributes['xmlns'] == xmlns; @@ -119,21 +126,22 @@ class XMLNode { return node.attributes['xmlns'] == xmlns; }); } - + /// Returns all children whose tag is equal to [tag]. - List findTags(String tag, { String? xmlns }) { + List findTags(String tag, {String? xmlns}) { return children.where((element) { - final xmlnsMatches = xmlns != null ? element.attributes['xmlns'] == xmlns : true; + final xmlnsMatches = + xmlns != null ? element.attributes['xmlns'] == xmlns : true; return element.tag == tag && xmlnsMatches; }).toList(); } - + List findTagsByXmlns(String xmlns) { return children - .where((element) => element.attributes['xmlns'] == xmlns) - .toList(); + .where((element) => element.attributes['xmlns'] == xmlns) + .toList(); } - + /// Returns the inner text of the node. If none is set, returns the "". String innerText() { return text ?? ''; diff --git a/packages/moxxmpp/lib/src/types/result.dart b/packages/moxxmpp/lib/src/types/result.dart index 06cabb9..a277740 100644 --- a/packages/moxxmpp/lib/src/types/result.dart +++ b/packages/moxxmpp/lib/src/types/result.dart @@ -1,6 +1,9 @@ class Result { - - const Result(this._data) : assert(_data is T || _data is V, 'Invalid data type: Must be either $T or $V'); + const Result(this._data) + : assert( + _data is T || _data is V, + 'Invalid data type: Must be either $T or $V', + ); final dynamic _data; bool isType() => _data is S; diff --git a/packages/moxxmpp/lib/src/util/queue.dart b/packages/moxxmpp/lib/src/util/queue.dart index f4c2894..db0456d 100644 --- a/packages/moxxmpp/lib/src/util/queue.dart +++ b/packages/moxxmpp/lib/src/util/queue.dart @@ -14,7 +14,7 @@ class AsyncQueue { /// The actual job queue. final Queue _queue = Queue(); - + /// Indicates whether we are currently executing a job. bool _running = false; @@ -23,7 +23,7 @@ class AsyncQueue { @visibleForTesting bool get isRunning => _running; - + /// Adds a job [job] to the queue. Future addJob(AsyncQueueJob job) async { await _lock.synchronized(() { @@ -39,7 +39,7 @@ class AsyncQueue { Future clear() async { await _lock.synchronized(_queue.clear); } - + Future _popJob() async { final job = _queue.removeFirst(); final future = job(); diff --git a/packages/moxxmpp/lib/src/util/wait.dart b/packages/moxxmpp/lib/src/util/wait.dart index 1b92a1a..473c8fd 100644 --- a/packages/moxxmpp/lib/src/util/wait.dart +++ b/packages/moxxmpp/lib/src/util/wait.dart @@ -53,7 +53,7 @@ class WaitForTracker { } }); } - + /// Remove all tasks from the tracker. Future clear() async { await _lock.synchronized(_tracker.clear); diff --git a/packages/moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart b/packages/moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart index bf2dfd5..97899ef 100644 --- a/packages/moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart +++ b/packages/moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart @@ -8,20 +8,23 @@ const blurhashThumbnailType = '$fileThumbnailsXmlns:blurhash'; abstract class Thumbnail {} class BlurhashThumbnail extends Thumbnail { - BlurhashThumbnail(this.hash); final String hash; } Thumbnail? parseFileThumbnailElement(XMLNode node) { - assert(node.attributes['xmlns'] == fileThumbnailsXmlns, 'Invalid element xmlns'); + assert( + node.attributes['xmlns'] == fileThumbnailsXmlns, + 'Invalid element xmlns', + ); assert(node.tag == 'file-thumbnail', 'Invalid element name'); switch (node.attributes['type']!) { - case blurhashThumbnailType: { - final hash = node.firstTag('blurhash')!.innerText(); - return BlurhashThumbnail(hash); - } + case blurhashThumbnailType: + { + final hash = node.firstTag('blurhash')!.innerText(); + return BlurhashThumbnail(hash); + } } return null; @@ -48,7 +51,7 @@ XMLNode constructFileThumbnailElement(Thumbnail thumbnail) { return XMLNode.xmlns( tag: 'file-thumbnail', xmlns: fileThumbnailsXmlns, - attributes: { 'type': type }, - children: [ node ], + attributes: {'type': type}, + children: [node], ); } diff --git a/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart b/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart index d0a802a..7c1caad 100644 --- a/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart +++ b/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart @@ -15,34 +15,38 @@ class FileUploadNotificationManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'file-upload', - tagXmlns: fileUploadNotificationXmlns, - callback: _onFileUploadNotificationReceived, - priority: -99, - ), - StanzaHandler( - stanzaTag: 'message', - tagName: 'replaces', - tagXmlns: fileUploadNotificationXmlns, - callback: _onFileUploadNotificationReplacementReceived, - priority: -99, - ), - StanzaHandler( - stanzaTag: 'message', - tagName: 'cancelled', - tagXmlns: fileUploadNotificationXmlns, - callback: _onFileUploadNotificationCancellationReceived, - priority: -99, - ), - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'file-upload', + tagXmlns: fileUploadNotificationXmlns, + callback: _onFileUploadNotificationReceived, + priority: -99, + ), + StanzaHandler( + stanzaTag: 'message', + tagName: 'replaces', + tagXmlns: fileUploadNotificationXmlns, + callback: _onFileUploadNotificationReplacementReceived, + priority: -99, + ), + StanzaHandler( + stanzaTag: 'message', + tagName: 'cancelled', + tagXmlns: fileUploadNotificationXmlns, + callback: _onFileUploadNotificationCancellationReceived, + priority: -99, + ), + ]; @override Future isSupported() async => true; - Future _onFileUploadNotificationReceived(Stanza message, StanzaHandlerData state) async { - final funElement = message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; + Future _onFileUploadNotificationReceived( + Stanza message, + StanzaHandlerData state, + ) async { + final funElement = + message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; return state.copyWith( fun: FileMetadataData.fromXML( funElement.firstTag('file', xmlns: fileMetadataXmlns)!, @@ -50,15 +54,23 @@ class FileUploadNotificationManager extends XmppManagerBase { ); } - Future _onFileUploadNotificationReplacementReceived(Stanza message, StanzaHandlerData state) async { - final element = message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; + Future _onFileUploadNotificationReplacementReceived( + Stanza message, + StanzaHandlerData state, + ) async { + final element = + message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; return state.copyWith( funReplacement: element.attributes['id']! as String, ); } - Future _onFileUploadNotificationCancellationReceived(Stanza message, StanzaHandlerData state) async { - final element = message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; + Future _onFileUploadNotificationCancellationReceived( + Stanza message, + StanzaHandlerData state, + ) async { + final element = + message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; return state.copyWith( funCancellation: element.attributes['id']! as String, ); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0004.dart b/packages/moxxmpp/lib/src/xeps/xep_0004.dart index e24aa8a..27ea054 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0004.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0004.dart @@ -3,14 +3,16 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; class DataFormOption { - const DataFormOption({ required this.value, this.label }); + const DataFormOption({required this.value, this.label}); final String? label; final String value; XMLNode toXml() { return XMLNode( tag: 'option', - attributes: label != null ? { 'label': label } : {}, + attributes: label != null + ? {'label': label} + : {}, children: [ XMLNode( tag: 'value', @@ -23,13 +25,13 @@ class DataFormOption { class DataFormField { const DataFormField({ - required this.options, - required this.values, - required this.isRequired, - this.varAttr, - this.type, - this.description, - this.label, + required this.options, + required this.values, + required this.isRequired, + this.varAttr, + this.type, + this.description, + this.label, }); final String? description; final bool isRequired; @@ -43,9 +45,13 @@ class DataFormField { return XMLNode( tag: 'field', attributes: { - ...varAttr != null ? { 'var': varAttr } : {}, - ...type != null ? { 'type': type } : {}, - ...label != null ? { 'label': label } : {} + ...varAttr != null + ? {'var': varAttr} + : {}, + ...type != null ? {'type': type} : {}, + ...label != null + ? {'label': label} + : {} }, children: [ ...description != null ? [XMLNode(tag: 'desc', text: description)] : [], @@ -59,12 +65,12 @@ class DataFormField { class DataForm { const DataForm({ - required this.type, - required this.instructions, - required this.fields, - required this.reported, - required this.items, - this.title, + required this.type, + required this.instructions, + required this.fields, + required this.reported, + required this.items, + this.title, }); final String type; final String? title; @@ -76,23 +82,23 @@ class DataForm { DataFormField? getFieldByVar(String varAttr) { return firstWhereOrNull(fields, (field) => field.varAttr == varAttr); } - + XMLNode toXml() { return XMLNode.xmlns( tag: 'x', xmlns: dataFormsXmlns, - attributes: { - 'type': type - }, + attributes: {'type': type}, children: [ ...instructions.map((i) => XMLNode(tag: 'instruction', text: i)), ...title != null ? [XMLNode(tag: 'title', text: title)] : [], ...fields.map((field) => field.toXml()), ...reported.map((report) => report.toXml()), - ...items.map((item) => XMLNode( - tag: 'item', - children: item.map((i) => i.toXml()).toList(), - ),), + ...items.map( + (item) => XMLNode( + tag: 'item', + children: item.map((i) => i.toXml()).toList(), + ), + ), ], ); } @@ -128,10 +134,19 @@ DataForm parseDataForm(XMLNode x) { final type = x.attributes['type']! as String; final title = x.firstTag('title')?.innerText(); - final instructions = x.findTags('instructions').map((i) => i.innerText()).toList(); + final instructions = + x.findTags('instructions').map((i) => i.innerText()).toList(); final fields = x.findTags('field').map(_parseDataFormField).toList(); - final reported = x.firstTag('reported')?.findTags('field').map((i) => _parseDataFormField(i.firstTag('field')!)).toList() ?? []; - final items = x.findTags('item').map((i) => i.findTags('field').map(_parseDataFormField).toList()).toList(); + final reported = x + .firstTag('reported') + ?.findTags('field') + .map((i) => _parseDataFormField(i.firstTag('field')!)) + .toList() ?? + []; + final items = x + .findTags('item') + .map((i) => i.findTags('field').map(_parseDataFormField).toList()) + .toList(); return DataForm( type: type, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart index f1df4d6..5e4001e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart @@ -4,7 +4,7 @@ import 'package:meta/meta.dart'; @immutable class DiscoCacheKey { const DiscoCacheKey(this.jid, this.node); - + /// The JID we're requesting disco data from. final String jid; @@ -13,11 +13,9 @@ class DiscoCacheKey { @override bool operator ==(Object other) { - return other is DiscoCacheKey && - jid == other.jid && - node == other.node; + return other is DiscoCacheKey && jid == other.jid && node == other.node; } - + @override int get hashCode => jid.hashCode ^ node.hashCode; } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart index 8b9c6c9..5b6810c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/helpers.dart @@ -5,21 +5,29 @@ import 'package:moxxmpp/src/stringxml.dart'; // TODO(PapaTutuWawa): Move types into types.dart Stanza buildDiscoInfoQueryStanza(String entity, String? node) { - return Stanza.iq(to: entity, type: 'get', children: [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoInfoXmlns, - attributes: node != null ? { 'node': node } : {}, - ) - ],); + return Stanza.iq( + to: entity, + type: 'get', + children: [ + XMLNode.xmlns( + tag: 'query', + xmlns: discoInfoXmlns, + attributes: node != null ? {'node': node} : {}, + ) + ], + ); } -Stanza buildDiscoItemsQueryStanza(String entity, { String? node }) { - return Stanza.iq(to: entity, type: 'get', children: [ - XMLNode.xmlns( - tag: 'query', - xmlns: discoItemsXmlns, - attributes: node != null ? { 'node': node } : {}, - ) - ],); +Stanza buildDiscoItemsQueryStanza(String entity, {String? node}) { + return Stanza.iq( + to: entity, + type: 'get', + children: [ + XMLNode.xmlns( + tag: 'query', + xmlns: discoItemsXmlns, + attributes: node != null ? {'node': node} : {}, + ) + ], + ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart index 7372df1..3da89c1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/types.dart @@ -5,7 +5,12 @@ import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart'; class Identity { - const Identity({ required this.category, required this.type, this.name, this.lang }); + const Identity({ + required this.category, + required this.type, + this.name, + this.lang, + }); final String category; final String type; final String? name; @@ -18,7 +23,9 @@ class Identity { 'category': category, 'type': type, 'name': name, - ...lang == null ? {} : { 'xml:lang': lang } + ...lang == null + ? {} + : {'xml:lang': lang} }, ); } @@ -50,7 +57,8 @@ class DiscoInfo { name: element.attributes['name'] as String?, ), ); - } else if (element.tag == 'x' && element.attributes['xmlns'] == dataFormsXmlns) { + } else if (element.tag == 'x' && + element.attributes['xmlns'] == dataFormsXmlns) { extendedInfo.add( parseDataForm(element), ); @@ -76,18 +84,22 @@ class DiscoInfo { return XMLNode.xmlns( tag: 'query', xmlns: discoInfoXmlns, - attributes: node != null ? - { 'node': node!, } : - {}, + attributes: node != null + ? { + 'node': node!, + } + : {}, children: [ ...identities.map((identity) => identity.toXMLNode()), - ...features.map((feature) => XMLNode( - tag: 'feature', - attributes: { 'var': feature, }, - ),), - - if (extendedInfo.isNotEmpty) - ...extendedInfo.map((ei) => ei.toXml()), + ...features.map( + (feature) => XMLNode( + tag: 'feature', + attributes: { + 'var': feature, + }, + ), + ), + if (extendedInfo.isNotEmpty) ...extendedInfo.map((ei) => ei.toXml()), ], ); } @@ -95,7 +107,7 @@ class DiscoInfo { @immutable 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? node; final String? name; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart b/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart index 029a59f..d8e682d 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart @@ -32,15 +32,15 @@ class DiscoManager extends XmppManagerBase { /// [identities] is a list of disco identities that should be added by default /// to a disco#info response. DiscoManager(List identities) - : _identities = List.from(identities), - super(discoManager); + : _identities = List.from(identities), + super(discoManager); /// Our features final List _features = List.empty(growable: true); /// Disco identities that we advertise final List _identities; - + /// Map full JID to Capability hashes final Map _capHashCache = {}; @@ -51,10 +51,12 @@ class DiscoManager extends XmppManagerBase { final Map _discoInfoCache = {}; /// The tracker for tracking disco#info queries that are in flight. - final WaitForTracker> _discoInfoTracker = WaitForTracker(); + final WaitForTracker> + _discoInfoTracker = WaitForTracker(); /// The tracker for tracking disco#info queries that are in flight. - final WaitForTracker>> _discoItemsTracker = WaitForTracker(); + final WaitForTracker>> + _discoItemsTracker = WaitForTracker(); /// Cache lock final Lock _cacheLock = Lock(); @@ -72,26 +74,27 @@ class DiscoManager extends XmppManagerBase { List get features => _features; @visibleForTesting - WaitForTracker> get infoTracker => _discoInfoTracker; - - @override - List getIncomingStanzaHandlers() => [ - StanzaHandler( - tagName: 'query', - tagXmlns: discoInfoXmlns, - stanzaTag: 'iq', - callback: _onDiscoInfoRequest, - ), - StanzaHandler( - tagName: 'query', - tagXmlns: discoItemsXmlns, - stanzaTag: 'iq', - callback: _onDiscoItemsRequest, - ), - ]; + WaitForTracker> + get infoTracker => _discoInfoTracker; @override - List getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ]; + List getIncomingStanzaHandlers() => [ + StanzaHandler( + tagName: 'query', + tagXmlns: discoInfoXmlns, + stanzaTag: 'iq', + callback: _onDiscoInfoRequest, + ), + StanzaHandler( + tagName: 'query', + tagXmlns: discoItemsXmlns, + stanzaTag: 'iq', + callback: _onDiscoItemsRequest, + ), + ]; + + @override + List getDiscoFeatures() => [discoInfoXmlns, discoItemsXmlns]; @override Future isSupported() async => true; @@ -114,7 +117,7 @@ class DiscoManager extends XmppManagerBase { await _discoItemsTracker.resolveAll( Result>(UnknownDiscoError()), ); - + await _cacheLock.synchronized(() async { // Clear the cache _discoInfoCache.clear(); @@ -131,7 +134,7 @@ class DiscoManager extends XmppManagerBase { void registerItemsCallback(String node, DiscoItemsRequestCallback callback) { _discoItemsCallbacks[node] = callback; } - + /// Adds a list of features to the possible disco info response. /// This function only adds features that are not already present in the disco features. void addFeatures(List features) { @@ -151,7 +154,7 @@ class DiscoManager extends XmppManagerBase { } } } - + Future _onPresence(JID from, Stanza presence) async { final c = presence.firstTag('c', xmlns: capsXmlns); if (c == null) return; @@ -161,7 +164,7 @@ class DiscoManager extends XmppManagerBase { c.attributes['node']! as String, c.attributes['hash']! as String, ); - + // Check if we already know of that cache var cached = false; await _cacheLock.synchronized(() async { @@ -172,8 +175,11 @@ class DiscoManager extends XmppManagerBase { if (cached) return; // Request the cap hash - logger.finest("Received capability hash we don't know about. Requesting it..."); - final result = await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}'); + logger.finest( + "Received capability hash we don't know about. Requesting it...", + ); + final result = + await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}'); if (result.isType()) return; await _cacheLock.synchronized(() async { @@ -181,7 +187,7 @@ class DiscoManager extends XmppManagerBase { _capHashInfoCache[info.ver] = result.get(); }); } - + /// Returns the [DiscoInfo] object that would be used as the response to a disco#info /// query against our bare JID with no node. The results node attribute is set /// to [node]. @@ -194,8 +200,11 @@ class DiscoManager extends XmppManagerBase { null, ); } - - Future _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { + + Future _onDiscoInfoRequest( + Stanza stanza, + StanzaHandlerData state, + ) async { if (stanza.type != 'get') return state; final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!; @@ -226,7 +235,10 @@ class DiscoManager extends XmppManagerBase { return state.copyWith(done: true); } - Future _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { + Future _onDiscoItemsRequest( + Stanza stanza, + StanzaHandlerData state, + ) async { if (stanza.type != 'get') return state; final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!; @@ -254,7 +266,10 @@ class DiscoManager extends XmppManagerBase { return state; } - Future _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result result) async { + Future _exitDiscoInfoCriticalSection( + DiscoCacheKey key, + Result result, + ) async { await _cacheLock.synchronized(() async { // Add to cache if it is a result if (result.isType()) { @@ -264,12 +279,18 @@ class DiscoManager extends XmppManagerBase { await _discoInfoTracker.resolve(key, result); } - + /// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. - Future> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async { + Future> discoInfoQuery( + String entity, { + String? node, + bool shouldEncrypt = true, + }) async { final cacheKey = DiscoCacheKey(entity, node); DiscoInfo? info; - final ffuture = await _cacheLock.synchronized>?>?>(() async { + final ffuture = await _cacheLock + .synchronized>?>?>( + () async { // Check if we already know what the JID supports if (_discoInfoCache.containsKey(cacheKey)) { info = _discoInfoCache[cacheKey]; @@ -305,7 +326,7 @@ class DiscoManager extends XmppManagerBase { await _exitDiscoInfoCriticalSection(cacheKey, result); return result; } - + final result = Result( DiscoInfo.fromQuery( query, @@ -317,22 +338,26 @@ class DiscoManager extends XmppManagerBase { } /// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. - Future>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async { + Future>> 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() - .sendStanza( - buildDiscoItemsQueryStanza(entity, node: node), - encrypted: !shouldEncrypt, - ) as Stanza; + final stanza = await getAttributes().sendStanza( + buildDiscoItemsQueryStanza(entity, node: node), + encrypted: !shouldEncrypt, + ) as Stanza; final query = stanza.firstTag('query'); if (query == null) { - final result = Result>(InvalidResponseDiscoError()); + final result = + Result>(InvalidResponseDiscoError()); await _discoItemsTracker.resolve(key, result); return result; } @@ -340,16 +365,22 @@ class DiscoManager extends XmppManagerBase { if (stanza.type == 'error') { //final error = stanza.firstTag('error'); //print("Disco Items error: " + error.toXml()); - final result = Result>(ErrorResponseDiscoError()); + final result = + Result>(ErrorResponseDiscoError()); await _discoItemsTracker.resolve(key, result); return result; } - final items = query.findTags('item').map((node) => DiscoItem( - jid: node.attributes['jid']! as String, - node: node.attributes['node'] as String?, - name: node.attributes['name'] as String?, - ),).toList(); + final items = query + .findTags('item') + .map( + (node) => DiscoItem( + jid: node.attributes['jid']! as String, + node: node.attributes['node'] as String?, + name: node.attributes['name'] as String?, + ), + ) + .toList(); final result = Result>(items); await _discoItemsTracker.resolve(key, result); @@ -357,7 +388,11 @@ class DiscoManager extends XmppManagerBase { } /// Queries information about a jid based on its node and capability hash. - Future> discoInfoCapHashQuery(String jid, String node, String ver) async { + Future> discoInfoCapHashQuery( + String jid, + String node, + String ver, + ) async { return discoInfoQuery(jid, node: '$node#$ver'); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0054.dart b/packages/moxxmpp/lib/src/xeps/xep_0054.dart index e67d7f5..c57c9bf 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0054.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0054.dart @@ -16,12 +16,12 @@ class UnknownVCardError extends VCardError {} class InvalidVCardError extends VCardError {} class VCardPhoto { - const VCardPhoto({ this.binval }); + const VCardPhoto({this.binval}); final String? binval; } class VCard { - const VCard({ this.nickname, this.url, this.photo }); + const VCard({this.nickname, this.url, this.photo}); final String? nickname; final String? url; final VCardPhoto? photo; @@ -30,26 +30,29 @@ class VCard { class VCardManager extends XmppManagerBase { VCardManager() : super(vcardManager); final Map _lastHash = {}; - + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'presence', - tagName: 'x', - tagXmlns: vCardTempUpdate, - callback: _onPresence, - ) - ]; + StanzaHandler( + stanzaTag: 'presence', + tagName: 'x', + tagXmlns: vCardTempUpdate, + callback: _onPresence, + ) + ]; @override Future isSupported() async => true; - + /// In case we get the avatar hash some other way. void setLastHash(String jid, String hash) { _lastHash[jid] = hash; } - - Future _onPresence(Stanza presence, StanzaHandlerData state) async { + + Future _onPresence( + Stanza presence, + StanzaHandlerData state, + ) async { final x = presence.firstTag('x', xmlns: vCardTempUpdate)!; final hash = x.firstTag('photo')!.innerText(); @@ -76,10 +79,10 @@ class VCardManager extends XmppManagerBase { logger.warning('Failed to retrieve vCard for $from'); } } - + return state.copyWith(done: true); } - + VCardPhoto? _parseVCardPhoto(XMLNode? node) { if (node == null) return null; @@ -87,18 +90,18 @@ class VCardManager extends XmppManagerBase { binval: node.firstTag('BINVAL')?.innerText(), ); } - + VCard _parseVCard(XMLNode vcard) { final nickname = vcard.firstTag('NICKNAME')?.innerText(); final url = vcard.firstTag('URL')?.innerText(); - + return VCard( url: url, nickname: nickname, photo: _parseVCardPhoto(vcard.firstTag('PHOTO')), ); } - + Future> requestVCard(String jid) async { final result = await getAttributes().sendStanza( Stanza.iq( @@ -114,10 +117,14 @@ class VCardManager extends XmppManagerBase { encrypted: true, ); - if (result.attributes['type'] != 'result') return Result(UnknownVCardError()); + if (result.attributes['type'] != 'result') { + return Result(UnknownVCardError()); + } final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns); - if (vcard == null) return Result(UnknownVCardError()); - + if (vcard == null) { + return Result(UnknownVCardError()); + } + return Result(_parseVCard(vcard)); - } + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0060/helpers.dart b/packages/moxxmpp/lib/src/xeps/xep_0060/helpers.dart index f66ec15..763e0fe 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0060/helpers.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0060/helpers.dart @@ -20,6 +20,6 @@ PubSubError getPubSubError(XMLNode stanza) { return EjabberdMaxItemsError(); } } - + return UnknownPubSubError(); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart b/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart index 97af503..57ddcb5 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart @@ -22,7 +22,7 @@ class PubSubPublishOptions { }); final String? accessModel; final String? maxItems; - + XMLNode toXml() { return DataForm( type: 'submit', @@ -33,33 +33,41 @@ class PubSubPublishOptions { const DataFormField( options: [], isRequired: false, - values: [ pubsubPublishOptionsXmlns ], + values: [pubsubPublishOptionsXmlns], varAttr: 'FORM_TYPE', type: 'hidden', ), - ...accessModel != null ? [ - DataFormField( - options: [], - isRequired: false, - values: [ accessModel! ], - varAttr: 'pubsub#access_model', - ) - ] : [], - ...maxItems != null ? [ - DataFormField( - options: [], - isRequired: false, - values: [maxItems! ], - varAttr: 'pubsub#max_items', - ), - ] : [], + ...accessModel != null + ? [ + DataFormField( + options: [], + isRequired: false, + values: [accessModel!], + varAttr: 'pubsub#access_model', + ) + ] + : [], + ...maxItems != null + ? [ + DataFormField( + options: [], + isRequired: false, + values: [maxItems!], + varAttr: 'pubsub#max_items', + ), + ] + : [], ], ).toXml(); } } class PubSubItem { - const PubSubItem({ required this.id, required this.node, required this.payload }); + const PubSubItem({ + required this.id, + required this.node, + required this.payload, + }); final String id; final String node; final XMLNode payload; @@ -73,32 +81,37 @@ class PubSubManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'event', - tagXmlns: pubsubEventXmlns, - callback: _onPubsubMessage, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'event', + tagXmlns: pubsubEventXmlns, + callback: _onPubsubMessage, + ) + ]; @override Future isSupported() async => true; - Future _onPubsubMessage(Stanza message, StanzaHandlerData state) async { + Future _onPubsubMessage( + Stanza message, + StanzaHandlerData state, + ) async { logger.finest('Received PubSub event'); final event = message.firstTag('event', xmlns: pubsubEventXmlns)!; final items = event.firstTag('items')!; final item = items.firstTag('item')!; - getAttributes().sendEvent(PubSubNotificationEvent( - item: PubSubItem( - id: item.attributes['id']! as String, - node: items.attributes['node']! as String, - payload: item.children[0], + getAttributes().sendEvent( + PubSubNotificationEvent( + item: PubSubItem( + id: item.attributes['id']! as String, + node: items.attributes['node']! as String, + payload: item.children[0], + ), + from: message.attributes['from']! as String, ), - from: message.attributes['from']! as String, - ),); - + ); + return state.copyWith(done: true); } @@ -107,27 +120,37 @@ class PubSubManager extends XmppManagerBase { final response = await dm.discoItemsQuery(jid, node: node); var count = 0; if (response.isType()) { - logger.warning('_getNodeItemCount: disco#items query failed. Assuming no items.'); + logger.warning( + '_getNodeItemCount: disco#items query failed. Assuming no items.', + ); } else { count = response.get>().length; } return count; } - - Future _preprocessPublishOptions(String jid, String node, PubSubPublishOptions options) async { + + Future _preprocessPublishOptions( + String jid, + String node, + PubSubPublishOptions options, + ) async { if (options.maxItems != null) { final dm = getAttributes().getManagerById(discoManager)!; final result = await dm.discoInfoQuery(jid); if (result.isType()) { if (options.maxItems == 'max') { - logger.severe('disco#info query failed and options.maxItems is set to "max".'); + logger.severe( + 'disco#info query failed and options.maxItems is set to "max".', + ); return options; } } - final nodeMultiItemsSupported = result.isType() && result.get().features.contains(pubsubNodeConfigMultiItems); - final nodeMaxSupported = result.isType() && result.get().features.contains(pubsubNodeConfigMax); + final nodeMultiItemsSupported = result.isType() && + result.get().features.contains(pubsubNodeConfigMultiItems); + final nodeMaxSupported = result.isType() && + result.get().features.contains(pubsubNodeConfigMax); if (options.maxItems != null && !nodeMultiItemsSupported) { // TODO(PapaTutuWawa): Here, we need to admit defeat logger.finest('PubSub host does not support multi-items!'); @@ -136,7 +159,9 @@ class PubSubManager extends XmppManagerBase { accessModel: options.accessModel, ); } else if (options.maxItems == 'max' && !nodeMaxSupported) { - logger.finest('PubSub host does not support node-config-max. Working around it'); + logger.finest( + 'PubSub host does not support node-config-max. Working around it', + ); final count = await _getNodeItemCount(jid, node) + 1; return PubSubPublishOptions( @@ -148,7 +173,7 @@ class PubSubManager extends XmppManagerBase { return options; } - + Future> subscribe(String jid, String node) async { final attrs = getAttributes(); final result = await attrs.sendStanza( @@ -173,13 +198,19 @@ class PubSubManager extends XmppManagerBase { ), ); - if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); + if (result.attributes['type'] != 'result') { + return Result(UnknownPubSubError()); + } final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); - if (pubsub == null) return Result(UnknownPubSubError()); + if (pubsub == null) { + return Result(UnknownPubSubError()); + } final subscription = pubsub.firstTag('subscription'); - if (subscription == null) return Result(UnknownPubSubError()); + if (subscription == null) { + return Result(UnknownPubSubError()); + } return Result(subscription.attributes['subscription'] == 'subscribed'); } @@ -208,27 +239,32 @@ class PubSubManager extends XmppManagerBase { ), ); - if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); + if (result.attributes['type'] != 'result') { + return Result(UnknownPubSubError()); + } final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); - if (pubsub == null) return Result(UnknownPubSubError()); + if (pubsub == null) { + return Result(UnknownPubSubError()); + } final subscription = pubsub.firstTag('subscription'); - if (subscription == null) return Result(UnknownPubSubError()); + if (subscription == null) { + return Result(UnknownPubSubError()); + } return Result(subscription.attributes['subscription'] == 'none'); } - + /// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it /// was successful. False otherwise. Future> publish( String jid, String node, XMLNode payload, { - String? id, - PubSubPublishOptions? options, - } - ) async { + String? id, + PubSubPublishOptions? options, + }) async { return _publish( jid, node, @@ -242,12 +278,11 @@ class PubSubManager extends XmppManagerBase { String jid, String node, XMLNode payload, { - String? id, - PubSubPublishOptions? options, - // Should, if publishing fails, try to reconfigure and publish again? - bool tryConfigureAndPublish = true, - } - ) async { + String? id, + PubSubPublishOptions? options, + // Should, if publishing fails, try to reconfigure and publish again? + bool tryConfigureAndPublish = true, + }) async { PubSubPublishOptions? pubOptions; if (options != null) { pubOptions = await _preprocessPublishOptions(jid, node, options); @@ -264,21 +299,25 @@ class PubSubManager extends XmppManagerBase { children: [ XMLNode( tag: 'publish', - attributes: { 'node': node }, + attributes: {'node': node}, children: [ XMLNode( tag: 'item', - attributes: id != null ? { 'id': id } : {}, - children: [ payload ], + attributes: id != null + ? {'id': id} + : {}, + children: [payload], ) ], ), - ...options != null ? [ - XMLNode( - tag: 'publish-options', - children: [options.toXml()], - ), - ] : [], + ...options != null + ? [ + XMLNode( + tag: 'publish-options', + children: [options.toXml()], + ), + ] + : [], ], ) ], @@ -302,10 +341,16 @@ class PubSubManager extends XmppManagerBase { options: options, tryConfigureAndPublish: false, ); - if (publishResult.isType()) return publishResult; - } else if (error is EjabberdMaxItemsError && tryConfigureAndPublish && options != null) { + if (publishResult.isType()) { + return publishResult; + } + } else if (error is EjabberdMaxItemsError && + tryConfigureAndPublish && + options != null) { // TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info. - logger.warning('Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...'); + logger.warning( + 'Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...', + ); final count = await _getNodeItemCount(jid, node) + 1; return publish( jid, @@ -323,20 +368,31 @@ class PubSubManager extends XmppManagerBase { } final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns); - if (pubsubElement == null) return Result(MalformedResponseError()); + if (pubsubElement == null) { + return Result(MalformedResponseError()); + } final publishElement = pubsubElement.firstTag('publish'); - if (publishElement == null) return Result(MalformedResponseError()); + if (publishElement == null) { + return Result(MalformedResponseError()); + } final item = publishElement.firstTag('item'); - if (item == null) return Result(MalformedResponseError()); + if (item == null) { + return Result(MalformedResponseError()); + } - if (id != null) return Result(item.attributes['id'] == id); + if (id != null) { + return Result(item.attributes['id'] == id); + } return const Result(true); } - - Future>> getItems(String jid, String node) async { + + Future>> getItems( + String jid, + String node, + ) async { final result = await getAttributes().sendStanza( Stanza.iq( type: 'get', @@ -346,33 +402,38 @@ class PubSubManager extends XmppManagerBase { tag: 'pubsub', xmlns: pubsubXmlns, children: [ - XMLNode(tag: 'items', attributes: { 'node': node }), + XMLNode(tag: 'items', attributes: {'node': node}), ], ) ], ), ); - if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); + if (result.attributes['type'] != 'result') { + return Result(getPubSubError(result)); + } final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); - if (pubsub == null) return Result(getPubSubError(result)); + if (pubsub == null) { + return Result(getPubSubError(result)); + } - final items = pubsub - .firstTag('items')! - .children.map((item) { - return PubSubItem( - id: item.attributes['id']! as String, - payload: item.children[0], - node: node, - ); - }) - .toList(); + final items = pubsub.firstTag('items')!.children.map((item) { + return PubSubItem( + id: item.attributes['id']! as String, + payload: item.children[0], + node: node, + ); + }).toList(); return Result(items); } - Future> getItem(String jid, String node, String id) async { + Future> getItem( + String jid, + String node, + String id, + ) async { final result = await getAttributes().sendStanza( Stanza.iq( type: 'get', @@ -384,11 +445,11 @@ class PubSubManager extends XmppManagerBase { children: [ XMLNode( tag: 'items', - attributes: { 'node': node }, + attributes: {'node': node}, children: [ XMLNode( tag: 'item', - attributes: { 'id': id }, + attributes: {'id': id}, ), ], ), @@ -398,7 +459,9 @@ class PubSubManager extends XmppManagerBase { ), ); - if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); + if (result.attributes['type'] != 'result') { + return Result(getPubSubError(result)); + } final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); if (pubsub == null) return Result(getPubSubError(result)); @@ -415,7 +478,11 @@ class PubSubManager extends XmppManagerBase { return Result(item); } - Future> configure(String jid, String node, PubSubPublishOptions options) async { + Future> configure( + String jid, + String node, + PubSubPublishOptions options, + ) async { final attrs = getAttributes(); // Request the form @@ -439,7 +506,9 @@ class PubSubManager extends XmppManagerBase { ], ), ); - if (form.attributes['type'] != 'result') return Result(getPubSubError(form)); + if (form.attributes['type'] != 'result') { + return Result(getPubSubError(form)); + } final submit = await attrs.sendStanza( Stanza.iq( @@ -464,7 +533,9 @@ class PubSubManager extends XmppManagerBase { ], ), ); - if (submit.attributes['type'] != 'result') return Result(getPubSubError(form)); + if (submit.attributes['type'] != 'result') { + return Result(getPubSubError(form)); + } return const Result(true); } @@ -499,7 +570,11 @@ class PubSubManager extends XmppManagerBase { return const Result(true); } - Future> retract(JID host, String node, String itemId) async { + Future> retract( + JID host, + String node, + String itemId, + ) async { final request = await getAttributes().sendStanza( Stanza.iq( type: 'set', diff --git a/packages/moxxmpp/lib/src/xeps/xep_0066.dart b/packages/moxxmpp/lib/src/xeps/xep_0066.dart index bca9f3b..61bb4cc 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0066.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0066.dart @@ -8,7 +8,7 @@ import 'package:moxxmpp/src/stringxml.dart'; /// A data class representing the jabber:x:oob tag. class OOBData { - const OOBData({ this.url, this.desc }); + const OOBData({this.url, this.desc}); final String? url; final String? desc; } @@ -22,7 +22,7 @@ XMLNode constructOOBNode(OOBData data) { if (data.desc != null) { children.add(XMLNode(tag: 'desc', text: data.desc)); } - + return XMLNode.xmlns( tag: 'x', xmlns: oobDataXmlns, @@ -34,24 +34,27 @@ class OOBManager extends XmppManagerBase { OOBManager() : super(oobManager); @override - List getDiscoFeatures() => [ oobDataXmlns ]; + List getDiscoFeatures() => [oobDataXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'x', - tagXmlns: oobDataXmlns, - callback: _onMessage, - // Before the message manager - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'x', + tagXmlns: oobDataXmlns, + callback: _onMessage, + // Before the message manager + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final x = message.firstTag('x', xmlns: oobDataXmlns)!; final url = x.firstTag('url'); final desc = x.firstTag('desc'); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0084.dart b/packages/moxxmpp/lib/src/xeps/xep_0084.dart index 39c84c2..78bf1bd 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0084.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0084.dart @@ -15,7 +15,7 @@ abstract class AvatarError {} class UnknownAvatarError extends AvatarError {} class UserAvatar { - const UserAvatar({ required this.base64, required this.hash }); + const UserAvatar({required this.base64, required this.hash}); final String base64; final String hash; } @@ -47,8 +47,9 @@ class UserAvatarMetadata { class UserAvatarManager extends XmppManagerBase { UserAvatarManager() : super(userAvatarManager); - PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager; - + PubSubManager _getPubSubManager() => + getAttributes().getManagerById(pubsubManager)! as PubSubManager; + @override Future onXmppEvent(XmppEvent event) async { if (event is PubSubNotificationEvent) { @@ -56,7 +57,9 @@ class UserAvatarManager extends XmppManagerBase { if (event.item.payload.tag != 'data' || event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) { - logger.warning('Received avatar update from ${event.from} but the payload is invalid. Ignoring...'); + logger.warning( + 'Received avatar update from ${event.from} but the payload is invalid. Ignoring...', + ); return; } @@ -96,7 +99,11 @@ class UserAvatarManager extends XmppManagerBase { /// Publish the avatar data, [base64], on the pubsub node using [hash] as /// the item id. [hash] must be the SHA-1 hash of the image data, while /// [base64] must be the base64-encoded version of the image data. - Future> publishUserAvatar(String base64, String hash, bool public) async { + Future> publishUserAvatar( + String base64, + String hash, + bool public, + ) async { final pubsub = _getPubSubManager(); final result = await pubsub.publish( getAttributes().getFullJID().toBare().toString(), @@ -113,14 +120,17 @@ class UserAvatarManager extends XmppManagerBase { ); if (result.isType()) return Result(UnknownAvatarError()); - + return const Result(true); } /// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public] /// is true, then the node will be set to an 'open' access model. If [public] is false, /// then the node will be set to an 'roster' access model. - Future> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async { + Future> publishUserAvatarMetadata( + UserAvatarMetadata metadata, + bool public, + ) async { final pubsub = _getPubSubManager(); final result = await pubsub.publish( getAttributes().getFullJID().toBare().toString(), @@ -150,7 +160,7 @@ class UserAvatarManager extends XmppManagerBase { if (result.isType()) return Result(UnknownAvatarError()); return const Result(true); } - + /// Subscribe the data and metadata node of [jid]. Future> subscribe(String jid) async { await _getPubSubManager().subscribe(jid, userAvatarDataXmlns); @@ -172,7 +182,11 @@ class UserAvatarManager extends XmppManagerBase { /// the node. Future> getAvatarId(String jid) async { final disco = getAttributes().getManagerById(discoManager)! as DiscoManager; - final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns, shouldEncrypt: false); + final response = await disco.discoItemsQuery( + jid, + node: userAvatarDataXmlns, + shouldEncrypt: false, + ); if (response.isType()) return Result(UnknownAvatarError()); final items = response.get>(); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0085.dart b/packages/moxxmpp/lib/src/xeps/xep_0085.dart index 9bf51de..b3d8ea3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0085.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0085.dart @@ -6,86 +6,96 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -enum ChatState { - active, - composing, - paused, - inactive, - gone -} +enum ChatState { active, composing, paused, inactive, gone } ChatState chatStateFromString(String raw) { - switch(raw) { - case 'active': { - return ChatState.active; - } - case 'composing': { - return ChatState.composing; - } - case 'paused': { - return ChatState.paused; - } - case 'inactive': { - return ChatState.inactive; - } - case 'gone': { - return ChatState.gone; - } - default: { - return ChatState.gone; - } + switch (raw) { + case 'active': + { + return ChatState.active; + } + case 'composing': + { + return ChatState.composing; + } + case 'paused': + { + return ChatState.paused; + } + case 'inactive': + { + return ChatState.inactive; + } + case 'gone': + { + return ChatState.gone; + } + default: + { + return ChatState.gone; + } } } + String chatStateToString(ChatState state) => state.toString().split('.').last; class ChatStateManager extends XmppManagerBase { ChatStateManager() : super(chatStateManager); @override - List getDiscoFeatures() => [ chatStateXmlns ]; + List getDiscoFeatures() => [chatStateXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagXmlns: chatStateXmlns, - callback: _onChatStateReceived, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagXmlns: chatStateXmlns, + callback: _onChatStateReceived, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onChatStateReceived(Stanza message, StanzaHandlerData state) async { + + Future _onChatStateReceived( + Stanza message, + StanzaHandlerData state, + ) async { final element = state.stanza.firstTagByXmlns(chatStateXmlns)!; ChatState? chatState; switch (element.tag) { - case 'active': { - chatState = ChatState.active; - } - break; - case 'composing': { - chatState = ChatState.composing; - } - break; - case 'paused': { - chatState = ChatState.paused; - } - break; - case 'inactive': { - chatState = ChatState.inactive; - } - break; - case 'gone': { - chatState = ChatState.gone; - } - break; - default: { - logger.warning("Received invalid chat state '${element.tag}'"); - } + case 'active': + { + chatState = ChatState.active; + } + break; + case 'composing': + { + chatState = ChatState.composing; + } + break; + case 'paused': + { + chatState = ChatState.paused; + } + break; + case 'inactive': + { + chatState = ChatState.inactive; + } + break; + case 'gone': + { + chatState = ChatState.gone; + } + break; + default: + { + logger.warning("Received invalid chat state '${element.tag}'"); + } } return state.copyWith(chatState: chatState); @@ -93,14 +103,18 @@ class ChatStateManager extends XmppManagerBase { /// Send a chat state notification to [to]. You can specify the type attribute /// of the message with [messageType]. - void sendChatState(ChatState state, String to, { String messageType = 'chat' }) { + void sendChatState( + ChatState state, + String to, { + String messageType = 'chat', + }) { final tagName = state.toString().split('.').last; getAttributes().sendStanza( Stanza.message( to: to, type: messageType, - children: [ XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns) ], + children: [XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns)], ), ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0115.dart b/packages/moxxmpp/lib/src/xeps/xep_0115.dart index 2ac3cb6..cbeaf8d 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0115.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0115.dart @@ -21,11 +21,17 @@ class CapabilityHashInfo { /// Calculates the Entitiy Capability hash according to XEP-0115 based on the /// disco information. -Future calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm) async { +Future calculateCapabilityHash( + DiscoInfo info, + HashAlgorithm algorithm, +) async { final buffer = StringBuffer(); final identitiesSorted = info.identities - .map((Identity i) => '${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}') - .toList(); + .map( + (Identity i) => + '${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}', + ) + .toList(); // ignore: cascade_invocations identitiesSorted.sort(ioctetSortComparator); buffer.write('${identitiesSorted.join("<")}<'); @@ -36,20 +42,23 @@ Future calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm) if (info.extendedInfo.isNotEmpty) { final sortedExt = info.extendedInfo - ..sort((a, b) => ioctetSortComparator( - a.getFieldByVar('FORM_TYPE')!.values.first, - b.getFieldByVar('FORM_TYPE')!.values.first, - ), - ); + ..sort( + (a, b) => ioctetSortComparator( + a.getFieldByVar('FORM_TYPE')!.values.first, + b.getFieldByVar('FORM_TYPE')!.values.first, + ), + ); for (final ext in sortedExt) { buffer.write('${ext.getFieldByVar("FORM_TYPE")!.values.first}<'); - final sortedFields = ext.fields..sort((a, b) => ioctetSortComparator( - a.varAttr!, - b.varAttr!, - ), - ); + final sortedFields = ext.fields + ..sort( + (a, b) => ioctetSortComparator( + a.varAttr!, + b.varAttr!, + ), + ); for (final field in sortedFields) { if (field.varAttr == 'FORM_TYPE') continue; @@ -62,8 +71,9 @@ Future 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 @@ -71,7 +81,8 @@ Future calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm) /// the DiscoManager. /// NOTE: This manager requires that the DiscoManager is also registered. class EntityCapabilitiesManager extends XmppManagerBase { - EntityCapabilitiesManager(this._capabilityHashBase) : super(entityCapabilitiesManager); + 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. @@ -84,15 +95,15 @@ class EntityCapabilitiesManager extends XmppManagerBase { Future isSupported() async => true; @override - List getDiscoFeatures() => [ capsXmlns ]; + List getDiscoFeatures() => [capsXmlns]; /// Computes, if required, the capability hash of the data provided by /// the DiscoManager. Future getCapabilityHash() async { _capabilityHash ??= await calculateCapabilityHash( getAttributes() - .getManagerById(discoManager)! - .getDiscoInfo(null), + .getManagerById(discoManager)! + .getDiscoInfo(null), getHashByName('sha-1')!, ); @@ -103,11 +114,11 @@ class EntityCapabilitiesManager extends XmppManagerBase { final hash = await getCapabilityHash(); return '$_capabilityHashBase#$hash'; } - + Future _onInfoQuery() async { return getAttributes() - .getManagerById(discoManager)! - .getDiscoInfo(await _getNode()); + .getManagerById(discoManager)! + .getDiscoInfo(await _getNode()); } Future> _prePresenceSent() async { @@ -123,20 +134,22 @@ class EntityCapabilitiesManager extends XmppManagerBase { ), ]; } - + @override Future postRegisterCallback() async { await super.postRegisterCallback(); - - getAttributes().getManagerById(discoManager)!.registerInfoCallback( - await _getNode(), - _onInfoQuery, - ); getAttributes() - .getManagerById(presenceManager)! - .registerPreSendCallback( - _prePresenceSent, - ); + .getManagerById(discoManager)! + .registerInfoCallback( + await _getNode(), + _onInfoQuery, + ); + + getAttributes() + .getManagerById(presenceManager)! + .registerPreSendCallback( + _prePresenceSent, + ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0184.dart b/packages/moxxmpp/lib/src/xeps/xep_0184.dart index de55091..5a562d2 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0184.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0184.dart @@ -19,7 +19,7 @@ XMLNode makeMessageDeliveryResponse(String id) { return XMLNode.xmlns( tag: 'received', xmlns: deliveryXmlns, - attributes: { 'id': id }, + attributes: {'id': id}, ); } @@ -27,40 +27,49 @@ class MessageDeliveryReceiptManager extends XmppManagerBase { MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager); @override - List getDiscoFeatures() => [ deliveryXmlns ]; + List getDiscoFeatures() => [deliveryXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'received', - tagXmlns: deliveryXmlns, - callback: _onDeliveryReceiptReceived, - // Before the message handler - priority: -99, - ), - StanzaHandler( - stanzaTag: 'message', - tagName: 'request', - tagXmlns: deliveryXmlns, - callback: _onDeliveryRequestReceived, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'received', + tagXmlns: deliveryXmlns, + callback: _onDeliveryReceiptReceived, + // Before the message handler + priority: -99, + ), + StanzaHandler( + stanzaTag: 'message', + tagName: 'request', + tagXmlns: deliveryXmlns, + callback: _onDeliveryRequestReceived, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onDeliveryRequestReceived(Stanza message, StanzaHandlerData state) async { + + Future _onDeliveryRequestReceived( + Stanza message, + StanzaHandlerData state, + ) async { return state.copyWith(deliveryReceiptRequested: true); } - - Future _onDeliveryReceiptReceived(Stanza message, StanzaHandlerData state) async { + + Future _onDeliveryReceiptReceived( + Stanza message, + StanzaHandlerData state, + ) async { final received = message.firstTag('received', xmlns: deliveryXmlns)!; for (final item in message.children) { - if (!['origin-id', 'stanza-id', 'delay', 'store', 'received'].contains(item.tag)) { - logger.info("Won't handle stanza as delivery receipt because we found an '${item.tag}' element"); + if (!['origin-id', 'stanza-id', 'delay', 'store', 'received'] + .contains(item.tag)) { + logger.info( + "Won't handle stanza as delivery receipt because we found an '${item.tag}' element", + ); return state.copyWith(done: true); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0191.dart b/packages/moxxmpp/lib/src/xeps/xep_0191.dart index 5a14403..e8a96e5 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0191.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0191.dart @@ -16,19 +16,19 @@ class BlockingManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'iq', - tagName: 'unblock', - tagXmlns: blockingXmlns, - callback: _unblockPush, - ), - StanzaHandler( - stanzaTag: 'iq', - tagName: 'block', - tagXmlns: blockingXmlns, - callback: _blockPush, - ) - ]; + StanzaHandler( + stanzaTag: 'iq', + tagName: 'unblock', + tagXmlns: blockingXmlns, + callback: _unblockPush, + ), + StanzaHandler( + stanzaTag: 'iq', + tagName: 'block', + tagXmlns: blockingXmlns, + callback: _blockPush, + ) + ]; @override Future isSupported() async { @@ -54,20 +54,29 @@ class BlockingManager extends XmppManagerBase { } } } - - Future _blockPush(Stanza iq, StanzaHandlerData state) async { + + Future _blockPush( + Stanza iq, + StanzaHandlerData state, + ) async { final block = iq.firstTag('block', xmlns: blockingXmlns)!; getAttributes().sendEvent( BlocklistBlockPushEvent( - items: block.findTags('item').map((i) => i.attributes['jid']! as String).toList(), + items: block + .findTags('item') + .map((i) => i.attributes['jid']! as String) + .toList(), ), ); return state.copyWith(done: true); } - Future _unblockPush(Stanza iq, StanzaHandlerData state) async { + Future _unblockPush( + Stanza iq, + StanzaHandlerData state, + ) async { final unblock = iq.firstTag('unblock', xmlns: blockingXmlns)!; final items = unblock.findTags('item'); @@ -85,7 +94,7 @@ class BlockingManager extends XmppManagerBase { return state.copyWith(done: true); } - + Future block(List items) async { final result = await getAttributes().sendStanza( Stanza.iq( @@ -94,14 +103,12 @@ class BlockingManager extends XmppManagerBase { XMLNode.xmlns( tag: 'block', xmlns: blockingXmlns, - children: items - .map((item) { - return XMLNode( - tag: 'item', - attributes: { 'jid': item }, - ); - }) - .toList(), + children: items.map((item) { + return XMLNode( + tag: 'item', + attributes: {'jid': item}, + ); + }).toList(), ) ], ), @@ -125,7 +132,7 @@ class BlockingManager extends XmppManagerBase { return result.attributes['type'] == 'result'; } - + Future unblock(List items) async { assert(items.isNotEmpty, 'The list of items to unblock must be non-empty'); @@ -136,10 +143,14 @@ class BlockingManager extends XmppManagerBase { XMLNode.xmlns( tag: 'unblock', xmlns: blockingXmlns, - children: items.map((item) => XMLNode( - tag: 'item', - attributes: { 'jid': item }, - ),).toList(), + children: items + .map( + (item) => XMLNode( + tag: 'item', + attributes: {'jid': item}, + ), + ) + .toList(), ) ], ), @@ -162,6 +173,9 @@ class BlockingManager extends XmppManagerBase { ); final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!; - return blocklist.findTags('item').map((item) => item.attributes['jid']! as String).toList(); + return blocklist + .findTags('item') + .map((item) => item.attributes['jid']! as String) + .toList(); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 9f96b7d..274f345 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -25,16 +25,16 @@ enum _StreamManagementNegotiatorState { /// is wanted. class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { StreamManagementNegotiator() - : _state = _StreamManagementNegotiatorState.ready, - _supported = false, - _resumeFailed = false, - _isResumed = false, - _log = Logger('StreamManagementNegotiator'), - super(10, false, smXmlns, streamManagementNegotiator); + : _state = _StreamManagementNegotiatorState.ready, + _supported = false, + _resumeFailed = false, + _isResumed = false, + _log = Logger('StreamManagementNegotiator'), + super(10, false, smXmlns, streamManagementNegotiator); _StreamManagementNegotiatorState _state; bool _resumeFailed; bool _isResumed; - + final Logger _log; /// True if Stream Management is supported on this stream. @@ -43,7 +43,7 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { /// True if the current stream is resumed. False if not. bool get isResumed => _isResumed; - + @override bool matchesFeature(List features) { final sm = attributes.getManagerById(smManager)!; @@ -54,25 +54,32 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { } else { // We cannot do a stream resumption final br = attributes.getNegotiatorById(resourceBindingNegotiator); - return super.matchesFeature(features) && br?.state == NegotiatorState.done && attributes.isAuthenticated(); + return super.matchesFeature(features) && + br?.state == NegotiatorState.done && + attributes.isAuthenticated(); } } - + @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { // negotiate is only called when we matched the stream feature, so we know // that the server advertises it. _supported = true; switch (_state) { case _StreamManagementNegotiatorState.ready: - final sm = attributes.getManagerById(smManager)!; + final sm = + attributes.getManagerById(smManager)!; final srid = sm.state.streamResumptionId; final h = sm.state.s2c; // Attempt stream resumption first if (srid != null) { - _log.finest('Found stream resumption Id. Attempting to perform stream resumption'); + _log.finest( + 'Found stream resumption Id. Attempting to perform stream resumption', + ); _state = _StreamManagementNegotiatorState.resumeRequested; attributes.sendNonza(StreamManagementResumeNonza(srid, h)); } else { @@ -82,46 +89,53 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { } return const Result(NegotiatorState.ready); - case _StreamManagementNegotiatorState.resumeRequested: - if (nonza.tag == 'resumed') { - _log.finest('Stream Management resumption successful'); + case _StreamManagementNegotiatorState.resumeRequested: + if (nonza.tag == 'resumed') { + _log.finest('Stream Management resumption successful'); - assert(attributes.getFullJID().resource != '', 'Resume only works when we already have a resource bound and know about it'); + assert( + attributes.getFullJID().resource != '', + 'Resume only works when we already have a resource bound and know about it', + ); - final csi = attributes.getManagerById(csiManager) as CSIManager?; - if (csi != null) { - csi.restoreCSIState(); - } - - final h = int.parse(nonza.attributes['h']! as String); - await attributes.sendEvent(StreamResumedEvent(h: h)); - - _resumeFailed = false; - _isResumed = true; - return const Result(NegotiatorState.skipRest); - } else { - // We assume it is - _log.info('Stream resumption failed. Expected , got ${nonza.tag}, Proceeding with new stream...'); - await attributes.sendEvent(StreamResumeFailedEvent()); - final sm = attributes.getManagerById(smManager)!; - - // We have to do this because we otherwise get a stanza stuck in the queue, - // thus spamming the server on every nonza we receive. - // ignore: cascade_invocations - await sm.setState(StreamManagementState(0, 0)); - await sm.commitState(); - - _resumeFailed = true; - _isResumed = false; - _state = _StreamManagementNegotiatorState.ready; - return const Result(NegotiatorState.retryLater); + final csi = attributes.getManagerById(csiManager) as CSIManager?; + if (csi != null) { + csi.restoreCSIState(); } + + final h = int.parse(nonza.attributes['h']! as String); + await attributes.sendEvent(StreamResumedEvent(h: h)); + + _resumeFailed = false; + _isResumed = true; + return const Result(NegotiatorState.skipRest); + } else { + // We assume it is + _log.info( + 'Stream resumption failed. Expected , got ${nonza.tag}, Proceeding with new stream...', + ); + await attributes.sendEvent(StreamResumeFailedEvent()); + final sm = + attributes.getManagerById(smManager)!; + + // We have to do this because we otherwise get a stanza stuck in the queue, + // thus spamming the server on every nonza we receive. + // ignore: cascade_invocations + await sm.setState(StreamManagementState(0, 0)); + await sm.commitState(); + + _resumeFailed = true; + _isResumed = false; + _state = _StreamManagementNegotiatorState.ready; + return const Result(NegotiatorState.retryLater); + } case _StreamManagementNegotiatorState.enableRequested: if (nonza.tag == 'enabled') { _log.finest('Stream Management enabled'); final id = nonza.attributes['id'] as String?; - if (id != null && ['true', '1'].contains(nonza.attributes['resume'])) { + if (id != null && + ['true', '1'].contains(nonza.attributes['resume'])) { _log.info('Stream Resumption available'); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/nonzas.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/nonzas.dart index 0639ba2..fda84a1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/nonzas.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/nonzas.dart @@ -2,41 +2,39 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; class StreamManagementEnableNonza extends XMLNode { - StreamManagementEnableNonza() : super( - tag: 'enable', - attributes: { - 'xmlns': smXmlns, - 'resume': 'true' - }, - ); + StreamManagementEnableNonza() + : super( + tag: 'enable', + attributes: {'xmlns': smXmlns, 'resume': 'true'}, + ); } class StreamManagementResumeNonza extends XMLNode { - StreamManagementResumeNonza(String id, int h) : super( - tag: 'resume', - attributes: { - 'xmlns': smXmlns, - 'previd': id, - 'h': h.toString() - }, - ); + StreamManagementResumeNonza(String id, int h) + : super( + tag: 'resume', + attributes: { + 'xmlns': smXmlns, + 'previd': id, + 'h': h.toString() + }, + ); } class StreamManagementAckNonza extends XMLNode { - StreamManagementAckNonza(int h) : super( - tag: 'a', - attributes: { - 'xmlns': smXmlns, - 'h': h.toString() - }, - ); + StreamManagementAckNonza(int h) + : super( + tag: 'a', + attributes: {'xmlns': smXmlns, 'h': h.toString()}, + ); } class StreamManagementRequestNonza extends XMLNode { - StreamManagementRequestNonza() : super( - tag: 'r', - attributes: { - 'xmlns': smXmlns, - }, - ); + StreamManagementRequestNonza() + : super( + tag: 'r', + attributes: { + 'xmlns': smXmlns, + }, + ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/state.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/state.dart index 4ddcec2..4df8158 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/state.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/state.dart @@ -7,13 +7,12 @@ part 'state.g.dart'; class StreamManagementState with _$StreamManagementState { factory StreamManagementState( int c2s, - int s2c, - { - String? streamResumptionLocation, - String? streamResumptionId, - } - ) = _StreamManagementState; + int s2c, { + String? streamResumptionLocation, + String? streamResumptionId, + }) = _StreamManagementState; // JSON - factory StreamManagementState.fromJson(Map json) => _$StreamManagementStateFromJson(json); + factory StreamManagementState.fromJson(Map json) => + _$StreamManagementStateFromJson(json); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart index 68d0e6e..88d6e16 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart @@ -80,12 +80,16 @@ class StreamManagementManager extends XmppManagerBase { bool shouldTriggerAckedEvent(Stanza stanza) { return false; } - + @override Future isSupported() async { - return getAttributes().getNegotiatorById(streamManagementNegotiator)!.isSupported; + return getAttributes() + .getNegotiatorById( + streamManagementNegotiator, + )! + .isSupported; } - + /// Returns the amount of stanzas waiting to get acked int getUnackedStanzaCount() => _unackedStanzas.length; @@ -117,40 +121,40 @@ class StreamManagementManager extends XmppManagerBase { _pendingAcks = 0; }); } - + StreamManagementState get state => _state; bool get streamResumed => _streamResumed; @override List getNonzaHandlers() => [ - NonzaHandler( - nonzaTag: 'r', - nonzaXmlns: smXmlns, - callback: _handleAckRequest, - ), - NonzaHandler( - nonzaTag: 'a', - nonzaXmlns: smXmlns, - callback: _handleAckResponse, - ) - ]; + NonzaHandler( + nonzaTag: 'r', + nonzaXmlns: smXmlns, + callback: _handleAckRequest, + ), + NonzaHandler( + nonzaTag: 'a', + nonzaXmlns: smXmlns, + callback: _handleAckResponse, + ) + ]; @override List getIncomingPreStanzaHandlers() => [ - StanzaHandler( - callback: _onServerStanzaReceived, - priority: 9999, - ) - ]; + StanzaHandler( + callback: _onServerStanzaReceived, + priority: 9999, + ) + ]; @override List getOutgoingPostStanzaHandlers() => [ - StanzaHandler( - callback: _onClientStanzaSent, - ) - ]; - + StanzaHandler( + callback: _onClientStanzaSent, + ) + ]; + @override Future onXmppEvent(XmppEvent event) async { if (event is StreamResumedEvent) { @@ -181,17 +185,17 @@ class StreamManagementManager extends XmppManagerBase { _streamResumed = false; } else if (event is ConnectionStateChangedEvent) { switch (event.state) { - case XmppConnectionState.connected: - // Push out all pending stanzas - await onStreamResumed(0); - break; - case XmppConnectionState.error: - case XmppConnectionState.notConnected: - _stopAckTimer(); - break; - case XmppConnectionState.connecting: - // NOOP - break; + case XmppConnectionState.connected: + // Push out all pending stanzas + await onStreamResumed(0); + break; + case XmppConnectionState.error: + case XmppConnectionState.notConnected: + _stopAckTimer(); + break; + case XmppConnectionState.connecting: + // NOOP + break; } } } @@ -223,7 +227,8 @@ class StreamManagementManager extends XmppManagerBase { _ackLock.synchronized(() async { final now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastAckTimestamp >= ackTimeout.inMilliseconds && _pendingAcks > 0) { + if (now - _lastAckTimestamp >= ackTimeout.inMilliseconds && + _pendingAcks > 0) { _stopAckTimer(); await getAttributes().getConnection().reconnectionPolicy.onFailure(); } @@ -242,13 +247,13 @@ class StreamManagementManager extends XmppManagerBase { _startAckTimer(); logger.fine('_pendingAcks is now at $_pendingAcks'); - + getAttributes().sendNonza(StreamManagementRequestNonza()); - + logger.fine('_sendAckRequest: Releasing lock...'); - }); + }); } - + /// Resets the enablement of stream management, but __NOT__ the internal state. /// This is to prevent ack requests being sent before we resume or re-enable /// stream management. @@ -256,13 +261,13 @@ class StreamManagementManager extends XmppManagerBase { _streamManagementEnabled = false; logger.finest('Stream Management disabled'); } - + /// Enables support for XEP-0198 stream management void _enableStreamManagement() { _streamManagementEnabled = true; logger.finest('Stream Management enabled'); } - + /// Returns whether XEP-0198 stream management is enabled bool isStreamManagementEnabled() => _streamManagementEnabled; @@ -295,42 +300,44 @@ class StreamManagementManager extends XmppManagerBase { logger.fine('_pendingAcks is now at $_pendingAcks'); }); }); - + // Return early if we acked nothing. // Taken from slixmpp's stream management code logger.fine('_handleAckResponse: Waiting to aquire lock...'); await _stateLock.synchronized(() async { - logger.fine('_handleAckResponse: Done...'); - if (h == _state.c2s && _unackedStanzas.isEmpty) { - logger.fine('_handleAckResponse: Releasing lock...'); - return; - } - - final attrs = getAttributes(); - final sequences = _unackedStanzas.keys.toList()..sort(); - for (final height in sequences) { - // Do nothing if the ack does not concern this stanza - if (height > h) continue; - - final stanza = _unackedStanzas[height]!; - _unackedStanzas.remove(height); - - // Create a StanzaAckedEvent if the stanza is correct - if (shouldTriggerAckedEvent(stanza)) { - attrs.sendEvent(StanzaAckedEvent(stanza)); - } - } - - if (h > _state.c2s) { - logger.info('C2S height jumped from ${_state.c2s} (local) to $h (remote).'); - // ignore: cascade_invocations - logger.info('Proceeding with $h as local C2S counter.'); - - _state = _state.copyWith(c2s: h); - await commitState(); - } - + logger.fine('_handleAckResponse: Done...'); + if (h == _state.c2s && _unackedStanzas.isEmpty) { logger.fine('_handleAckResponse: Releasing lock...'); + return; + } + + final attrs = getAttributes(); + final sequences = _unackedStanzas.keys.toList()..sort(); + for (final height in sequences) { + // Do nothing if the ack does not concern this stanza + if (height > h) continue; + + final stanza = _unackedStanzas[height]!; + _unackedStanzas.remove(height); + + // Create a StanzaAckedEvent if the stanza is correct + if (shouldTriggerAckedEvent(stanza)) { + attrs.sendEvent(StanzaAckedEvent(stanza)); + } + } + + if (h > _state.c2s) { + logger.info( + 'C2S height jumped from ${_state.c2s} (local) to $h (remote).', + ); + // ignore: cascade_invocations + logger.info('Proceeding with $h as local C2S counter.'); + + _state = _state.copyWith(c2s: h); + await commitState(); + } + + logger.fine('_handleAckResponse: Releasing lock...'); }); return true; @@ -340,33 +347,40 @@ class StreamManagementManager extends XmppManagerBase { Future _incrementC2S() async { logger.fine('_incrementC2S: Waiting to aquire lock...'); await _stateLock.synchronized(() async { - logger.fine('_incrementC2S: Done'); - _state = _state.copyWith(c2s: _state.c2s + 1 % xmlUintMax); - await commitState(); - logger.fine('_incrementC2S: Releasing lock...'); + logger.fine('_incrementC2S: Done'); + _state = _state.copyWith(c2s: _state.c2s + 1 % xmlUintMax); + await commitState(); + logger.fine('_incrementC2S: Releasing lock...'); }); } + Future _incrementS2C() async { logger.fine('_incrementS2C: Waiting to aquire lock...'); await _stateLock.synchronized(() async { - logger.fine('_incrementS2C: Done'); - _state = _state.copyWith(s2c: _state.s2c + 1 % xmlUintMax); - await commitState(); - logger.fine('_incrementS2C: Releasing lock...'); + logger.fine('_incrementS2C: Done'); + _state = _state.copyWith(s2c: _state.s2c + 1 % xmlUintMax); + await commitState(); + logger.fine('_incrementS2C: Releasing lock...'); }); } - + /// Called whenever we receive a stanza from the server. - Future _onServerStanzaReceived(Stanza stanza, StanzaHandlerData state) async { + Future _onServerStanzaReceived( + Stanza stanza, + StanzaHandlerData state, + ) async { await _incrementS2C(); return state; } /// Called whenever we send a stanza. - Future _onClientStanzaSent(Stanza stanza, StanzaHandlerData state) async { + Future _onClientStanzaSent( + Stanza stanza, + StanzaHandlerData state, + ) async { await _incrementC2S(); _unackedStanzas[_state.c2s] = stanza; - + if (isStreamManagementEnabled()) { await _sendAckRequest(); } @@ -382,7 +396,7 @@ class StreamManagementManager extends XmppManagerBase { final stanzas = _unackedStanzas.values.toList(); _unackedStanzas.clear(); - + // Retransmit the rest of the queue final attrs = getAttributes(); for (final stanza in stanzas) { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0203.dart b/packages/moxxmpp/lib/src/xeps/xep_0203.dart index c453da8..cc33598 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0203.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0203.dart @@ -21,14 +21,17 @@ class DelayedDeliveryManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - callback: _onIncomingMessage, - priority: 200, - ), - ]; + StanzaHandler( + stanzaTag: 'message', + callback: _onIncomingMessage, + priority: 200, + ), + ]; - Future _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { + Future _onIncomingMessage( + Stanza stanza, + StanzaHandlerData state, + ) async { final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); if (delay == null) return state; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0280.dart b/packages/moxxmpp/lib/src/xeps/xep_0280.dart index f711bec..5a2ad78 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0280.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0280.dart @@ -24,24 +24,24 @@ class CarbonsManager extends XmppManagerBase { /// Indicates that we know that [CarbonsManager._supported] is accurate. bool _gotSupported = false; - + @override List getIncomingPreStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'received', - tagXmlns: carbonsXmlns, - callback: _onMessageReceived, - priority: -98, - ), - StanzaHandler( - stanzaTag: 'message', - tagName: 'sent', - tagXmlns: carbonsXmlns, - callback: _onMessageSent, - priority: -98, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'received', + tagXmlns: carbonsXmlns, + callback: _onMessageReceived, + priority: -98, + ), + StanzaHandler( + stanzaTag: 'message', + tagName: 'sent', + tagXmlns: carbonsXmlns, + callback: _onMessageSent, + priority: -98, + ) + ]; @override Future isSupported() async { @@ -68,8 +68,11 @@ class CarbonsManager extends XmppManagerBase { } } } - - Future _onMessageReceived(Stanza message, StanzaHandlerData state) async { + + Future _onMessageReceived( + Stanza message, + StanzaHandlerData state, + ) async { final from = JID.fromString(message.attributes['from']! as String); final received = message.firstTag('received', xmlns: carbonsXmlns)!; if (!isCarbonValid(from)) return state.copyWith(done: true); @@ -83,7 +86,10 @@ class CarbonsManager extends XmppManagerBase { ); } - Future _onMessageSent(Stanza message, StanzaHandlerData state) async { + Future _onMessageSent( + Stanza message, + StanzaHandlerData state, + ) async { final from = JID.fromString(message.attributes['from']! as String); final sent = message.firstTag('sent', xmlns: carbonsXmlns)!; if (!isCarbonValid(from)) return state.copyWith(done: true); @@ -154,14 +160,14 @@ class CarbonsManager extends XmppManagerBase { } logger.fine('Successfully disabled message carbons'); - + _isEnabled = false; return true; } /// True if Message Carbons are enabled. False, if not. bool get isEnabled => _isEnabled; - + @visibleForTesting void forceEnable() { _isEnabled = true; @@ -172,9 +178,10 @@ class CarbonsManager extends XmppManagerBase { /// /// Returns true if the carbon is valid. Returns false if not. bool isCarbonValid(JID senderJid) { - return _isEnabled && getAttributes().getFullJID().bareCompare( - senderJid, - ensureBare: true, - ); + return _isEnabled && + getAttributes().getFullJID().bareCompare( + senderJid, + ensureBare: true, + ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0297.dart b/packages/moxxmpp/lib/src/xeps/xep_0297.dart index 4d060d1..72458c9 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0297.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0297.dart @@ -4,7 +4,10 @@ import 'package:moxxmpp/src/stringxml.dart'; /// Extracts the message stanza from the node. Stanza unpackForwarded(XMLNode forwarded) { - assert(forwarded.attributes['xmlns'] == forwardedXmlns, 'Invalid element xmlns'); + assert( + forwarded.attributes['xmlns'] == forwardedXmlns, + 'Invalid element xmlns', + ); assert(forwarded.tag == 'forwarded', 'Invalid element name'); // NOTE: We only use this XEP (for now) in the context of Message Carbons diff --git a/packages/moxxmpp/lib/src/xeps/xep_0300.dart b/packages/moxxmpp/lib/src/xeps/xep_0300.dart index 51526fe..624cff7 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0300.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0300.dart @@ -8,7 +8,7 @@ XMLNode constructHashElement(String algo, String base64Hash) { return XMLNode.xmlns( tag: 'hash', xmlns: hashXmlns, - attributes: { 'algo': algo }, + attributes: {'algo': algo}, text: base64Hash, ); } @@ -68,15 +68,18 @@ class CryptographicHashManager extends XmppManagerBase { @override List getDiscoFeatures() => [ - '$hashFunctionNameBaseXmlns:$hashSha256', - '$hashFunctionNameBaseXmlns:$hashSha512', - //'$hashFunctionNameBaseXmlns:$hashSha3256', - //'$hashFunctionNameBaseXmlns:$hashSha3512', - //'$hashFunctionNameBaseXmlns:$hashBlake2b256', - '$hashFunctionNameBaseXmlns:$hashBlake2b512', - ]; + '$hashFunctionNameBaseXmlns:$hashSha256', + '$hashFunctionNameBaseXmlns:$hashSha512', + //'$hashFunctionNameBaseXmlns:$hashSha3256', + //'$hashFunctionNameBaseXmlns:$hashSha3512', + //'$hashFunctionNameBaseXmlns:$hashBlake2b256', + '$hashFunctionNameBaseXmlns:$hashBlake2b512', + ]; - static Future> hashFromData(List data, HashFunction function) async { + static Future> hashFromData( + List data, + HashFunction function, + ) async { // TODO(PapaTutuWawa): Implement the others as well HashAlgorithm algo; switch (function) { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0308.dart b/packages/moxxmpp/lib/src/xeps/xep_0308.dart index e53a1d0..2cd9aa7 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0308.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0308.dart @@ -20,24 +20,27 @@ class LastMessageCorrectionManager extends XmppManagerBase { LastMessageCorrectionManager() : super(lastMessageCorrectionManager); @override - List getDiscoFeatures() => [ lmcXmlns ]; - + List getDiscoFeatures() => [lmcXmlns]; + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'replace', - tagXmlns: lmcXmlns, - callback: _onMessage, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'replace', + tagXmlns: lmcXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza stanza, StanzaHandlerData state) async { + + Future _onMessage( + Stanza stanza, + StanzaHandlerData state, + ) async { final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!; return state.copyWith( lastMessageCorrectionSid: edit.attributes['id']! as String, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0333.dart b/packages/moxxmpp/lib/src/xeps/xep_0333.dart index de269cb..76ae8e3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0333.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0333.dart @@ -16,11 +16,14 @@ XMLNode makeChatMarkerMarkable() { } XMLNode makeChatMarker(String tag, String id) { - assert(['received', 'displayed', 'acknowledged'].contains(tag), 'Invalid chat marker'); + assert( + ['received', 'displayed', 'acknowledged'].contains(tag), + 'Invalid chat marker', + ); return XMLNode.xmlns( tag: tag, xmlns: chatMarkersXmlns, - attributes: { 'id': id }, + attributes: {'id': id}, ); } @@ -28,36 +31,41 @@ class ChatMarkerManager extends XmppManagerBase { ChatMarkerManager() : super(chatMarkerManager); @override - List getDiscoFeatures() => [ chatMarkersXmlns ]; + List getDiscoFeatures() => [chatMarkersXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagXmlns: chatMarkersXmlns, - callback: _onMessage, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagXmlns: chatMarkersXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final marker = message.firstTagByXmlns(chatMarkersXmlns)!; // Handle the explicitly if (marker.tag == 'markable') return state.copyWith(isMarkable: true); - + if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) { logger.warning("Unknown message marker '${marker.tag}' found."); } else { - getAttributes().sendEvent(ChatMarkerEvent( + getAttributes().sendEvent( + ChatMarkerEvent( from: JID.fromString(message.from!), type: marker.tag, id: marker.attributes['id']! as String, - ),); + ), + ); } return state.copyWith(done: true); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0334.dart b/packages/moxxmpp/lib/src/xeps/xep_0334.dart index 3b7853f..57b768e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0334.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0334.dart @@ -10,12 +10,16 @@ enum MessageProcessingHint { MessageProcessingHint messageProcessingHintFromXml(XMLNode element) { switch (element.tag) { - case 'no-permanent-store': return MessageProcessingHint.noPermanentStore; - case 'no-store': return MessageProcessingHint.noStore; - case 'no-copy': return MessageProcessingHint.noCopies; - case 'store': return MessageProcessingHint.store; + case 'no-permanent-store': + return MessageProcessingHint.noPermanentStore; + case 'no-store': + return MessageProcessingHint.noStore; + case 'no-copy': + return MessageProcessingHint.noCopies; + case 'store': + return MessageProcessingHint.store; } - + assert(false, 'Invalid Message Processing Hint: ${element.tag}'); return MessageProcessingHint.noStore; } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0352.dart b/packages/moxxmpp/lib/src/xeps/xep_0352.dart index 7dc6833..6ac38c4 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0352.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0352.dart @@ -7,21 +7,19 @@ import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; class CSIActiveNonza extends XMLNode { - CSIActiveNonza() : super( - tag: 'active', - attributes: { - 'xmlns': csiXmlns - }, - ); + CSIActiveNonza() + : super( + tag: 'active', + attributes: {'xmlns': csiXmlns}, + ); } class CSIInactiveNonza extends XMLNode { - CSIInactiveNonza() : super( - tag: 'inactive', - attributes: { - 'xmlns': csiXmlns - }, - ); + CSIInactiveNonza() + : super( + tag: 'inactive', + attributes: {'xmlns': csiXmlns}, + ); } /// A Stub negotiator that is just for "intercepting" the stream feature. @@ -31,9 +29,11 @@ class CSINegotiator extends XmppFeatureNegotiatorBase { /// True if CSI is supported. False otherwise. bool _supported = false; bool get isSupported => _supported; - + @override - Future> negotiate(XMLNode nonza) async { + Future> negotiate( + XMLNode nonza, + ) async { // negotiate is only called when the negotiator matched, meaning the server // advertises CSI. _supported = true; @@ -56,9 +56,11 @@ class CSIManager extends XmppManagerBase { @override Future isSupported() async { - return getAttributes().getNegotiatorById(csiNegotiator)!.isSupported; + return getAttributes() + .getNegotiatorById(csiNegotiator)! + .isSupported; } - + /// To be called after a stream has been resumed as CSI does not /// survive a stream resumption. void restoreCSIState() { @@ -68,7 +70,7 @@ class CSIManager extends XmppManagerBase { setInactive(); } } - + /// Tells the server to top optimizing traffic Future setActive() async { _isActive = true; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0359.dart b/packages/moxxmpp/lib/src/xeps/xep_0359.dart index b97cd84..21c10e3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0359.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0359.dart @@ -13,7 +13,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; /// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of /// the message stanza. class StableStanzaId { - const StableStanzaId({ this.originId, this.stanzaId, this.stanzaIdBy }); + const StableStanzaId({this.originId, this.stanzaId, this.stanzaIdBy}); final String? originId; final String? stanzaId; final String? stanzaIdBy; @@ -23,7 +23,7 @@ XMLNode makeOriginIdElement(String id) { return XMLNode.xmlns( tag: 'origin-id', xmlns: stableIdXmlns, - attributes: { 'id': id }, + attributes: {'id': id}, ); } @@ -31,22 +31,25 @@ class StableIdManager extends XmppManagerBase { StableIdManager() : super(stableIdManager); @override - List getDiscoFeatures() => [ stableIdXmlns ]; + List getDiscoFeatures() => [stableIdXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - callback: _onMessage, - // Before the MessageManager - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + callback: _onMessage, + // Before the MessageManager + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final from = JID.fromString(message.attributes['from']! as String); String? originId; String? stanzaId; @@ -74,10 +77,14 @@ class StableIdManager extends XmppManagerBase { stanzaId = stanzaIdTag.attributes['id']! as String; stanzaIdBy = stanzaIdTag.attributes['by']! as String; } else { - logger.finest('${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... '); + logger.finest( + '${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... ', + ); } } else { - logger.finest('Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... '); + logger.finest( + 'Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... ', + ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0363/xep_0363.dart b/packages/moxxmpp/lib/src/xeps/xep_0363/xep_0363.dart index df04a57..541b924 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0363/xep_0363.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0363/xep_0363.dart @@ -13,7 +13,7 @@ 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_0363/errors.dart'; -const allowedHTTPHeaders = [ 'authorization', 'cookie', 'expires' ]; +const allowedHTTPHeaders = ['authorization', 'cookie', 'expires']; class HttpFileUploadSlot { const HttpFileUploadSlot(this.putUrl, this.getUrl, this.headers); @@ -32,12 +32,12 @@ String _stripNewlinesFromString(String value) { @visibleForTesting Map prepareHeaders(Map headers) { return headers.map((key, value) { - return MapEntry( - _stripNewlinesFromString(key), - _stripNewlinesFromString(value), - ); + return MapEntry( + _stripNewlinesFromString(key), + _stripNewlinesFromString(value), + ); }) - ..removeWhere((key, _) => !allowedHTTPHeaders.contains(key.toLowerCase())); + ..removeWhere((key, _) => !allowedHTTPHeaders.contains(key.toLowerCase())); } class HttpFileUploadManager extends XmppManagerBase { @@ -58,7 +58,10 @@ class HttpFileUploadManager extends XmppManagerBase { /// Returns whether the entity provided an identity that tells us that we can ask it /// for an HTTP upload slot. bool _containsFileUploadIdentity(DiscoInfo info) { - return listContains(info.identities, (Identity id) => id.category == 'store' && id.type == 'file'); + return listContains( + info.identities, + (Identity id) => id.category == 'store' && id.type == 'file', + ); } /// Extract the maximum filesize in octets from the disco response. Returns null @@ -87,12 +90,14 @@ class HttpFileUploadManager extends XmppManagerBase { } } } - + @override Future isSupported() async { if (_gotSupported) return _supported; - - final result = await getAttributes().getManagerById(discoManager)!.performDiscoSweep(); + + final result = await getAttributes() + .getManagerById(discoManager)! + .performDiscoSweep(); if (result.isType()) { _gotSupported = false; _supported = false; @@ -102,8 +107,9 @@ class HttpFileUploadManager extends XmppManagerBase { final infos = result.get>(); _gotSupported = true; for (final info in infos) { - if (_containsFileUploadIdentity(info) && info.features.contains(httpFileUploadXmlns)) { - logger.info('Discovered HTTP File Upload for ${info.jid}'); + if (_containsFileUploadIdentity(info) && + info.features.contains(httpFileUploadXmlns)) { + logger.info('Discovered HTTP File Upload for ${info.jid}'); _entityJid = info.jid; _maxUploadSize = _getMaxFileSize(info); @@ -119,19 +125,29 @@ class HttpFileUploadManager extends XmppManagerBase { /// the file's size in octets. [contentType] is optional and refers to the file's /// Mime type. /// Returns an [HttpFileUploadSlot] if the request was successful; null otherwise. - Future> requestUploadSlot(String filename, int filesize, { String? contentType }) async { - if (!(await isSupported())) return Result(NoEntityKnownError()); + Future> requestUploadSlot( + String filename, + int filesize, { + String? contentType, + }) async { + if (!(await isSupported())) { + return Result(NoEntityKnownError()); + } if (_entityJid == null) { - logger.warning('Attempted to request HTTP File Upload slot but no entity is known to send this request to.'); + logger.warning( + 'Attempted to request HTTP File Upload slot but no entity is known to send this request to.', + ); return Result(NoEntityKnownError()); } if (_maxUploadSize != null && filesize > _maxUploadSize!) { - logger.warning('Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit'); + logger.warning( + 'Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit', + ); return Result(FileTooBigError()); } - + final attrs = getAttributes(); final response = await attrs.sendStanza( Stanza.iq( @@ -144,7 +160,7 @@ class HttpFileUploadManager extends XmppManagerBase { attributes: { 'filename': filename, 'size': filesize.toString(), - ...contentType != null ? { 'content-type': contentType } : {} + ...contentType != null ? {'content-type': contentType} : {} }, ) ], diff --git a/packages/moxxmpp/lib/src/xeps/xep_0380.dart b/packages/moxxmpp/lib/src/xeps/xep_0380.dart index 16236ce..9022596 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0380.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0380.dart @@ -18,25 +18,39 @@ enum ExplicitEncryptionType { String _explicitEncryptionTypeToString(ExplicitEncryptionType type) { switch (type) { - case ExplicitEncryptionType.otr: return emeOtr; - case ExplicitEncryptionType.legacyOpenPGP: return emeLegacyOpenPGP; - case ExplicitEncryptionType.openPGP: return emeOpenPGP; - case ExplicitEncryptionType.omemo: return emeOmemo; - case ExplicitEncryptionType.omemo1: return emeOmemo1; - case ExplicitEncryptionType.omemo2: return emeOmemo2; - case ExplicitEncryptionType.unknown: return ''; + case ExplicitEncryptionType.otr: + return emeOtr; + case ExplicitEncryptionType.legacyOpenPGP: + return emeLegacyOpenPGP; + case ExplicitEncryptionType.openPGP: + return emeOpenPGP; + case ExplicitEncryptionType.omemo: + return emeOmemo; + case ExplicitEncryptionType.omemo1: + return emeOmemo1; + case ExplicitEncryptionType.omemo2: + return emeOmemo2; + case ExplicitEncryptionType.unknown: + return ''; } } ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) { switch (str) { - case emeOtr: return ExplicitEncryptionType.otr; - case emeLegacyOpenPGP: return ExplicitEncryptionType.legacyOpenPGP; - case emeOpenPGP: return ExplicitEncryptionType.openPGP; - case emeOmemo: return ExplicitEncryptionType.omemo; - case emeOmemo1: return ExplicitEncryptionType.omemo1; - case emeOmemo2: return ExplicitEncryptionType.omemo2; - default: return ExplicitEncryptionType.unknown; + case emeOtr: + return ExplicitEncryptionType.otr; + case emeLegacyOpenPGP: + return ExplicitEncryptionType.legacyOpenPGP; + case emeOpenPGP: + return ExplicitEncryptionType.openPGP; + case emeOmemo: + return ExplicitEncryptionType.omemo; + case emeOmemo1: + return ExplicitEncryptionType.omemo1; + case emeOmemo2: + return ExplicitEncryptionType.omemo2; + default: + return ExplicitEncryptionType.unknown; } } @@ -58,20 +72,23 @@ class EmeManager extends XmppManagerBase { Future isSupported() async => true; @override - List getDiscoFeatures() => [ emeXmlns ]; - + List getDiscoFeatures() => [emeXmlns]; + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - tagName: 'encryption', - tagXmlns: emeXmlns, - callback: _onStanzaReceived, - // Before the message handler - priority: -99, - ), - ]; + StanzaHandler( + tagName: 'encryption', + tagXmlns: emeXmlns, + callback: _onStanzaReceived, + // Before the message handler + priority: -99, + ), + ]; - Future _onStanzaReceived(Stanza message, StanzaHandlerData state) async { + Future _onStanzaReceived( + Stanza message, + StanzaHandlerData state, + ) async { final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; return state.copyWith( diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/crypto.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/crypto.dart index f374836..bbe3f35 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/crypto.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/crypto.dart @@ -15,6 +15,7 @@ bool checkAffixElements(XMLNode envelope, String sender, JID ourJid) { if (to == null) return false; final encReceiver = JID.fromString(to); - return encSender.toBare().toString() == JID.fromString(sender).toBare().toString() && - encReceiver.toBare().toString() == ourJid.toBare().toString(); + return encSender.toBare().toString() == + JID.fromString(sender).toBare().toString() && + encReceiver.toBare().toString() == ourJid.toBare().toString(); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/helpers.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/helpers.dart index 1ed902b..f31d7fa 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/helpers.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/helpers.dart @@ -46,7 +46,8 @@ XMLNode bundleToXML(OmemoBundle bundle) { for (final pk in bundle.opksEncoded.entries) { prekeys.add( XMLNode( - tag: 'pk', attributes: { + tag: 'pk', + attributes: { 'id': '${pk.key}', }, text: pk.value, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart index fdfd872..0b31d23 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart @@ -1,6 +1,5 @@ /// A simple wrapper class for defining elements that should not be encrypted. class DoNotEncrypt { - const DoNotEncrypt(this.tag, this.xmlns); final String tag; final String xmlns; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart index 362f570..75eec1e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart @@ -52,42 +52,42 @@ abstract class BaseOmemoManager extends XmppManagerBase { @override List getIncomingPreStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'iq', - tagXmlns: omemoXmlns, - tagName: 'encrypted', - callback: _onIncomingStanza, - ), - StanzaHandler( - stanzaTag: 'presence', - tagXmlns: omemoXmlns, - tagName: 'encrypted', - callback: _onIncomingStanza, - ), - StanzaHandler( - stanzaTag: 'message', - tagXmlns: omemoXmlns, - tagName: 'encrypted', - callback: _onIncomingStanza, - ), - ]; + StanzaHandler( + stanzaTag: 'iq', + tagXmlns: omemoXmlns, + tagName: 'encrypted', + callback: _onIncomingStanza, + ), + StanzaHandler( + stanzaTag: 'presence', + tagXmlns: omemoXmlns, + tagName: 'encrypted', + callback: _onIncomingStanza, + ), + StanzaHandler( + stanzaTag: 'message', + tagXmlns: omemoXmlns, + tagName: 'encrypted', + callback: _onIncomingStanza, + ), + ]; @override List getOutgoingPreStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'iq', - callback: _onOutgoingStanza, - ), - StanzaHandler( - stanzaTag: 'presence', - callback: _onOutgoingStanza, - ), - StanzaHandler( - stanzaTag: 'message', - callback: _onOutgoingStanza, - priority: 100, - ), - ]; + StanzaHandler( + stanzaTag: 'iq', + callback: _onOutgoingStanza, + ), + StanzaHandler( + stanzaTag: 'presence', + callback: _onOutgoingStanza, + ), + StanzaHandler( + stanzaTag: 'message', + callback: _onOutgoingStanza, + priority: 100, + ), + ]; @override Future onXmppEvent(XmppEvent event) async { @@ -98,8 +98,8 @@ abstract class BaseOmemoManager extends XmppManagerBase { final ownJid = getAttributes().getFullJID().toBare().toString(); final jid = JID.fromString(event.from).toBare(); final ids = event.item.payload.children - .map((child) => int.parse(child.attributes['id']! as String)) - .toList(); + .map((child) => int.parse(child.attributes['id']! as String)) + .toList(); if (event.from == ownJid) { // Another client published to our device list node @@ -113,8 +113,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { } // Tell the OmemoManager - (await getOmemoManager()) - .onDeviceListUpdate(jid.toString(), ids); + (await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids); // Generate an event getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); @@ -124,7 +123,6 @@ abstract class BaseOmemoManager extends XmppManagerBase { @visibleForOverriding Future getOmemoManager(); - /// Wrapper around using getSessionManager and then calling getDeviceId on it. Future _getDeviceId() async => (await getOmemoManager()).getDeviceId(); @@ -154,7 +152,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { return true; } - + /// Encrypt [children] using OMEMO. This either produces an element with /// an attached payload, if [children] is not null, or an empty OMEMO message if /// [children] is null. This function takes care of creating the affix elements as @@ -169,7 +167,6 @@ abstract class BaseOmemoManager extends XmppManagerBase { tag: 'content', children: children, ), - XMLNode( tag: 'rpad', text: generateRpad(), @@ -201,7 +198,11 @@ abstract class BaseOmemoManager extends XmppManagerBase { return payload.toXml(); } - XMLNode _buildEncryptedElement(EncryptionResult result, String recipientJid, int deviceId) { + XMLNode _buildEncryptedElement( + EncryptionResult result, + String recipientJid, + int deviceId, + ) { final keyElements = >{}; for (final key in result.encryptedKeys) { final keyElement = XMLNode( @@ -239,7 +240,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { ), ]; } - + return XMLNode.xmlns( tag: 'encrypted', xmlns: omemoXmlns, @@ -257,7 +258,10 @@ abstract class BaseOmemoManager extends XmppManagerBase { } /// For usage with omemo_dart's OmemoManager. - Future sendEmptyMessageImpl(EncryptionResult result, String toJid) async { + Future sendEmptyMessageImpl( + EncryptionResult result, + String toJid, + ) async { await getAttributes().sendStanza( Stanza.message( to: toJid, @@ -285,7 +289,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { final om = await getOmemoManager(); await om.sendOmemoHeartbeat(jid); } - + /// For usage with omemo_dart's OmemoManager Future?> fetchDeviceList(String jid) async { final result = await getDeviceList(JID.fromString(jid)); @@ -301,8 +305,11 @@ abstract class BaseOmemoManager extends XmppManagerBase { return result.get(); } - - Future _onOutgoingStanza(Stanza stanza, StanzaHandlerData state) async { + + Future _onOutgoingStanza( + Stanza stanza, + StanzaHandlerData state, + ) async { if (state.encrypted) { logger.finest('Not encrypting since state.encrypted is true'); return state; @@ -317,10 +324,14 @@ abstract class BaseOmemoManager extends XmppManagerBase { final toJid = JID.fromString(stanza.to!).toBare(); final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza); if (!shouldEncryptResult && !state.forceEncryption) { - logger.finest('Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.'); + logger.finest( + 'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.', + ); return state; } else { - logger.finest('Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}'); + logger.finest( + 'Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}', + ); } final toEncrypt = List.empty(growable: true); @@ -332,17 +343,18 @@ abstract class BaseOmemoManager extends XmppManagerBase { toEncrypt.add(child); } } - + logger.finest('Beginning encryption'); final carbonsEnabled = getAttributes() - .getManagerById(carbonsManager)?.isEnabled ?? false; + .getManagerById(carbonsManager) + ?.isEnabled ?? + false; final om = await getOmemoManager(); final result = await om.onOutgoingStanza( OmemoOutgoingStanza( [ toJid.toString(), - if (carbonsEnabled) - getAttributes().getFullJID().toBare().toString(), + if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(), ], _buildEnvelope(toEncrypt, toJid.toString()), ), @@ -357,20 +369,21 @@ abstract class BaseOmemoManager extends XmppManagerBase { 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(), + cancelReason: result.jidEncryptionErrors[toJid.toString()] + is NoKeyMaterialAvailableException + ? OmemoNotSupportedForContactException() + : UnknownOmemoError(), cancel: true, ); } - + final encrypted = _buildEncryptedElement( result, toJid.toString(), await _getDeviceId(), ); children.add(encrypted); - + // Only add message specific metadata when actually sending a message if (stanza.tag == 'message') { children @@ -381,7 +394,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { // https://xmpp.org/extensions/xep-0384.html#message-structure-description. ..add(MessageProcessingHint.store.toXml()); } - + return state.copyWith( stanza: state.stanza.copyWith( children: children, @@ -395,8 +408,11 @@ abstract class BaseOmemoManager extends XmppManagerBase { /// encrypted. @visibleForOverriding Future shouldEncryptStanza(JID toJid, Stanza stanza); - - Future _onIncomingStanza(Stanza stanza, StanzaHandlerData state) async { + + Future _onIncomingStanza( + Stanza stanza, + StanzaHandlerData state, + ) async { final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns); if (encrypted == null) return state; if (stanza.from == null) return state; @@ -427,7 +443,8 @@ abstract class BaseOmemoManager extends XmppManagerBase { OmemoIncomingStanza( fromJid.toString(), sid, - state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch, + state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? + DateTime.now().millisecondsSinceEpoch, keys, payloadElement?.innerText(), ), @@ -438,9 +455,13 @@ abstract class BaseOmemoManager extends XmppManagerBase { if (result.error != null) { other['encryption_error'] = result.error; } else { - children = stanza.children.where( - (child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns, - ).toList(); + children = stanza.children + .where( + (child) => + child.tag != 'encrypted' || + child.attributes['xmlns'] != omemoXmlns, + ) + .toList(); } if (result.payload != null) { @@ -459,7 +480,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { final envelopeChildren = envelope.firstTag('content')?.children; if (envelopeChildren != null) { children.addAll( - // Do not add forbidden elements from the envelope + // Do not add forbidden elements from the envelope envelopeChildren.where(shouldEncryptElement), ); } else { @@ -490,44 +511,57 @@ abstract class BaseOmemoManager extends XmppManagerBase { /// device list PubSub node. /// /// On success, returns the XML data. On failure, returns an OmemoError. - Future> _retrieveDeviceListPayload(JID jid) async { + Future> _retrieveDeviceListPayload( + JID jid, + ) async { final pm = getAttributes().getManagerById(pubsubManager)!; - final result = await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns); + final result = + await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns); if (result.isType()) return Result(UnknownOmemoError()); return Result(result.get>().first.payload); } - + /// Retrieves the OMEMO device list from [jid]. Future>> getDeviceList(JID jid) async { final itemsRaw = await _retrieveDeviceListPayload(jid); if (itemsRaw.isType()) return Result(UnknownOmemoError()); - final ids = itemsRaw.get().children - .map((child) => int.parse(child.attributes['id']! as String)) - .toList(); + final ids = itemsRaw + .get() + .children + .map((child) => int.parse(child.attributes['id']! as String)) + .toList(); return Result(ids); } /// Retrieve all device bundles for the JID [jid]. /// /// On success, returns a list of devices. On failure, returns am OmemoError. - Future>> retrieveDeviceBundles(JID jid) async { + Future>> retrieveDeviceBundles( + JID jid, + ) async { // TODO(Unknown): Should we query the device list first? final pm = getAttributes().getManagerById(pubsubManager)!; final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); if (bundlesRaw.isType()) return Result(UnknownOmemoError()); - final bundles = bundlesRaw.get>().map( - (bundle) => bundleFromXML(jid, int.parse(bundle.id), bundle.payload), - ).toList(); + final bundles = bundlesRaw + .get>() + .map( + (bundle) => bundleFromXML(jid, int.parse(bundle.id), bundle.payload), + ) + .toList(); return Result(bundles); } - + /// Retrieves a bundle from entity [jid] with the device id [deviceId]. /// /// On success, returns the device bundle. On failure, returns an OmemoError. - Future> retrieveDeviceBundle(JID jid, int deviceId) async { + Future> retrieveDeviceBundle( + JID jid, + int deviceId, + ) async { final pm = getAttributes().getManagerById(pubsubManager)!; final bareJid = jid.toBare().toString(); final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId'); @@ -557,8 +591,8 @@ abstract class BaseOmemoManager extends XmppManagerBase { ); final ids = deviceList.children - .map((child) => int.parse(child.attributes['id']! as String)); - + .map((child) => int.parse(child.attributes['id']! as String)); + if (!ids.contains(bundle.id)) { // Only update the device list if the device Id is not there final newDeviceList = XMLNode.xmlns( @@ -574,7 +608,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { ), ], ); - + final deviceListPublish = await pm.publish( bareJid.toString(), omemoDevicesXmlns, @@ -585,7 +619,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { ), ); if (deviceListPublish.isType()) return const Result(false); - } + } final deviceBundlePublish = await pm.publish( bareJid.toString(), @@ -597,7 +631,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { maxItems: 'max', ), ); - + return Result(deviceBundlePublish.isType()); } @@ -618,7 +652,8 @@ abstract class BaseOmemoManager extends XmppManagerBase { if (items.isType()) return Result(UnknownOmemoError()); final nodes = items.get>(); - final result = nodes.any((item) => item.node == omemoDevicesXmlns) && nodes.any((item) => item.node == omemoBundlesXmlns); + final result = nodes.any((item) => item.node == omemoDevicesXmlns) && + nodes.any((item) => item.node == omemoBundlesXmlns); return Result(result); } @@ -648,8 +683,8 @@ abstract class BaseOmemoManager extends XmppManagerBase { tag: 'devices', xmlns: omemoDevicesXmlns, children: payload.children - .where((child) => child.attributes['id'] != '$deviceId') - .toList(), + .where((child) => child.attributes['id'] != '$deviceId') + .toList(), ); final publishResult = await pm.publish( jid.toString(), diff --git a/packages/moxxmpp/lib/src/xeps/xep_0385.dart b/packages/moxxmpp/lib/src/xeps/xep_0385.dart index 33ef706..fdc6215 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0385.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0385.dart @@ -8,13 +8,20 @@ import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; 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 int size; final String description; final Map hashes; // algo -> hash value final List thumbnails; - + final String url; } @@ -29,7 +36,8 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) { } var url = ''; - final references = file.firstTag('sources')!.findTags('reference', xmlns: referenceXmlns); + final references = + file.firstTag('sources')!.findTags('reference', xmlns: referenceXmlns); for (final i in references) { if (i.attributes['type'] != 'data') continue; @@ -43,14 +51,15 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) { final thumbnails = List.empty(growable: true); for (final child in file.children) { // TODO(Unknown): Handle other thumbnails - if (child.tag == 'file-thumbnail' && child.attributes['xmlns'] == fileThumbnailsXmlns) { + if (child.tag == 'file-thumbnail' && + child.attributes['xmlns'] == fileThumbnailsXmlns) { final thumb = parseFileThumbnailElement(child); if (thumb != null) { thumbnails.add(thumb); } } } - + return StatelessMediaSharingData( mediaType: file.firstTag('media-type')!.innerText(), size: int.parse(file.firstTag('size')!.innerText()), @@ -65,24 +74,27 @@ class SIMSManager extends XmppManagerBase { SIMSManager() : super(simsManager); @override - List getDiscoFeatures() => [ simsXmlns ]; - + List getDiscoFeatures() => [simsXmlns]; + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - callback: _onMessage, - tagName: 'reference', - tagXmlns: referenceXmlns, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + callback: _onMessage, + tagName: 'reference', + tagXmlns: referenceXmlns, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final references = message.findTags('reference', xmlns: referenceXmlns); for (final ref in references) { final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0414.dart b/packages/moxxmpp/lib/src/xeps/xep_0414.dart index 3a41183..4e459b2 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0414.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0414.dart @@ -1,7 +1,6 @@ import 'package:cryptography/cryptography.dart'; class InvalidHashAlgorithmException implements Exception { - InvalidHashAlgorithmException(this.name); final String name; @@ -11,16 +10,20 @@ class InvalidHashAlgorithmException implements Exception { /// Returns the hash algorithm specified by its name, according to XEP-0414. HashAlgorithm? getHashByName(String name) { switch (name) { - case 'sha-1': return Sha1(); - case 'sha-256': return Sha256(); - case 'sha-512': return Sha512(); + case 'sha-1': + return Sha1(); + case 'sha-256': + return Sha256(); + case 'sha-512': + return Sha512(); // NOTE: cryptography provides an implementation of blake2b, however, // I have no idea what it's output length is and you cannot set // one. => New dependency // TODO(Unknown): Implement //case "blake2b-256": ; // hashLengthInBytes == 64 => 512? - case 'blake2b-512': Blake2b(); + case 'blake2b-512': + Blake2b(); // NOTE: cryptography does not provide SHA3 hashes => New dependency // TODO(Unknown): Implement //case "sha3-256": ; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0424.dart b/packages/moxxmpp/lib/src/xeps/xep_0424.dart index 69f513f..a9cfc6f 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0424.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0424.dart @@ -15,22 +15,25 @@ class MessageRetractionManager extends XmppManagerBase { MessageRetractionManager() : super(messageRetractionManager); @override - List getDiscoFeatures() => [ messageRetractionXmlns ]; + List getDiscoFeatures() => [messageRetractionXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - callback: _onMessage, - // Before the MessageManager - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + callback: _onMessage, + // Before the MessageManager + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final applyTo = message.firstTag('apply-to', xmlns: fasteningXmlns); if (applyTo == null) { return state; @@ -41,14 +44,13 @@ class MessageRetractionManager extends XmppManagerBase { return state; } - final isFallbackBody = message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; + final isFallbackBody = + message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; return state.copyWith( messageRetraction: MessageRetractionData( applyTo.attributes['id']! as String, - isFallbackBody ? - message.firstTag('body')?.innerText() : - null, + isFallbackBody ? message.firstTag('body')?.innerText() : null, ), ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0444.dart b/packages/moxxmpp/lib/src/xeps/xep_0444.dart index 240ea92..15448ea 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0444.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0444.dart @@ -32,32 +32,36 @@ class MessageReactionsManager extends XmppManagerBase { MessageReactionsManager() : super(messageReactionsManager); @override - List getDiscoFeatures() => [ messageReactionsXmlns ]; + List getDiscoFeatures() => [messageReactionsXmlns]; @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'reactions', - tagXmlns: messageReactionsXmlns, - callback: _onReactionsReceived, - // Before the message handler - priority: -99, - ), - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'reactions', + tagXmlns: messageReactionsXmlns, + callback: _onReactionsReceived, + // Before the message handler + priority: -99, + ), + ]; @override Future isSupported() async => true; - - Future _onReactionsReceived(Stanza message, StanzaHandlerData state) async { - final reactionsElement = message.firstTag('reactions', xmlns: messageReactionsXmlns)!; + + Future _onReactionsReceived( + Stanza message, + StanzaHandlerData state, + ) async { + final reactionsElement = + message.firstTag('reactions', xmlns: messageReactionsXmlns)!; return state.copyWith( messageReactions: MessageReactions( reactionsElement.attributes['id']! as String, reactionsElement.children - .where((c) => c.tag == 'reaction') - .map((c) => c.innerText()) - .toList(), + .where((c) => c.tag == 'reaction') + .map((c) => c.innerText()) + .toList(), ), ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0446.dart b/packages/moxxmpp/lib/src/xeps/xep_0446.dart index fafc493..c768eaf 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0446.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0446.dart @@ -18,13 +18,18 @@ class FileMetadataData { /// Parse [node] as a FileMetadataData element. factory FileMetadataData.fromXML(XMLNode node) { - assert(node.attributes['xmlns'] == fileMetadataXmlns, 'Invalid element xmlns'); + assert( + node.attributes['xmlns'] == fileMetadataXmlns, + 'Invalid element xmlns', + ); assert(node.tag == 'file', 'Invalid element anme'); final lengthElement = node.firstTag('length'); - final length = lengthElement != null ? int.parse(lengthElement.innerText()) : null; + final length = + lengthElement != null ? int.parse(lengthElement.innerText()) : null; final sizeElement = node.firstTag('size'); - final size = sizeElement != null ? int.parse(sizeElement.innerText()) : null; + final size = + sizeElement != null ? int.parse(sizeElement.innerText()) : null; final hashes = {}; for (final e in node.findTags('hash')) { @@ -51,7 +56,7 @@ class FileMetadataData { if (heightString != null) { height = int.parse(heightString.innerText()); } - + return FileMetadataData( mediaType: node.firstTag('media-type')?.innerText(), width: width, @@ -82,13 +87,27 @@ class FileMetadataData { children: List.empty(growable: true), ); - if (mediaType != null) node.addChild(XMLNode(tag: 'media-type', text: mediaType)); - if (width != null) node.addChild(XMLNode(tag: 'width', text: '$width')); - if (height != null) node.addChild(XMLNode(tag: 'height', text: '$height')); - if (desc != null) node.addChild(XMLNode(tag: 'desc', text: desc)); - if (length != null) node.addChild(XMLNode(tag: 'length', text: length.toString())); - if (name != null) node.addChild(XMLNode(tag: 'name', text: name)); - if (size != null) node.addChild(XMLNode(tag: 'size', text: size.toString())); + if (mediaType != null) { + node.addChild(XMLNode(tag: 'media-type', text: mediaType)); + } + if (width != null) { + node.addChild(XMLNode(tag: 'width', text: '$width')); + } + if (height != null) { + node.addChild(XMLNode(tag: 'height', text: '$height')); + } + if (desc != null) { + node.addChild(XMLNode(tag: 'desc', text: desc)); + } + if (length != null) { + node.addChild(XMLNode(tag: 'length', text: length.toString())); + } + if (name != null) { + node.addChild(XMLNode(tag: 'name', text: name)); + } + if (size != null) { + node.addChild(XMLNode(tag: 'size', text: size.toString())); + } for (final hash in hashes.entries) { node.addChild( @@ -101,7 +120,7 @@ class FileMetadataData { constructFileThumbnailElement(thumbnail), ); } - + return node; } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0447.dart b/packages/moxxmpp/lib/src/xeps/xep_0447.dart index 31c393d..81ee7d8 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0447.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0447.dart @@ -21,9 +21,14 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource { StatelessFileSharingUrlSource(this.url); factory StatelessFileSharingUrlSource.fromXml(XMLNode element) { - assert(element.attributes['xmlns'] == urlDataXmlns, 'Element has the wrong xmlns'); + assert( + element.attributes['xmlns'] == urlDataXmlns, + 'Element has the wrong xmlns', + ); - return StatelessFileSharingUrlSource(element.attributes['target']! as String); + return StatelessFileSharingUrlSource( + element.attributes['target']! as String, + ); } final String url; @@ -44,9 +49,12 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource { /// StatelessFileSharingSources contained with it. /// If [checkXmlns] is true, then the sources element must also have an xmlns attribute /// of "urn:xmpp:sfs:0". -List processStatelessFileSharingSources(XMLNode node, { bool checkXmlns = true }) { +List processStatelessFileSharingSources( + XMLNode node, { + bool checkXmlns = true, +}) { final sources = List.empty(growable: true); - + final sourcesElement = node.firstTag( 'sources', xmlns: checkXmlns ? sfsXmlns : null, @@ -88,9 +96,7 @@ class StatelessFileSharingData { metadata.toXML(), XMLNode( tag: 'sources', - children: sources - .map((source) => source.toXml()) - .toList(), + children: sources.map((source) => source.toXml()).toList(), ), ], ); @@ -99,7 +105,8 @@ class StatelessFileSharingData { StatelessFileSharingUrlSource? getFirstUrlSource() { return firstWhereOrNull( sources, - (StatelessFileSharingSource source) => source is StatelessFileSharingUrlSource, + (StatelessFileSharingSource source) => + source is StatelessFileSharingUrlSource, ) as StatelessFileSharingUrlSource?; } } @@ -109,24 +116,29 @@ class SFSManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'file-sharing', - tagXmlns: sfsXmlns, - callback: _onMessage, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'file-sharing', + tagXmlns: sfsXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza message, StanzaHandlerData state) async { + + Future _onMessage( + Stanza message, + StanzaHandlerData state, + ) async { final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; return state.copyWith( - sfs: StatelessFileSharingData.fromXML(sfs, ), + sfs: StatelessFileSharingData.fromXML( + sfs, + ), ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0448.dart b/packages/moxxmpp/lib/src/xeps/xep_0448.dart index 97dac00..7c13639 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0448.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0448.dart @@ -38,10 +38,18 @@ SFSEncryptionType encryptionTypeFromNamespace(String xmlns) { } class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { - - StatelessFileSharingEncryptedSource(this.encryption, this.key, this.iv, this.hashes, this.source); + StatelessFileSharingEncryptedSource( + this.encryption, + this.key, + this.iv, + this.hashes, + this.source, + ); factory StatelessFileSharingEncryptedSource.fromXml(XMLNode element) { - assert(element.attributes['xmlns'] == sfsEncryptionXmlns, 'Element has invalid xmlns'); + assert( + element.attributes['xmlns'] == sfsEncryptionXmlns, + 'Element has invalid xmlns', + ); final key = base64Decode(element.firstTag('key')!.text!); final iv = base64Decode(element.firstTag('iv')!.text!); @@ -50,7 +58,8 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { // Find the first URL source final source = firstWhereOrNull( sources, - (XMLNode child) => child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns, + (XMLNode child) => + child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns, )!; // Find hashes @@ -58,7 +67,7 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { for (final hash in element.findTags('hash', xmlns: hashXmlns)) { hashes[hash.attributes['algo']! as String] = hash.text!; } - + return StatelessFileSharingEncryptedSource( encryptionTypeFromNamespace(element.attributes['cipher']! as String), key, @@ -67,7 +76,7 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { StatelessFileSharingUrlSource.fromXml(source), ); } - + final List key; final List iv; final SFSEncryptionType encryption; @@ -91,7 +100,8 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { tag: 'iv', text: base64Encode(iv), ), - ...hashes.entries.map((hash) => constructHashElement(hash.key, hash.value)), + ...hashes.entries + .map((hash) => constructHashElement(hash.key, hash.value)), XMLNode.xmlns( tag: 'sources', xmlns: sfsXmlns, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0449.dart b/packages/moxxmpp/lib/src/xeps/xep_0449.dart index a90754e..f67afab 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0449.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0449.dart @@ -22,7 +22,9 @@ class Sticker { assert(node.tag == 'item', 'sticker has wrong tag'); return Sticker( - FileMetadataData.fromXML(node.firstTag('file', xmlns: fileMetadataXmlns)!), + FileMetadataData.fromXML( + node.firstTag('file', xmlns: fileMetadataXmlns)!, + ), processStatelessFileSharingSources(node, checkXmlns: false), {}, ); @@ -31,7 +33,7 @@ class Sticker { final FileMetadataData metadata; final List sources; // Language -> suggestion - final Map suggests; + final Map suggests; XMLNode toPubSubXML() { final suggestsElements = suggests.keys.map((suggest) { @@ -73,7 +75,11 @@ class StickerPack { this.restricted, ); - factory StickerPack.fromXML(String id, XMLNode node, { bool hashAvailable = true }) { + factory StickerPack.fromXML( + String id, + XMLNode node, { + bool hashAvailable = true, + }) { assert(node.tag == 'pack', 'node has wrong tag'); assert(node.attributes['xmlns'] == stickersXmlns, 'node has wrong XMLNS'); @@ -84,7 +90,7 @@ class StickerPack { hashAlgorithm = hashFunctionFromName(hash.attributes['algo']! as String); hashValue = hash.innerText(); } - + return StickerPack( id, node.firstTag('name')!.innerText(), @@ -92,9 +98,9 @@ class StickerPack { hashAlgorithm, hashValue, node.children - .where((e) => e.tag == 'item') - .map(Sticker.fromXML) - .toList(), + .where((e) => e.tag == 'item') + .map(Sticker.fromXML) + .toList(), node.firstTag('restricted') != null, ); } @@ -122,7 +128,7 @@ class StickerPack { restricted, ); } - + XMLNode toXML() { return XMLNode.xmlns( tag: 'pack', @@ -142,13 +148,10 @@ class StickerPack { hashValue, ), - ...restricted ? - [XMLNode(tag: 'restricted')] : - [], - + ...restricted ? [XMLNode(tag: 'restricted')] : [], + // Stickers - ...stickers - .map((sticker) => sticker.toPubSubXML()), + ...stickers.map((sticker) => sticker.toPubSubXML()), ], ); } @@ -232,16 +235,19 @@ class StickersManager extends XmppManagerBase { @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagXmlns: stickersXmlns, - tagName: 'sticker', - callback: _onIncomingMessage, - priority: -99, - ), - ]; + StanzaHandler( + stanzaTag: 'message', + tagXmlns: stickersXmlns, + tagName: 'sticker', + callback: _onIncomingMessage, + priority: -99, + ), + ]; - Future _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { + Future _onIncomingMessage( + Stanza stanza, + StanzaHandlerData state, + ) async { final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; return state.copyWith( stickerPackId: sticker.attributes['pack']! as String, @@ -252,7 +258,11 @@ class StickersManager extends XmppManagerBase { /// [accessModel] will be used as the PubSub node's access model. /// /// On success, returns true. On failure, returns a PubSubError. - Future> publishStickerPack(JID jid, StickerPack pack, { String? accessModel }) async { + Future> publishStickerPack( + JID jid, + StickerPack pack, { + String? accessModel, + }) async { assert(pack.id != '', 'The sticker pack must have an id'); final pm = getAttributes().getManagerById(pubsubManager)!; @@ -271,7 +281,10 @@ class StickersManager extends XmppManagerBase { /// Removes the sticker pack with id [id] from the PubSub node of [jid]. /// /// On success, returns the true. On failure, returns a PubSubError. - Future> retractStickerPack(JID jid, String id) async { + Future> retractStickerPack( + JID jid, + String id, + ) async { final pm = getAttributes().getManagerById(pubsubManager)!; return pm.retract( @@ -284,7 +297,10 @@ class StickersManager extends XmppManagerBase { /// Fetches the sticker pack with id [id] from [jid]. /// /// On success, returns the StickerPack. On failure, returns a PubSubError. - Future> fetchStickerPack(JID jid, String id) async { + Future> fetchStickerPack( + JID jid, + String id, + ) async { final pm = getAttributes().getManagerById(pubsubManager)!; final stickerPackDataRaw = await pm.getItem( jid.toBare().toString(), diff --git a/packages/moxxmpp/lib/src/xeps/xep_0461.dart b/packages/moxxmpp/lib/src/xeps/xep_0461.dart index ce4cc97..11ee5bf 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0461.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0461.dart @@ -45,17 +45,14 @@ class QuoteData { /// Takes the body of the message we want to quote [quoteBody] and the content of /// the reply [body] and computes the fallback body and its length. factory QuoteData.fromBodies(String quoteBody, String body) { - final fallback = quoteBody - .split('\n') - .map((line) => '> $line\n') - .join(); + final fallback = quoteBody.split('\n').map((line) => '> $line\n').join(); return QuoteData( '$fallback$body', fallback.length, ); } - + /// The new body with fallback data at the beginning final String body; @@ -70,25 +67,28 @@ class MessageRepliesManager extends XmppManagerBase { @override List getDiscoFeatures() => [ - replyXmlns, - ]; - + replyXmlns, + ]; + @override List getIncomingStanzaHandlers() => [ - StanzaHandler( - stanzaTag: 'message', - tagName: 'reply', - tagXmlns: replyXmlns, - callback: _onMessage, - // Before the message handler - priority: -99, - ) - ]; + StanzaHandler( + stanzaTag: 'message', + tagName: 'reply', + tagXmlns: replyXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; @override Future isSupported() async => true; - - Future _onMessage(Stanza stanza, StanzaHandlerData state) async { + + Future _onMessage( + Stanza stanza, + StanzaHandlerData state, + ) async { final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; final id = reply.attributes['id']! as String; final to = reply.attributes['to'] as String?; diff --git a/packages/moxxmpp/test/xeps/xep_0060_test.dart b/packages/moxxmpp/test/xeps/xep_0060_test.dart new file mode 100644 index 0000000..af04fcc --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0060_test.dart @@ -0,0 +1,172 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +import '../helpers/logging.dart'; +import '../helpers/xmpp.dart'; + +class StubbedDiscoManager extends DiscoManager { + StubbedDiscoManager() : super([]); + + @override + Future> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async { + final result = DiscoInfo.fromQuery( + XMLNode.fromString( + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +http://jabber.org/network/serverinfo + + +mailto:support@tigase.net +xmpp:tigase@mix.tigase.im +xmpp:tigase@muc.tigase.org +https://tigase.net/technical-support + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + ), + JID.fromString('pubsub.server.example.org'), + ); + + return Result(result); + } +} + +T? getDiscoManagerStub(String id) { + return StubbedDiscoManager() as T; +} + +void main() { + initLogger(); + + test('Test publishing with pubsub#max_items when the server does not support it', () async { + XMLNode? sent; + final manager = PubSubManager(); + manager.register( + XmppManagerAttributes( + sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async { + sent = stanza; + + return XMLNode.fromString(''); + }, + sendNonza: (_) {}, + sendEvent: (_) {}, + getManagerById: getDiscoManagerStub, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString('hallo@example.server'), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('hallo@example.server/uwu'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), + getNegotiatorById: getNegotiatorNullStub, + ), + ); + + final result = await manager.preprocessPublishOptions( + 'pubsub.server.example.org', + 'example:node', + PubSubPublishOptions( + maxItems: 'max', + ), + ); + + }); +} diff --git a/packages/moxxmpp_socket_tcp/lib/src/socket.dart b/packages/moxxmpp_socket_tcp/lib/src/socket.dart index d116c2a..3a60bef 100644 --- a/packages/moxxmpp_socket_tcp/lib/src/socket.dart +++ b/packages/moxxmpp_socket_tcp/lib/src/socket.dart @@ -22,7 +22,8 @@ class TCPSocketWrapper extends BaseSocketWrapper { final StreamController _dataStream = StreamController.broadcast(); /// The stream of outgoing (TCPSocketWrapper -> XmppConnection) events. - final StreamController _eventStream = StreamController.broadcast(); + final StreamController _eventStream = + StreamController.broadcast(); /// A subscription on the socket's data stream. StreamSubscription? _socketSubscription; @@ -68,7 +69,7 @@ class TCPSocketWrapper extends BaseSocketWrapper { bool onBadCertificate(dynamic certificate, String domain) { return false; } - + Future _xep368Connect(String domain) async { // TODO(Unknown): Maybe do DNSSEC one day final results = await srvQuery('_xmpps-client._tcp.$domain', false); @@ -80,7 +81,9 @@ class TCPSocketWrapper extends BaseSocketWrapper { results.sort(srvRecordSortComparator); for (final srv in results) { try { - _log.finest('Attempting secure connection to ${srv.target}:${srv.port}...'); + _log.finest( + 'Attempting secure connection to ${srv.target}:${srv.port}...', + ); // Workaround: We cannot set the SNI directly when using SecureSocket.connect. // instead, we connect using a regular socket and then secure it. This allows @@ -93,14 +96,14 @@ class TCPSocketWrapper extends BaseSocketWrapper { _socket = await SecureSocket.secure( sock, host: domain, - supportedProtocols: const [ xmppClientALPNId ], + supportedProtocols: const [xmppClientALPNId], onBadCertificate: (cert) => onBadCertificate(cert, domain), ); _secure = true; _log.finest('Success!'); return true; - } on Exception catch(e) { + } on Exception catch (e) { _log.finest('Failure! $e'); if (e is HandshakeException) { @@ -112,10 +115,10 @@ class TCPSocketWrapper extends BaseSocketWrapper { if (failedDueToTLS) { _eventStream.add(XmppSocketTLSFailedEvent()); } - + return false; } - + Future _rfc6120Connect(String domain) async { // TODO(Unknown): Maybe do DNSSEC one day final results = await srvQuery('_xmpp-client._tcp.$domain', false); @@ -132,7 +135,7 @@ class TCPSocketWrapper extends BaseSocketWrapper { _log.finest('Success!'); return true; - } on Exception catch(e) { + } on Exception catch (e) { _log.finest('Failure! $e'); continue; } @@ -154,7 +157,7 @@ class TCPSocketWrapper extends BaseSocketWrapper { ); _log.finest('Success!'); return true; - } on Exception catch(e) { + } on Exception catch (e) { _log.finest('Failure! $e'); return false; } @@ -180,14 +183,14 @@ class TCPSocketWrapper extends BaseSocketWrapper { _log.severe('Failed to secure socket since _socket is null'); return false; } - + try { // The socket is closed during the entire process _expectSocketClosure = true; _socket = await SecureSocket.secure( _socket!, - supportedProtocols: const [ xmppClientALPNId ], + supportedProtocols: const [xmppClientALPNId], onBadCertificate: (cert) => onBadCertificate(cert, domain), ); @@ -204,7 +207,7 @@ class TCPSocketWrapper extends BaseSocketWrapper { return false; } } - + void _setupStreams() { if (_socket == null) { _log.severe('Failed to setup streams as _socket is null'); @@ -230,9 +233,9 @@ class TCPSocketWrapper extends BaseSocketWrapper { _expectSocketClosure = false; }); } - + @override - Future connect(String domain, { String? host, int? port }) async { + Future connect(String domain, {String? host, int? port}) async { _expectSocketClosure = false; _secure = false; @@ -280,7 +283,7 @@ class TCPSocketWrapper extends BaseSocketWrapper { try { _socket!.close(); - } catch(e) { + } catch (e) { _log.warning('Closing socket threw exception: $e'); } } @@ -289,10 +292,11 @@ class TCPSocketWrapper extends BaseSocketWrapper { Stream getDataStream() => _dataStream.stream.asBroadcastStream(); @override - Stream getEventStream() => _eventStream.stream.asBroadcastStream(); + Stream getEventStream() => + _eventStream.stream.asBroadcastStream(); @override - void write(Object? data, { String? redact }) { + void write(Object? data, {String? redact}) { if (_socket == null) { _log.severe('Failed to write to socket as _socket is null'); return;