diff --git a/packages/moxxmpp/CHANGELOG.md b/packages/moxxmpp/CHANGELOG.md index a68b154..12185d0 100644 --- a/packages/moxxmpp/CHANGELOG.md +++ b/packages/moxxmpp/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.2 +## 0.4.0 - **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue. - **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData` @@ -11,6 +11,11 @@ - **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`. - **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument. - Sent stanzas are now kept in a queue until sent. +- **BREAKING**: `MessageManager.sendMessage` does not use `MessageDetails` anymore. Instead, use `TypedMap`. +- `MessageManager` now allows registering callbacks for adding data whenever a message is sent. +- **BREAKING**: `MessageEvent` now makes use of `TypedMap`. +- **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead. +- **BREAKING**: `ChatState.toString()` is now `ChatState.toName()` ## 0.3.1 diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index e44a60f..92cd8f5 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -18,7 +18,6 @@ export 'package:moxxmpp/src/managers/namespaces.dart'; export 'package:moxxmpp/src/managers/priorities.dart'; export 'package:moxxmpp/src/message.dart'; export 'package:moxxmpp/src/namespaces.dart'; -export 'package:moxxmpp/src/negotiators/manager.dart'; export 'package:moxxmpp/src/negotiators/namespaces.dart'; export 'package:moxxmpp/src/negotiators/negotiator.dart'; export 'package:moxxmpp/src/ping.dart'; @@ -40,6 +39,7 @@ export 'package:moxxmpp/src/socket.dart'; export 'package:moxxmpp/src/stanza.dart'; export 'package:moxxmpp/src/stringxml.dart'; export 'package:moxxmpp/src/types/result.dart'; +export 'package:moxxmpp/src/util/typed_map.dart'; export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; export 'package:moxxmpp/src/xeps/staging/fast.dart'; export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 872a985..413d8a0 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -27,7 +27,9 @@ import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/util/queue.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; +import 'package:moxxmpp/src/xeps/xep_0198/types.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0352.dart'; import 'package:synchronized/synchronized.dart'; @@ -405,8 +407,7 @@ class XmppConnection { /// Returns true if we can send data through the socket. Future _canSendData() async { - return [XmppConnectionState.connected, XmppConnectionState.connecting] - .contains(await getConnectionState()); + return await getConnectionState() == XmppConnectionState.connected; } /// Sends a stanza described by [details] to the server. Until sent, the stanza is @@ -424,13 +425,17 @@ class XmppConnection { ); final completer = details.awaitable ? Completer() : null; - await _stanzaQueue.enqueueStanza( - StanzaQueueEntry( - details, - completer, - ), + final entry = StanzaQueueEntry( + details, + completer, ); + if (details.bypassQueue) { + await _sendStanzaImpl(entry); + } else { + await _stanzaQueue.enqueueStanza(entry); + } + return completer?.future; } @@ -471,8 +476,8 @@ class XmppConnection { initial: StanzaHandlerData( false, false, - null, newStanza, + TypedMap(), encrypted: details.encrypted, forceEncryption: details.forceEncryption, ), @@ -523,18 +528,20 @@ class XmppConnection { if (await _canSendData()) { _socket.write(data.stanza.toXml()); } else { - _log.fine('Not sending dat as _canSendData() returned false.'); + _log.fine('Not sending data as _canSendData() returned false.'); } // Run post-send handlers _log.fine('Running post stanza handlers..'); + final extensions = TypedMap() + ..set(StreamManagementData(details.excludeFromStreamManagement)); await _runOutgoingPostStanzaHandlers( newStanza, initial: StanzaHandlerData( false, false, - null, newStanza, + extensions, ), ); _log.fine('Done'); @@ -649,7 +656,7 @@ class XmppConnection { Stanza stanza, { StanzaHandlerData? initial, }) async { - var state = initial ?? StanzaHandlerData(false, false, null, stanza); + var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap()); for (final handler in handlers) { if (handler.matches(state.stanza)) { state = await handler.callback(state.stanza, state); @@ -724,7 +731,7 @@ class XmppConnection { // it. final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza); final prefix = incomingPreHandlers.encrypted && - incomingPreHandlers.other['encryption_error'] == null + incomingPreHandlers.encryptionError == null ? '(Encrypted) ' : ''; _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); @@ -743,10 +750,10 @@ class XmppConnection { initial: StanzaHandlerData( false, incomingPreHandlers.cancel, - incomingPreHandlers.cancelReason, incomingPreHandlers.stanza, + incomingPreHandlers.extensions, encrypted: incomingPreHandlers.encrypted, - other: incomingPreHandlers.other, + cancelReason: incomingPreHandlers.cancelReason, ), ); if (!incomingHandlers.done) { @@ -835,7 +842,7 @@ class XmppConnection { await _reconnectionPolicy.setShouldReconnect(false); if (triggeredByUser) { - getPresenceManager()?.sendUnavailablePresence(); + await getPresenceManager()?.sendUnavailablePresence(); } _socket.prepareDisconnect(); diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 2eaec7b..9c0b8b3 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -4,18 +4,11 @@ import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/roster/roster.dart'; import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; -import 'package:moxxmpp/src/xeps/xep_0066.dart'; -import 'package:moxxmpp/src/xeps/xep_0085.dart'; -import 'package:moxxmpp/src/xeps/xep_0334.dart'; -import 'package:moxxmpp/src/xeps/xep_0359.dart'; -import 'package:moxxmpp/src/xeps/xep_0385.dart'; -import 'package:moxxmpp/src/xeps/xep_0424.dart'; -import 'package:moxxmpp/src/xeps/xep_0444.dart'; -import 'package:moxxmpp/src/xeps/xep_0446.dart'; -import 'package:moxxmpp/src/xeps/xep_0447.dart'; -import 'package:moxxmpp/src/xeps/xep_0461.dart'; +import 'package:moxxmpp/src/xeps/xep_0084.dart'; +import 'package:moxxmpp/src/xeps/xep_0333.dart'; abstract class XmppEvent {} @@ -74,60 +67,42 @@ class RosterUpdatedEvent extends XmppEvent { /// Triggered when a message is received class MessageEvent extends XmppEvent { - MessageEvent({ - required this.body, - required this.fromJid, - required this.toJid, - required this.sid, - required this.isCarbon, - required this.deliveryReceiptRequested, - required this.isMarkable, - required this.encrypted, - required this.other, - this.originId, - this.stanzaIds, - this.error, + MessageEvent( + this.from, + this.to, + this.id, + this.encrypted, + this.extensions, { this.type, - this.oob, - this.sfs, - this.sims, - this.reply, - this.chatState, - this.fun, - this.funReplacement, - this.funCancellation, - this.messageRetraction, - this.messageCorrectionId, - this.messageReactions, - this.messageProcessingHints, - this.stickerPackId, + this.error, + this.encryptionError, }); - final StanzaError? error; - final String body; - final JID fromJid; - final JID toJid; - final String sid; + + /// The from attribute of the message. + final JID from; + + /// The to attribute of the message. + final JID to; + + /// The id attribute of the message. + final String id; + + /// The type attribute of the message. final String? type; - final String? originId; - final List? stanzaIds; - final bool isCarbon; - final bool deliveryReceiptRequested; - final bool isMarkable; - final OOBData? oob; - final StatelessFileSharingData? sfs; - final StatelessMediaSharingData? sims; - final ReplyData? reply; - final ChatState? chatState; - final FileMetadataData? fun; - final String? funReplacement; - final String? funCancellation; + + final StanzaError? error; + + /// Flag indicating whether the message was encrypted. final bool encrypted; - final MessageRetractionData? messageRetraction; - final String? messageCorrectionId; - final MessageReactions? messageReactions; - final List? messageProcessingHints; - final String? stickerPackId; - final Map other; + + /// The error in case an encryption error occurred. + final Object? encryptionError; + + /// Data added by other handlers. + final TypedMap extensions; + + /// Shorthand for extensions.get(). + T? get() => extensions.get(); } /// Triggered when a client responds to our delivery receipt request @@ -138,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent { } class ChatMarkerEvent extends XmppEvent { - ChatMarkerEvent({ - required this.type, - required this.from, - required this.id, - }); + ChatMarkerEvent( + this.from, + this.type, + this.id, + ); + + /// The entity that sent the chat marker. final JID from; - final String type; + + /// The type of chat marker that was sent. + final ChatMarker type; + + /// The id of the message that the marker applies to. final String id; } @@ -168,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent { final String resource; } -/// Triggered when we receive presence -class PresenceReceivedEvent extends XmppEvent { - PresenceReceivedEvent(this.jid, this.presence); - final JID jid; - final Stanza presence; -} - /// Triggered when we are starting an connection attempt class ConnectingEvent extends XmppEvent {} @@ -192,15 +166,35 @@ class SubscriptionRequestReceivedEvent extends XmppEvent { 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, - }); - final String jid; +/// Triggered when we receive a new or updated avatar via XEP-0084 +class UserAvatarUpdatedEvent extends XmppEvent { + UserAvatarUpdatedEvent( + this.jid, + this.metadata, + ); + + /// The JID of the user updating their avatar. + final JID jid; + + /// The metadata of the avatar. + final List metadata; +} + +/// Triggered when we receive a new or updated avatar via XEP-0054 +class VCardAvatarUpdatedEvent extends XmppEvent { + VCardAvatarUpdatedEvent( + this.jid, + this.base64, + this.hash, + ); + + /// The JID of the entity that updated their avatar. + final JID jid; + + /// The base64-encoded avatar data. final String base64; + + /// The SHA-1 hash of the avatar. final String hash; } diff --git a/packages/moxxmpp/lib/src/managers/data.dart b/packages/moxxmpp/lib/src/managers/data.dart index cbc2f85..6d8a5f2 100644 --- a/packages/moxxmpp/lib/src/managers/data.dart +++ b/packages/moxxmpp/lib/src/managers/data.dart @@ -1,79 +1,47 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:moxxmpp/src/stanza.dart'; -import 'package:moxxmpp/src/xeps/xep_0066.dart'; -import 'package:moxxmpp/src/xeps/xep_0085.dart'; -import 'package:moxxmpp/src/xeps/xep_0203.dart'; -import 'package:moxxmpp/src/xeps/xep_0359.dart'; -import 'package:moxxmpp/src/xeps/xep_0380.dart'; -import 'package:moxxmpp/src/xeps/xep_0385.dart'; -import 'package:moxxmpp/src/xeps/xep_0424.dart'; -import 'package:moxxmpp/src/xeps/xep_0444.dart'; -import 'package:moxxmpp/src/xeps/xep_0446.dart'; -import 'package:moxxmpp/src/xeps/xep_0447.dart'; -import 'package:moxxmpp/src/xeps/xep_0461.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -part 'data.freezed.dart'; +abstract class StanzaHandlerExtension {} -@freezed -class StanzaHandlerData with _$StanzaHandlerData { - factory StanzaHandlerData( - // Indicates to the runner that processing is now done. This means that all - // pre-processing is done and no other handlers should be consulted. - bool done, - // Indicates to the runner that processing is to be cancelled and no further handlers - // should run. The stanza also will not be sent. - bool cancel, - // The reason why we cancelled the processing and sending - 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, +class StanzaHandlerData { + StanzaHandlerData( + this.done, + this.cancel, + this.stanza, + this.extensions, { + this.cancelReason, + this.encryptionError, + this.encrypted = false, + this.forceEncryption = false, + }); - // XEP-0359 's id attribute, if available. - String? originId, + /// Indicates to the runner that processing is now done. This means that all + /// pre-processing is done and no other handlers should be consulted. + bool done; - // XEP-0359 elements, if available. - List? stanzaIds, - 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; + /// Indicates to the runner that processing is to be cancelled and no further handlers + /// should run. The stanza also will not be sent. + bool cancel; + + /// The reason why we cancelled the processing and sending. + Object? cancelReason; + + /// The reason why an encryption or decryption failed. + Object? encryptionError; + + /// 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 was received encrypted + 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. + bool forceEncryption; + + /// Additional data from other managers. + final TypedMap extensions; } diff --git a/packages/moxxmpp/lib/src/managers/data.freezed.dart b/packages/moxxmpp/lib/src/managers/data.freezed.dart deleted file mode 100644 index cca8e46..0000000 --- a/packages/moxxmpp/lib/src/managers/data.freezed.dart +++ /dev/null @@ -1,793 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'data.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); - -/// @nodoc -mixin _$StanzaHandlerData { -// Indicates to the runner that processing is now done. This means that all -// pre-processing is done and no other handlers should be consulted. - bool get done => - throw _privateConstructorUsedError; // Indicates to the runner that processing is to be cancelled and no further handlers -// should run. The stanza also will not be sent. - bool get cancel => - throw _privateConstructorUsedError; // The reason why we cancelled the processing and sending - dynamic get cancelReason => - throw _privateConstructorUsedError; // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely -// necessary, e.g. with Message Carbons or OMEMO - Stanza get stanza => - throw _privateConstructorUsedError; // Whether the stanza is retransmitted. Only useful in the context of outgoing -// stanza handlers. MUST NOT be overwritten. - bool get retransmitted => throw _privateConstructorUsedError; - StatelessMediaSharingData? get sims => throw _privateConstructorUsedError; - StatelessFileSharingData? get sfs => throw _privateConstructorUsedError; - OOBData? get oob => - throw _privateConstructorUsedError; // XEP-0359 's id attribute, if available. - String? get originId => - throw _privateConstructorUsedError; // XEP-0359 elements, if available. - List? get stanzaIds => throw _privateConstructorUsedError; - ReplyData? get reply => throw _privateConstructorUsedError; - ChatState? get chatState => throw _privateConstructorUsedError; - bool get isCarbon => throw _privateConstructorUsedError; - bool get deliveryReceiptRequested => throw _privateConstructorUsedError; - bool get isMarkable => - throw _privateConstructorUsedError; // File Upload Notifications -// A notification - FileMetadataData? get fun => - throw _privateConstructorUsedError; // The stanza id this replaces - String? get funReplacement => - throw _privateConstructorUsedError; // The stanza id this cancels - String? get funCancellation => - throw _privateConstructorUsedError; // Whether the stanza was received encrypted - bool get encrypted => - throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it -// would not normally. In the case of OMEMO: If shouldEncrypt returns false -// but forceEncryption is true, then the OMEMO manager will try to encrypt -// to the JID anyway. - bool get forceEncryption => - throw _privateConstructorUsedError; // The stated type of encryption used, if any was used - ExplicitEncryptionType? get encryptionType => - throw _privateConstructorUsedError; // Delayed Delivery - DelayedDelivery? get delayedDelivery => - throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need -// pass data around. - Map get other => - throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be -// retracted - MessageRetractionData? get messageRetraction => - throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id - String? get lastMessageCorrectionSid => - throw _privateConstructorUsedError; // Reactions data - MessageReactions? get messageReactions => - throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to - String? get stickerPackId => throw _privateConstructorUsedError; - - @JsonKey(ignore: true) - $StanzaHandlerDataCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $StanzaHandlerDataCopyWith<$Res> { - factory $StanzaHandlerDataCopyWith( - StanzaHandlerData value, $Res Function(StanzaHandlerData) then) = - _$StanzaHandlerDataCopyWithImpl<$Res, StanzaHandlerData>; - @useResult - $Res call( - {bool done, - bool cancel, - dynamic cancelReason, - Stanza stanza, - bool retransmitted, - StatelessMediaSharingData? sims, - StatelessFileSharingData? sfs, - OOBData? oob, - String? originId, - List? stanzaIds, - ReplyData? reply, - ChatState? chatState, - bool isCarbon, - bool deliveryReceiptRequested, - bool isMarkable, - FileMetadataData? fun, - String? funReplacement, - String? funCancellation, - bool encrypted, - bool forceEncryption, - ExplicitEncryptionType? encryptionType, - DelayedDelivery? delayedDelivery, - Map other, - MessageRetractionData? messageRetraction, - String? lastMessageCorrectionSid, - MessageReactions? messageReactions, - String? stickerPackId}); -} - -/// @nodoc -class _$StanzaHandlerDataCopyWithImpl<$Res, $Val extends StanzaHandlerData> - implements $StanzaHandlerDataCopyWith<$Res> { - _$StanzaHandlerDataCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? done = null, - Object? cancel = null, - Object? cancelReason = freezed, - Object? stanza = null, - Object? retransmitted = null, - Object? sims = freezed, - Object? sfs = freezed, - Object? oob = freezed, - Object? originId = freezed, - Object? stanzaIds = freezed, - Object? reply = freezed, - Object? chatState = freezed, - Object? isCarbon = null, - Object? deliveryReceiptRequested = null, - Object? isMarkable = null, - Object? fun = freezed, - Object? funReplacement = freezed, - Object? funCancellation = freezed, - Object? encrypted = null, - Object? forceEncryption = null, - Object? encryptionType = freezed, - Object? delayedDelivery = freezed, - Object? other = null, - Object? messageRetraction = freezed, - Object? lastMessageCorrectionSid = freezed, - Object? messageReactions = freezed, - Object? stickerPackId = freezed, - }) { - return _then(_value.copyWith( - done: null == done - ? _value.done - : done // ignore: cast_nullable_to_non_nullable - as bool, - cancel: null == cancel - ? _value.cancel - : cancel // ignore: cast_nullable_to_non_nullable - as bool, - cancelReason: freezed == cancelReason - ? _value.cancelReason - : cancelReason // ignore: cast_nullable_to_non_nullable - as dynamic, - stanza: null == stanza - ? _value.stanza - : stanza // ignore: cast_nullable_to_non_nullable - as Stanza, - retransmitted: null == retransmitted - ? _value.retransmitted - : retransmitted // ignore: cast_nullable_to_non_nullable - as bool, - sims: freezed == sims - ? _value.sims - : sims // ignore: cast_nullable_to_non_nullable - as StatelessMediaSharingData?, - sfs: freezed == sfs - ? _value.sfs - : sfs // ignore: cast_nullable_to_non_nullable - as StatelessFileSharingData?, - oob: freezed == oob - ? _value.oob - : oob // ignore: cast_nullable_to_non_nullable - as OOBData?, - originId: freezed == originId - ? _value.originId - : originId // ignore: cast_nullable_to_non_nullable - as String?, - stanzaIds: freezed == stanzaIds - ? _value.stanzaIds - : stanzaIds // ignore: cast_nullable_to_non_nullable - as List?, - reply: freezed == reply - ? _value.reply - : reply // ignore: cast_nullable_to_non_nullable - as ReplyData?, - chatState: freezed == chatState - ? _value.chatState - : chatState // ignore: cast_nullable_to_non_nullable - as ChatState?, - isCarbon: null == isCarbon - ? _value.isCarbon - : isCarbon // ignore: cast_nullable_to_non_nullable - as bool, - deliveryReceiptRequested: null == deliveryReceiptRequested - ? _value.deliveryReceiptRequested - : deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable - as bool, - isMarkable: null == isMarkable - ? _value.isMarkable - : isMarkable // ignore: cast_nullable_to_non_nullable - as bool, - fun: freezed == fun - ? _value.fun - : fun // ignore: cast_nullable_to_non_nullable - as FileMetadataData?, - funReplacement: freezed == funReplacement - ? _value.funReplacement - : funReplacement // ignore: cast_nullable_to_non_nullable - as String?, - funCancellation: freezed == funCancellation - ? _value.funCancellation - : funCancellation // ignore: cast_nullable_to_non_nullable - as String?, - encrypted: null == encrypted - ? _value.encrypted - : encrypted // ignore: cast_nullable_to_non_nullable - as bool, - forceEncryption: null == forceEncryption - ? _value.forceEncryption - : forceEncryption // ignore: cast_nullable_to_non_nullable - as bool, - encryptionType: freezed == encryptionType - ? _value.encryptionType - : encryptionType // ignore: cast_nullable_to_non_nullable - as ExplicitEncryptionType?, - delayedDelivery: freezed == delayedDelivery - ? _value.delayedDelivery - : delayedDelivery // ignore: cast_nullable_to_non_nullable - as DelayedDelivery?, - other: null == other - ? _value.other - : other // ignore: cast_nullable_to_non_nullable - as Map, - messageRetraction: freezed == messageRetraction - ? _value.messageRetraction - : messageRetraction // ignore: cast_nullable_to_non_nullable - as MessageRetractionData?, - lastMessageCorrectionSid: freezed == lastMessageCorrectionSid - ? _value.lastMessageCorrectionSid - : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable - as String?, - messageReactions: freezed == messageReactions - ? _value.messageReactions - : messageReactions // ignore: cast_nullable_to_non_nullable - as MessageReactions?, - stickerPackId: freezed == stickerPackId - ? _value.stickerPackId - : stickerPackId // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$_StanzaHandlerDataCopyWith<$Res> - implements $StanzaHandlerDataCopyWith<$Res> { - factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value, - $Res Function(_$_StanzaHandlerData) then) = - __$$_StanzaHandlerDataCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool done, - bool cancel, - dynamic cancelReason, - Stanza stanza, - bool retransmitted, - StatelessMediaSharingData? sims, - StatelessFileSharingData? sfs, - OOBData? oob, - String? originId, - List? stanzaIds, - ReplyData? reply, - ChatState? chatState, - bool isCarbon, - bool deliveryReceiptRequested, - bool isMarkable, - FileMetadataData? fun, - String? funReplacement, - String? funCancellation, - bool encrypted, - bool forceEncryption, - ExplicitEncryptionType? encryptionType, - DelayedDelivery? delayedDelivery, - Map other, - MessageRetractionData? messageRetraction, - String? lastMessageCorrectionSid, - MessageReactions? messageReactions, - String? stickerPackId}); -} - -/// @nodoc -class __$$_StanzaHandlerDataCopyWithImpl<$Res> - extends _$StanzaHandlerDataCopyWithImpl<$Res, _$_StanzaHandlerData> - implements _$$_StanzaHandlerDataCopyWith<$Res> { - __$$_StanzaHandlerDataCopyWithImpl( - _$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? done = null, - Object? cancel = null, - Object? cancelReason = freezed, - Object? stanza = null, - Object? retransmitted = null, - Object? sims = freezed, - Object? sfs = freezed, - Object? oob = freezed, - Object? originId = freezed, - Object? stanzaIds = freezed, - Object? reply = freezed, - Object? chatState = freezed, - Object? isCarbon = null, - Object? deliveryReceiptRequested = null, - Object? isMarkable = null, - Object? fun = freezed, - Object? funReplacement = freezed, - Object? funCancellation = freezed, - Object? encrypted = null, - Object? forceEncryption = null, - Object? encryptionType = freezed, - Object? delayedDelivery = freezed, - Object? other = null, - Object? messageRetraction = freezed, - Object? lastMessageCorrectionSid = freezed, - Object? messageReactions = freezed, - Object? stickerPackId = freezed, - }) { - return _then(_$_StanzaHandlerData( - null == done - ? _value.done - : done // ignore: cast_nullable_to_non_nullable - as bool, - null == cancel - ? _value.cancel - : cancel // ignore: cast_nullable_to_non_nullable - as bool, - freezed == cancelReason - ? _value.cancelReason - : cancelReason // ignore: cast_nullable_to_non_nullable - as dynamic, - null == stanza - ? _value.stanza - : stanza // ignore: cast_nullable_to_non_nullable - as Stanza, - retransmitted: null == retransmitted - ? _value.retransmitted - : retransmitted // ignore: cast_nullable_to_non_nullable - as bool, - sims: freezed == sims - ? _value.sims - : sims // ignore: cast_nullable_to_non_nullable - as StatelessMediaSharingData?, - sfs: freezed == sfs - ? _value.sfs - : sfs // ignore: cast_nullable_to_non_nullable - as StatelessFileSharingData?, - oob: freezed == oob - ? _value.oob - : oob // ignore: cast_nullable_to_non_nullable - as OOBData?, - originId: freezed == originId - ? _value.originId - : originId // ignore: cast_nullable_to_non_nullable - as String?, - stanzaIds: freezed == stanzaIds - ? _value._stanzaIds - : stanzaIds // ignore: cast_nullable_to_non_nullable - as List?, - reply: freezed == reply - ? _value.reply - : reply // ignore: cast_nullable_to_non_nullable - as ReplyData?, - chatState: freezed == chatState - ? _value.chatState - : chatState // ignore: cast_nullable_to_non_nullable - as ChatState?, - isCarbon: null == isCarbon - ? _value.isCarbon - : isCarbon // ignore: cast_nullable_to_non_nullable - as bool, - deliveryReceiptRequested: null == deliveryReceiptRequested - ? _value.deliveryReceiptRequested - : deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable - as bool, - isMarkable: null == isMarkable - ? _value.isMarkable - : isMarkable // ignore: cast_nullable_to_non_nullable - as bool, - fun: freezed == fun - ? _value.fun - : fun // ignore: cast_nullable_to_non_nullable - as FileMetadataData?, - funReplacement: freezed == funReplacement - ? _value.funReplacement - : funReplacement // ignore: cast_nullable_to_non_nullable - as String?, - funCancellation: freezed == funCancellation - ? _value.funCancellation - : funCancellation // ignore: cast_nullable_to_non_nullable - as String?, - encrypted: null == encrypted - ? _value.encrypted - : encrypted // ignore: cast_nullable_to_non_nullable - as bool, - forceEncryption: null == forceEncryption - ? _value.forceEncryption - : forceEncryption // ignore: cast_nullable_to_non_nullable - as bool, - encryptionType: freezed == encryptionType - ? _value.encryptionType - : encryptionType // ignore: cast_nullable_to_non_nullable - as ExplicitEncryptionType?, - delayedDelivery: freezed == delayedDelivery - ? _value.delayedDelivery - : delayedDelivery // ignore: cast_nullable_to_non_nullable - as DelayedDelivery?, - other: null == other - ? _value._other - : other // ignore: cast_nullable_to_non_nullable - as Map, - messageRetraction: freezed == messageRetraction - ? _value.messageRetraction - : messageRetraction // ignore: cast_nullable_to_non_nullable - as MessageRetractionData?, - lastMessageCorrectionSid: freezed == lastMessageCorrectionSid - ? _value.lastMessageCorrectionSid - : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable - as String?, - messageReactions: freezed == messageReactions - ? _value.messageReactions - : messageReactions // ignore: cast_nullable_to_non_nullable - as MessageReactions?, - stickerPackId: freezed == stickerPackId - ? _value.stickerPackId - : stickerPackId // ignore: cast_nullable_to_non_nullable - as String?, - )); - } -} - -/// @nodoc - -class _$_StanzaHandlerData implements _StanzaHandlerData { - _$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza, - {this.retransmitted = false, - this.sims, - this.sfs, - this.oob, - this.originId, - final List? stanzaIds, - this.reply, - this.chatState, - this.isCarbon = false, - this.deliveryReceiptRequested = false, - this.isMarkable = false, - this.fun, - this.funReplacement, - this.funCancellation, - this.encrypted = false, - this.forceEncryption = false, - this.encryptionType, - this.delayedDelivery, - final Map other = const {}, - this.messageRetraction, - this.lastMessageCorrectionSid, - this.messageReactions, - this.stickerPackId}) - : _stanzaIds = stanzaIds, - _other = other; - -// Indicates to the runner that processing is now done. This means that all -// pre-processing is done and no other handlers should be consulted. - @override - final bool done; -// Indicates to the runner that processing is to be cancelled and no further handlers -// should run. The stanza also will not be sent. - @override - final bool cancel; -// The reason why we cancelled the processing and sending - @override - final 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 - @override - final Stanza stanza; -// Whether the stanza is retransmitted. Only useful in the context of outgoing -// stanza handlers. MUST NOT be overwritten. - @override - @JsonKey() - final bool retransmitted; - @override - final StatelessMediaSharingData? sims; - @override - final StatelessFileSharingData? sfs; - @override - final OOBData? oob; -// XEP-0359 's id attribute, if available. - @override - final String? originId; -// XEP-0359 elements, if available. - final List? _stanzaIds; -// XEP-0359 elements, if available. - @override - List? get stanzaIds { - final value = _stanzaIds; - if (value == null) return null; - if (_stanzaIds is EqualUnmodifiableListView) return _stanzaIds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - - @override - final ReplyData? reply; - @override - final ChatState? chatState; - @override - @JsonKey() - final bool isCarbon; - @override - @JsonKey() - final bool deliveryReceiptRequested; - @override - @JsonKey() - final bool isMarkable; -// File Upload Notifications -// A notification - @override - final FileMetadataData? fun; -// The stanza id this replaces - @override - final String? funReplacement; -// The stanza id this cancels - @override - final String? funCancellation; -// Whether the stanza was received encrypted - @override - @JsonKey() - final bool encrypted; -// If true, forces the encryption manager to encrypt to the JID, even if it -// would not normally. In the case of OMEMO: If shouldEncrypt returns false -// but forceEncryption is true, then the OMEMO manager will try to encrypt -// to the JID anyway. - @override - @JsonKey() - final bool forceEncryption; -// The stated type of encryption used, if any was used - @override - final ExplicitEncryptionType? encryptionType; -// Delayed Delivery - @override - final DelayedDelivery? delayedDelivery; -// This is for stanza handlers that are not part of the XMPP library but still need -// pass data around. - final Map _other; -// This is for stanza handlers that are not part of the XMPP library but still need -// pass data around. - @override - @JsonKey() - Map get other { - if (_other is EqualUnmodifiableMapView) return _other; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_other); - } - -// If non-null, then it indicates the origin Id of the message that should be -// retracted - @override - final MessageRetractionData? messageRetraction; -// If non-null, then the message is a correction for the specified stanza Id - @override - final String? lastMessageCorrectionSid; -// Reactions data - @override - final MessageReactions? messageReactions; -// The Id of the sticker pack this sticker belongs to - @override - final String? stickerPackId; - - @override - String toString() { - return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, originId: $originId, stanzaIds: $stanzaIds, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$_StanzaHandlerData && - (identical(other.done, done) || other.done == done) && - (identical(other.cancel, cancel) || other.cancel == cancel) && - const DeepCollectionEquality() - .equals(other.cancelReason, cancelReason) && - (identical(other.stanza, stanza) || other.stanza == stanza) && - (identical(other.retransmitted, retransmitted) || - other.retransmitted == retransmitted) && - (identical(other.sims, sims) || other.sims == sims) && - (identical(other.sfs, sfs) || other.sfs == sfs) && - (identical(other.oob, oob) || other.oob == oob) && - (identical(other.originId, originId) || - other.originId == originId) && - const DeepCollectionEquality() - .equals(other._stanzaIds, _stanzaIds) && - (identical(other.reply, reply) || other.reply == reply) && - (identical(other.chatState, chatState) || - other.chatState == chatState) && - (identical(other.isCarbon, isCarbon) || - other.isCarbon == isCarbon) && - (identical( - other.deliveryReceiptRequested, deliveryReceiptRequested) || - other.deliveryReceiptRequested == deliveryReceiptRequested) && - (identical(other.isMarkable, isMarkable) || - other.isMarkable == isMarkable) && - (identical(other.fun, fun) || other.fun == fun) && - (identical(other.funReplacement, funReplacement) || - other.funReplacement == funReplacement) && - (identical(other.funCancellation, funCancellation) || - other.funCancellation == funCancellation) && - (identical(other.encrypted, encrypted) || - other.encrypted == encrypted) && - (identical(other.forceEncryption, forceEncryption) || - other.forceEncryption == forceEncryption) && - (identical(other.encryptionType, encryptionType) || - other.encryptionType == encryptionType) && - (identical(other.delayedDelivery, delayedDelivery) || - other.delayedDelivery == delayedDelivery) && - const DeepCollectionEquality().equals(other._other, this._other) && - (identical(other.messageRetraction, messageRetraction) || - other.messageRetraction == messageRetraction) && - (identical( - other.lastMessageCorrectionSid, lastMessageCorrectionSid) || - other.lastMessageCorrectionSid == lastMessageCorrectionSid) && - (identical(other.messageReactions, messageReactions) || - other.messageReactions == messageReactions) && - (identical(other.stickerPackId, stickerPackId) || - other.stickerPackId == stickerPackId)); - } - - @override - int get hashCode => Object.hashAll([ - runtimeType, - done, - cancel, - const DeepCollectionEquality().hash(cancelReason), - stanza, - retransmitted, - sims, - sfs, - oob, - originId, - const DeepCollectionEquality().hash(_stanzaIds), - reply, - chatState, - isCarbon, - deliveryReceiptRequested, - isMarkable, - fun, - funReplacement, - funCancellation, - encrypted, - forceEncryption, - encryptionType, - delayedDelivery, - const DeepCollectionEquality().hash(_other), - messageRetraction, - lastMessageCorrectionSid, - messageReactions, - stickerPackId - ]); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => - __$$_StanzaHandlerDataCopyWithImpl<_$_StanzaHandlerData>( - this, _$identity); -} - -abstract class _StanzaHandlerData implements StanzaHandlerData { - factory _StanzaHandlerData(final bool done, final bool cancel, - final dynamic cancelReason, final Stanza stanza, - {final bool retransmitted, - final StatelessMediaSharingData? sims, - final StatelessFileSharingData? sfs, - final OOBData? oob, - final String? originId, - final List? stanzaIds, - final ReplyData? reply, - final ChatState? chatState, - final bool isCarbon, - final bool deliveryReceiptRequested, - final bool isMarkable, - final FileMetadataData? fun, - final String? funReplacement, - final String? funCancellation, - final bool encrypted, - final bool forceEncryption, - final ExplicitEncryptionType? encryptionType, - final DelayedDelivery? delayedDelivery, - final Map other, - final MessageRetractionData? messageRetraction, - final String? lastMessageCorrectionSid, - final MessageReactions? messageReactions, - final String? stickerPackId}) = _$_StanzaHandlerData; - - @override // Indicates to the runner that processing is now done. This means that all -// pre-processing is done and no other handlers should be consulted. - bool get done; - @override // Indicates to the runner that processing is to be cancelled and no further handlers -// should run. The stanza also will not be sent. - bool get cancel; - @override // The reason why we cancelled the processing and sending - dynamic get cancelReason; - @override // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely -// necessary, e.g. with Message Carbons or OMEMO - Stanza get stanza; - @override // Whether the stanza is retransmitted. Only useful in the context of outgoing -// stanza handlers. MUST NOT be overwritten. - bool get retransmitted; - @override - StatelessMediaSharingData? get sims; - @override - StatelessFileSharingData? get sfs; - @override - OOBData? get oob; - @override // XEP-0359 's id attribute, if available. - String? get originId; - @override // XEP-0359 elements, if available. - List? get stanzaIds; - @override - ReplyData? get reply; - @override - ChatState? get chatState; - @override - bool get isCarbon; - @override - bool get deliveryReceiptRequested; - @override - bool get isMarkable; - @override // File Upload Notifications -// A notification - FileMetadataData? get fun; - @override // The stanza id this replaces - String? get funReplacement; - @override // The stanza id this cancels - String? get funCancellation; - @override // Whether the stanza was received encrypted - bool get encrypted; - @override // If true, forces the encryption manager to encrypt to the JID, even if it -// would not normally. In the case of OMEMO: If shouldEncrypt returns false -// but forceEncryption is true, then the OMEMO manager will try to encrypt -// to the JID anyway. - bool get forceEncryption; - @override // The stated type of encryption used, if any was used - ExplicitEncryptionType? get encryptionType; - @override // Delayed Delivery - DelayedDelivery? get delayedDelivery; - @override // This is for stanza handlers that are not part of the XMPP library but still need -// pass data around. - Map get other; - @override // If non-null, then it indicates the origin Id of the message that should be -// retracted - MessageRetractionData? get messageRetraction; - @override // If non-null, then the message is a correction for the specified stanza Id - String? get lastMessageCorrectionSid; - @override // Reactions data - MessageReactions? get messageReactions; - @override // The Id of the sticker pack this sticker belongs to - String? get stickerPackId; - @override - @JsonKey(ignore: true) - _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index 03e40e0..713493d 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -31,3 +31,4 @@ const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; const stickersManager = 'org.moxxmpp.stickersmanager'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; +const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint'; diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index ae3ea31..bae047e 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -1,89 +1,71 @@ -import 'package:moxlib/moxlib.dart'; +import 'package:collection/collection.dart'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; -import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart'; -import 'package:moxxmpp/src/xeps/xep_0085.dart'; -import 'package:moxxmpp/src/xeps/xep_0184.dart'; -import 'package:moxxmpp/src/xeps/xep_0308.dart'; -import 'package:moxxmpp/src/xeps/xep_0333.dart'; -import 'package:moxxmpp/src/xeps/xep_0334.dart'; -import 'package:moxxmpp/src/xeps/xep_0359.dart'; -import 'package:moxxmpp/src/xeps/xep_0424.dart'; -import 'package:moxxmpp/src/xeps/xep_0444.dart'; -import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; -import 'package:moxxmpp/src/xeps/xep_0448.dart'; +import 'package:moxxmpp/src/xeps/xep_0449.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; -/// Data used to build a message stanza. -/// -/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be -/// added. This is recommended when sharing files but may cause issues when the message -/// stanza should include a SFS element without any fallbacks. -class MessageDetails { - const MessageDetails({ - required this.to, - this.body, - this.requestDeliveryReceipt = false, - this.requestChatMarkers = true, - this.id, - this.originId, - this.quoteBody, - this.quoteId, - this.quoteFrom, - this.chatState, - this.sfs, - this.fun, - this.funReplacement, - this.funCancellation, - this.shouldEncrypt = false, - this.messageRetraction, - this.lastMessageCorrectionId, - this.messageReactions, - this.messageProcessingHints, - this.stickerPackId, - this.setOOBFallbackBody = true, - }); - final String to; +/// A callback that is called whenever a message is sent using +/// [MessageManager.sendMessage]. The input the typed map that is passed to +/// sendMessage. +typedef MessageSendingCallback = List Function( + TypedMap, +); + +/// The raw content of the element. +class MessageBodyData implements StanzaHandlerExtension { + const MessageBodyData(this.body); + + /// The content of the element. final String? body; - final bool requestDeliveryReceipt; - final bool requestChatMarkers; - final String? id; - final String? originId; - final String? quoteBody; - final String? quoteId; - final String? quoteFrom; - final ChatState? chatState; - final StatelessFileSharingData? sfs; - final FileMetadataData? fun; - final String? funReplacement; - final String? funCancellation; - final bool shouldEncrypt; - final MessageRetractionData? messageRetraction; - final String? lastMessageCorrectionId; - final MessageReactions? messageReactions; - final String? stickerPackId; - final List? messageProcessingHints; - final bool setOOBFallbackBody; + + XMLNode toXML() { + return XMLNode( + tag: 'body', + text: body, + ); + } +} + +/// The id attribute of the message stanza. +class MessageIdData implements StanzaHandlerExtension { + const MessageIdData(this.id); + + /// The id attribute of the stanza. + final String id; } class MessageManager extends XmppManagerBase { MessageManager() : super(messageManager); + /// The priority of the message handler. If a handler should run before this one, + /// which emits the [MessageEvent] event and terminates processing, make sure it + /// has a priority greater than [messageHandlerPriority]. + static int messageHandlerPriority = -100; + + /// A list of callbacks that are called when a message is sent in order to add + /// appropriate child elements. + final List _messageSendingCallbacks = + List.empty(growable: true); + + void registerMessageSendingCallback(MessageSendingCallback callback) { + _messageSendingCallbacks.add(callback); + } + @override List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: 'message', callback: _onMessage, - priority: -100, + priority: messageHandlerPriority, ) ]; @@ -94,238 +76,69 @@ class MessageManager extends XmppManagerBase { 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)); - } - 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, - originId: state.originId, - stanzaIds: state.stanzaIds, - 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), + JID.fromString(state.stanza.attributes['from']! as String), + JID.fromString(state.stanza.attributes['to']! as String), + state.stanza.attributes['id']! as String, + state.encrypted, + state.extensions, + type: state.stanza.attributes['type'] as String?, + error: StanzaError.fromStanza(state.stanza), + encryptionError: state.encryptionError, ), ); - return state.copyWith(done: true); + return state..done = true; } - /// Send a message to to with the content body. If deliveryRequest is true, then - /// the message will also request a delivery receipt from the receiver. - /// If id is non-null, then it will be the id of the message stanza. - /// element to this id. If originId is non-null, then it will create an "origin-id" - /// 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, - ), - 'When quoting a message, then quoteFrom and quoteId must also be non-null', - ); - - final stanza = Stanza.message( - to: details.to, - type: 'chat', - id: details.id, - children: [], - ); - - if (details.quoteBody != null) { - final quote = QuoteData.fromBodies(details.quoteBody!, details.body!); - - stanza - ..addChild( - XMLNode(tag: 'body', text: quote.body), - ) - ..addChild( - XMLNode.xmlns( - tag: 'reply', - xmlns: replyXmlns, - attributes: {'to': details.quoteFrom!, 'id': details.quoteId!}, - ), - ) - ..addChild( - XMLNode.xmlns( - tag: 'fallback', - xmlns: fallbackXmlns, - attributes: {'for': replyXmlns}, - children: [ - XMLNode( - tag: 'body', - attributes: { - 'start': '0', - 'end': '${quote.fallbackLength}', - }, - ) - ], - ), - ); - } else { - var body = details.body; - if (details.sfs != null && details.setOOBFallbackBody) { - // TODO(Unknown): Maybe find a better solution - final firstSource = details.sfs!.sources.first; - if (firstSource is StatelessFileSharingUrlSource) { - body = firstSource.url; - } else if (firstSource is StatelessFileSharingEncryptedSource) { - body = firstSource.source.url; - } - } else if (details.messageRetraction?.fallback != null) { - body = details.messageRetraction!.fallback; - } - - if (body != null) { - stanza.addChild( - XMLNode(tag: 'body', text: body), - ); - } - } - - if (details.requestDeliveryReceipt) { - stanza.addChild(makeMessageDeliveryRequest()); - } - if (details.requestChatMarkers) { - stanza.addChild(makeChatMarkerMarkable()); - } - if (details.originId != null) { - stanza.addChild(makeOriginIdElement(details.originId!)); - } - - if (details.sfs != null) { - stanza.addChild(details.sfs!.toXML()); - - final source = details.sfs!.sources.first; - 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, - ), - ); - } - - if (details.fun != null) { - stanza.addChild( - XMLNode.xmlns( - tag: 'file-upload', - xmlns: fileUploadNotificationXmlns, - children: [ - details.fun!.toXML(), - ], - ), - ); - } - - if (details.funReplacement != null) { - stanza.addChild( - XMLNode.xmlns( - tag: 'replaces', - xmlns: fileUploadNotificationXmlns, - attributes: { - 'id': details.funReplacement!, - }, - ), - ); - } - - if (details.messageRetraction != null) { - stanza.addChild( - XMLNode.xmlns( - tag: 'apply-to', - xmlns: fasteningXmlns, - attributes: { - 'id': details.messageRetraction!.id, - }, - children: [ - XMLNode.xmlns( - tag: 'retract', - xmlns: messageRetractionXmlns, - ), - ], - ), - ); - - if (details.messageRetraction!.fallback != null) { - stanza.addChild( - XMLNode.xmlns( - tag: 'fallback', - xmlns: fallbackIndicationXmlns, - ), - ); - } - } - - if (details.lastMessageCorrectionId != null) { - stanza.addChild( - makeLastMessageCorrectionEdit( - details.lastMessageCorrectionId!, - ), - ); - } - - if (details.messageReactions != null) { - stanza.addChild(details.messageReactions!.toXml()); - } - - if (details.messageProcessingHints != null) { - for (final hint in details.messageProcessingHints!) { - stanza.addChild(hint.toXml()); - } - } - - if (details.stickerPackId != null) { - stanza.addChild( - XMLNode.xmlns( - tag: 'sticker', - xmlns: stickersXmlns, - attributes: { - 'pack': details.stickerPackId!, - }, - ), - ); - } - - getAttributes().sendStanza( + /// Send an unawaitable message to [to]. [extensions] is a typed map that contains + /// data for building the message. + Future sendMessage( + JID to, + TypedMap extensions, + ) async { + await getAttributes().sendStanza( StanzaDetails( - stanza, + Stanza.message( + to: to.toString(), + id: extensions.get()?.id, + type: 'chat', + children: _messageSendingCallbacks + .map((c) => c(extensions)) + .flattened + .toList(), + ), awaitable: false, ), ); } + + List _messageSendingCallback( + TypedMap extensions, + ) { + if (extensions.get() != null) { + return []; + } + if (extensions.get() != null) { + return []; + } + if (extensions.get() != null) { + return []; + } + if (extensions.get() != null) { + return []; + } + + final data = extensions.get(); + return data != null ? [data.toXML()] : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + registerMessageSendingCallback(_messageSendingCallback); + } } diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index f0be542..d5bd41f 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -9,6 +9,7 @@ const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'; // RFC 6121 const rosterXmlns = 'jabber:iq:roster'; const rosterVersioningXmlns = 'urn:xmpp:features:rosterver'; +const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval'; // XEP-0004 const dataFormsXmlns = 'jabber:x:data'; @@ -96,7 +97,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0'; // XEP-0372 const referenceXmlns = 'urn:xmpp:reference:0'; -// XEP-380 +// XEP-0380 const emeXmlns = 'urn:xmpp:eme:0'; const emeOtr = 'urn:xmpp:otr:0'; const emeLegacyOpenPGP = 'jabber:x:encrypted'; diff --git a/packages/moxxmpp/lib/src/negotiators/manager.dart b/packages/moxxmpp/lib/src/negotiators/manager.dart deleted file mode 100644 index 8b13789..0000000 --- a/packages/moxxmpp/lib/src/negotiators/manager.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index 17691ec..e4f30d2 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -11,3 +11,4 @@ const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2'; const bind2Negotiator = 'org.moxxmpp.bind2'; const saslFASTNegotiator = 'org.moxxmpp.sasl.fast'; const carbonsNegotiator = 'org.moxxmpp.bind2.carbons'; +const presenceNegotiator = 'org.moxxmpp.core.presence'; diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index 2abf680..f248601 100644 --- a/packages/moxxmpp/lib/src/presence.dart +++ b/packages/moxxmpp/lib/src/presence.dart @@ -6,14 +6,44 @@ import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/negotiators/namespaces.dart'; +import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; /// A function that will be called when presence, outside of subscription request /// management, will be sent. Useful for managers that want to add [XMLNode]s to said /// presence. typedef PresencePreSendCallback = Future> Function(); +/// A pseudo-negotiator that does not really negotiate anything. Instead, its purpose +/// is to look for a stream feature indicating that we can pre-approve subscription +/// requests, shown by [PresenceNegotiator.preApprovalSupported]. +class PresenceNegotiator extends XmppFeatureNegotiatorBase { + PresenceNegotiator() + : super(11, false, subscriptionPreApprovalXmlns, presenceNegotiator); + + /// Flag indicating whether presence subscription pre-approval is supported + bool _supported = false; + bool get preApprovalSupported => _supported; + + @override + Future> negotiate( + XMLNode nonza, + ) async { + _supported = true; + return const Result(NegotiatorState.done); + } + + @override + void reset() { + _supported = false; + + super.reset(); + } +} + /// A mandatory manager that handles initial presence sending, sending of subscription /// request management requests and triggers events for incoming presence stanzas. class PresenceManager extends XmppManagerBase { @@ -23,11 +53,17 @@ class PresenceManager extends XmppManagerBase { final List _presenceCallbacks = List.empty(growable: true); + /// The priority of the presence handler. If a handler should run before this one, + /// which terminates processing, make sure the handler has a priority greater than + /// [presenceHandlerPriority]. + static int presenceHandlerPriority = -100; + @override List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: 'presence', callback: _onPresence, + priority: presenceHandlerPriority, ) ]; @@ -66,7 +102,7 @@ class PresenceManager extends XmppManagerBase { from: JID.fromString(presence.from!), ), ); - return state.copyWith(done: true); + return state..done = true; } default: break; @@ -75,10 +111,7 @@ class PresenceManager extends XmppManagerBase { if (presence.from != null) { logger.finest("Received presence from '${presence.from}'"); - getAttributes().sendEvent( - PresenceReceivedEvent(JID.fromString(presence.from!), presence), - ); - return state.copyWith(done: true); + return state..done = true; } return state; @@ -112,24 +145,82 @@ class PresenceManager extends XmppManagerBase { } /// Send an unavailable presence with no 'to' attribute. - void sendUnavailablePresence() { - getAttributes().sendStanza( + Future sendUnavailablePresence() async { + // Bypass the queue so that this get's sent immediately. + // If we do it like this, we can also block the disconnection + // until we're actually ready. + await getAttributes().sendStanza( StanzaDetails( Stanza.presence( type: 'unavailable', ), awaitable: false, + bypassQueue: true, + excludeFromStreamManagement: true, + ), + ); + } + + /// Similar to [requestSubscription], but it also tells the server to automatically + /// accept a subscription request from [to], should it arrive. + /// This requires a [PresenceNegotiator] to be registered as this feature is optional. + /// + /// Returns true, when the stanza was sent. Returns false, when the stanza was not sent, + /// for example because the server does not support subscription pre-approvals. + Future preApproveSubscription(JID to) async { + final negotiator = getAttributes() + .getNegotiatorById(presenceNegotiator); + assert(negotiator != null, 'No PresenceNegotiator registered'); + + if (!negotiator!.preApprovalSupported) { + return false; + } + + await getAttributes().sendStanza( + StanzaDetails( + Stanza.presence( + type: 'subscribed', + to: to.toString(), + ), + awaitable: false, + ), + ); + return true; + } + + /// Sends a subscription request to [to]. + Future requestSubscription(JID to) async { + await getAttributes().sendStanza( + StanzaDetails( + Stanza.presence( + type: 'subscribe', + to: to.toString(), + ), + awaitable: false, ), ); } - /// Sends a subscription request to [to]. - void sendSubscriptionRequest(String to) { - getAttributes().sendStanza( + /// Accept a subscription request from [to]. + Future acceptSubscriptionRequest(JID to) async { + await getAttributes().sendStanza( StanzaDetails( Stanza.presence( - type: 'subscribe', - to: to, + type: 'subscribed', + to: to.toString(), + ), + awaitable: false, + ), + ); + } + + /// Send a subscription request rejection to [to]. + Future rejectSubscriptionRequest(JID to) async { + await getAttributes().sendStanza( + StanzaDetails( + Stanza.presence( + type: 'unsubscribed', + to: to.toString(), ), awaitable: false, ), @@ -137,38 +228,12 @@ class PresenceManager extends XmppManagerBase { } /// Sends an unsubscription request to [to]. - void sendUnsubscriptionRequest(String to) { - getAttributes().sendStanza( + Future unsubscribe(JID to) async { + await getAttributes().sendStanza( StanzaDetails( Stanza.presence( type: 'unsubscribe', - to: to, - ), - awaitable: false, - ), - ); - } - - /// Accept a presence subscription request for [to]. - void sendSubscriptionRequestApproval(String to) { - getAttributes().sendStanza( - StanzaDetails( - Stanza.presence( - type: 'subscribed', - to: to, - ), - awaitable: false, - ), - ); - } - - /// Reject a presence subscription request for [to]. - void sendSubscriptionRequestRejection(String to) { - getAttributes().sendStanza( - StanzaDetails( - Stanza.presence( - type: 'unsubscribed', - to: to, + to: to.toString(), ), awaitable: false, ), diff --git a/packages/moxxmpp/lib/src/roster/roster.dart b/packages/moxxmpp/lib/src/roster/roster.dart index 93ae392..c6d7758 100644 --- a/packages/moxxmpp/lib/src/roster/roster.dart +++ b/packages/moxxmpp/lib/src/roster/roster.dart @@ -145,7 +145,7 @@ class RosterManager extends XmppManagerBase { logger.warning( 'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}', ); - return state.copyWith(done: true); + return state..done = true; } final query = stanza.firstTag('query', xmlns: rosterXmlns)!; @@ -154,7 +154,7 @@ class RosterManager extends XmppManagerBase { if (item == null) { logger.warning('Received empty roster push'); - return state.copyWith(done: true); + return state..done = true; } unawaited( @@ -177,7 +177,7 @@ class RosterManager extends XmppManagerBase { [], ); - return state.copyWith(done: true); + return state..done = true; } /// Shared code between requesting rosters without and with roster versioning, if @@ -223,15 +223,21 @@ class RosterManager extends XmppManagerBase { return Result(result); } - /// Requests the roster following RFC 6121. - Future> requestRoster() async { + /// Requests the roster following RFC 6121. If [useRosterVersion] is set to false, then + /// roster versioning will not be used, even if the server supports it and we have a last + /// known roster version. + Future> requestRoster({ + bool useRosterVersion = true, + }) async { final attrs = getAttributes(); final query = XMLNode.xmlns( tag: 'query', xmlns: rosterXmlns, ); final rosterVersion = await _stateManager.getRosterVersion(); - if (rosterVersion != null && rosterVersioningAvailable()) { + if (rosterVersion != null && + rosterVersioningAvailable() && + useRosterVersion) { query.attributes['ver'] = rosterVersion; } diff --git a/packages/moxxmpp/lib/src/stanza.dart b/packages/moxxmpp/lib/src/stanza.dart index b06267a..0b07f8d 100644 --- a/packages/moxxmpp/lib/src/stanza.dart +++ b/packages/moxxmpp/lib/src/stanza.dart @@ -9,6 +9,8 @@ class StanzaDetails { this.awaitable = true, this.encrypted = false, this.forceEncryption = false, + this.bypassQueue = false, + this.excludeFromStreamManagement = false, }); /// The stanza to send. @@ -23,6 +25,16 @@ class StanzaDetails { final bool encrypted; final bool forceEncryption; + + /// Bypasses being put into the queue. Useful for sending stanzas that must go out + /// now, where it's okay if it does not get sent. + /// This should never have to be set to true. + final bool bypassQueue; + + /// This makes the Stream Management implementation, when available, ignore the stanza, + /// meaning that it gets counted but excluded from resending. + /// This should never have to be set to true. + final bool excludeFromStreamManagement; } /// A simple description of the element that may be inside a stanza diff --git a/packages/moxxmpp/lib/src/util/typed_map.dart b/packages/moxxmpp/lib/src/util/typed_map.dart new file mode 100644 index 0000000..f6a096e --- /dev/null +++ b/packages/moxxmpp/lib/src/util/typed_map.dart @@ -0,0 +1,23 @@ +/// A map, similar to Map, but always uses the type of the value as the key. +class TypedMap { + /// Create an empty typed map. + TypedMap(); + + /// Create a typed map from a list of values. + TypedMap.fromList(List items) { + for (final item in items) { + _data[item.runtimeType] = item; + } + } + + /// The internal mapping of type -> data + final Map _data = {}; + + /// Associate the type of [value] with [value] in the map. + void set(T value) { + _data[T] = value; + } + + /// Return the object of type [T] from the map, if it has been stored. + T? get() => _data[T] as T?; +} 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 7c1caad..14cdbf7 100644 --- a/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart +++ b/packages/moxxmpp/lib/src/xeps/staging/file_upload_notification.dart @@ -2,14 +2,70 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart'; /// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md - const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; +/// Indicates a file upload notification. +class FileUploadNotificationData implements StanzaHandlerExtension { + const FileUploadNotificationData(this.metadata); + + /// The file metadata indicated in the upload notification. + final FileMetadataData metadata; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'file-upload', + xmlns: fileUploadNotificationXmlns, + children: [ + metadata.toXML(), + ], + ); + } +} + +/// Indicates that a file upload has been cancelled. +class FileUploadNotificationCancellationData implements StanzaHandlerExtension { + const FileUploadNotificationCancellationData(this.id); + + /// The id of the upload notifiaction that is cancelled. + final String id; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'cancelled', + xmlns: fileUploadNotificationXmlns, + attributes: { + 'id': id, + }, + ); + } +} + +/// Indicates that a file upload has been completed. +class FileUploadNotificationReplacementData implements StanzaHandlerExtension { + const FileUploadNotificationReplacementData(this.id); + + /// The id of the upload notifiaction that is replaced. + final String id; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'replaces', + xmlns: fileUploadNotificationXmlns, + attributes: { + 'id': id, + }, + ); + } +} + class FileUploadNotificationManager extends XmppManagerBase { FileUploadNotificationManager() : super(fileUploadNotificationManager); @@ -47,11 +103,14 @@ class FileUploadNotificationManager extends XmppManagerBase { ) async { final funElement = message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; - return state.copyWith( - fun: FileMetadataData.fromXML( - funElement.firstTag('file', xmlns: fileMetadataXmlns)!, - ), - ); + return state + ..extensions.set( + FileUploadNotificationData( + FileMetadataData.fromXML( + funElement.firstTag('file', xmlns: fileMetadataXmlns)!, + ), + ), + ); } Future _onFileUploadNotificationReplacementReceived( @@ -60,9 +119,12 @@ class FileUploadNotificationManager extends XmppManagerBase { ) async { final element = message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; - return state.copyWith( - funReplacement: element.attributes['id']! as String, - ); + return state + ..extensions.set( + FileUploadNotificationReplacementData( + element.attributes['id']! as String, + ), + ); } Future _onFileUploadNotificationCancellationReceived( @@ -71,8 +133,42 @@ class FileUploadNotificationManager extends XmppManagerBase { ) async { final element = message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; - return state.copyWith( - funCancellation: element.attributes['id']! as String, - ); + return state + ..extensions.set( + FileUploadNotificationCancellationData( + element.attributes['id']! as String, + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final fun = extensions.get(); + if (fun != null) { + return [fun.toXML()]; + } + + final cancel = extensions.get(); + if (cancel != null) { + return [cancel.toXML()]; + } + + final replace = extensions.get(); + if (replace != null) { + return [replace.toXML()]; + } + + return []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } 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 263cdce..903304e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart @@ -184,7 +184,7 @@ class DiscoManager extends XmppManagerBase { ], ); - return state.copyWith(done: true); + return state..done = true; } await reply( @@ -195,7 +195,7 @@ class DiscoManager extends XmppManagerBase { ], ); - return state.copyWith(done: true); + return state..done = true; } Future _onDiscoItemsRequest( @@ -223,7 +223,7 @@ class DiscoManager extends XmppManagerBase { ], ); - return state.copyWith(done: true); + return state..done = true; } return state; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0054.dart b/packages/moxxmpp/lib/src/xeps/xep_0054.dart index 0245044..19d0717 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0054.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0054.dart @@ -66,11 +66,7 @@ class VCardManager extends XmppManagerBase { final binval = vcardResult.get().photo?.binval; if (binval != null) { getAttributes().sendEvent( - AvatarUpdatedEvent( - jid: from, - base64: binval, - hash: hash, - ), + VCardAvatarUpdatedEvent(JID.fromString(from), binval, hash), ); } else { logger.warning('No avatar data found'); @@ -80,7 +76,7 @@ class VCardManager extends XmppManagerBase { } } - return state.copyWith(done: true); + return state..done = true; } VCardPhoto? _parseVCardPhoto(XMLNode? node) { 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 bc3a32c..c840c37 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart @@ -114,7 +114,7 @@ class PubSubManager extends XmppManagerBase { ), ); - return state.copyWith(done: true); + return state..done = true; } Future _getNodeItemCount(JID jid, String node) async { @@ -179,13 +179,13 @@ class PubSubManager extends XmppManagerBase { return options; } - Future> subscribe(String jid, String node) async { + Future> subscribe(JID jid, String node) async { final attrs = getAttributes(); final result = (await attrs.sendStanza( StanzaDetails( Stanza.iq( type: 'set', - to: jid, + to: jid.toString(), children: [ XMLNode.xmlns( tag: 'pubsub', @@ -222,13 +222,13 @@ class PubSubManager extends XmppManagerBase { return Result(subscription.attributes['subscription'] == 'subscribed'); } - Future> unsubscribe(String jid, String node) async { + Future> unsubscribe(JID jid, String node) async { final attrs = getAttributes(); final result = (await attrs.sendStanza( StanzaDetails( Stanza.iq( type: 'set', - to: jid, + to: jid.toString(), children: [ XMLNode.xmlns( tag: 'pubsub', @@ -398,14 +398,14 @@ class PubSubManager extends XmppManagerBase { } Future>> getItems( - String jid, + JID jid, String node, ) async { final result = (await getAttributes().sendStanza( StanzaDetails( Stanza.iq( type: 'get', - to: jid, + to: jid.toString(), children: [ XMLNode.xmlns( tag: 'pubsub', diff --git a/packages/moxxmpp/lib/src/xeps/xep_0066.dart b/packages/moxxmpp/lib/src/xeps/xep_0066.dart index 61bb4cc..f69ea06 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0066.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0066.dart @@ -2,32 +2,32 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; /// A data class representing the jabber:x:oob tag. -class OOBData { - const OOBData({this.url, this.desc}); +class OOBData implements StanzaHandlerExtension { + const OOBData(this.url, this.desc); + + /// The communicated URL of the OOB data final String? url; + + /// The description of the url. final String? desc; -} -XMLNode constructOOBNode(OOBData data) { - final children = List.empty(growable: true); - - if (data.url != null) { - children.add(XMLNode(tag: 'url', text: data.url)); + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'x', + xmlns: oobDataXmlns, + children: [ + if (url != null) XMLNode(tag: 'url', text: url), + if (desc != null) XMLNode(tag: 'desc', text: desc), + ], + ); } - if (data.desc != null) { - children.add(XMLNode(tag: 'desc', text: data.desc)); - } - - return XMLNode.xmlns( - tag: 'x', - xmlns: oobDataXmlns, - children: children, - ); } class OOBManager extends XmppManagerBase { @@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase { final url = x.firstTag('url'); final desc = x.firstTag('desc'); - return state.copyWith( - oob: OOBData( - url: url?.innerText(), - desc: desc?.innerText(), - ), - ); + return state + ..extensions.set( + OOBData( + url?.innerText(), + desc?.innerText(), + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0084.dart b/packages/moxxmpp/lib/src/xeps/xep_0084.dart index 491a1b5..e676f44 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0084.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0084.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/base.dart'; @@ -15,10 +16,17 @@ abstract class AvatarError {} class UnknownAvatarError extends AvatarError {} -class UserAvatar { - const UserAvatar({required this.base64, required this.hash}); +class UserAvatarData { + const UserAvatarData(this.base64, this.hash); + + /// The base64-encoded avatar data. final String base64; + + /// The SHA-1 hash of the raw avatar data. final String hash; + + /// The raw avatar data. + List get data => base64Decode(base64); } class UserAvatarMetadata { @@ -27,21 +35,44 @@ class UserAvatarMetadata { this.length, this.width, this.height, - this.mime, + this.type, + this.url, ); - /// The amount of bytes in the file + factory UserAvatarMetadata.fromXML(XMLNode node) { + assert( + node.tag == 'metadata' && + node.attributes['xmlns'] == userAvatarMetadataXmlns, + ' element required', + ); + + final width = node.attributes['width'] as String?; + final height = node.attributes['height'] as String?; + return UserAvatarMetadata( + node.attributes['id']! as String, + int.parse(node.attributes['bytes']! as String), + width != null ? int.parse(width) : null, + height != null ? int.parse(height) : null, + node.attributes['type']! as String, + node.attributes['url'] as String?, + ); + } + + /// The amount of bytes in the file. final int length; - /// The identifier of the avatar + /// The identifier of the avatar. final String id; - /// Image proportions - final int width; - final int height; + /// Image proportions. + final int? width; + final int? height; - /// The MIME type of the avatar - final String mime; + /// The URL where the avatar can be found. + final String? url; + + /// The MIME type of the avatar. + final String type; } /// NOTE: This class requires a PubSubManager @@ -51,13 +82,18 @@ class UserAvatarManager extends XmppManagerBase { PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager; + @override + List getDiscoFeatures() => [ + '$userAvatarMetadataXmlns+notify', + ]; + @override Future onXmppEvent(XmppEvent event) async { if (event is PubSubNotificationEvent) { - if (event.item.node != userAvatarDataXmlns) return; + if (event.item.node != userAvatarMetadataXmlns) return; - if (event.item.payload.tag != 'data' || - event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) { + if (event.item.payload.tag != 'metadata' || + event.item.payload.attributes['xmlns'] != userAvatarMetadataXmlns) { logger.warning( 'Received avatar update from ${event.from} but the payload is invalid. Ignoring...', ); @@ -65,10 +101,12 @@ class UserAvatarManager extends XmppManagerBase { } getAttributes().sendEvent( - AvatarUpdatedEvent( - jid: event.from, - base64: event.item.payload.innerText(), - hash: event.item.id, + UserAvatarUpdatedEvent( + JID.fromString(event.from), + event.item.payload + .findTags('metadata', xmlns: userAvatarMetadataXmlns) + .map(UserAvatarMetadata.fromXML) + .toList(), ), ); } @@ -80,7 +118,7 @@ class UserAvatarManager extends XmppManagerBase { /// Requests the avatar from [jid]. Returns the avatar data if the request was /// successful. Null otherwise - Future> getUserAvatar(String jid) async { + Future> getUserAvatar(JID jid) async { final pubsub = _getPubSubManager(); final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns); if (resultsRaw.isType()) return Result(UnknownAvatarError()); @@ -90,9 +128,9 @@ class UserAvatarManager extends XmppManagerBase { final item = results[0]; return Result( - UserAvatar( - base64: item.payload.innerText(), - hash: item.id, + UserAvatarData( + item.payload.innerText(), + item.id, ), ); } @@ -146,7 +184,7 @@ class UserAvatarManager extends XmppManagerBase { 'bytes': metadata.length.toString(), 'height': metadata.height.toString(), 'width': metadata.width.toString(), - 'type': metadata.mime, + 'type': metadata.type, 'id': metadata.id, }, ), @@ -163,14 +201,14 @@ class UserAvatarManager extends XmppManagerBase { } /// Subscribe the data and metadata node of [jid]. - Future> subscribe(String jid) async { + Future> subscribe(JID jid) async { await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); return const Result(true); } /// Unsubscribe the data and metadata node of [jid]. - Future> unsubscribe(String jid) async { + Future> unsubscribe(JID jid) async { await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); return const Result(true); diff --git a/packages/moxxmpp/lib/src/xeps/xep_0085.dart b/packages/moxxmpp/lib/src/xeps/xep_0085.dart index ab28ab1..36a5f2a 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0085.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0085.dart @@ -2,43 +2,59 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -enum ChatState { active, composing, paused, inactive, gone } +enum ChatState implements StanzaHandlerExtension { + active, + composing, + paused, + inactive, + gone; -ChatState chatStateFromString(String raw) { - switch (raw) { - case 'active': - { + factory ChatState.fromName(String state) { + switch (state) { + case 'active': return ChatState.active; - } - case 'composing': - { + case 'composing': return ChatState.composing; - } - case 'paused': - { + case 'paused': return ChatState.paused; - } - case 'inactive': - { + case 'inactive': return ChatState.inactive; - } - case 'gone': - { + case 'gone': return ChatState.gone; - } - default: - { - return ChatState.gone; - } + } + + throw Exception('Invalid chat state $state'); + } + + String toName() { + switch (this) { + case ChatState.active: + return 'active'; + case ChatState.composing: + return 'composing'; + case ChatState.paused: + return 'paused'; + case ChatState.inactive: + return 'inactive'; + case ChatState.gone: + return 'gone'; + } + } + + XMLNode toXML() { + return XMLNode.xmlns( + tag: toName(), + xmlns: chatStateXmlns, + ); } } -String chatStateToString(ChatState state) => state.toString().split('.').last; - class ChatStateManager extends XmppManagerBase { ChatStateManager() : super(chatStateManager); @@ -64,62 +80,55 @@ class ChatStateManager extends XmppManagerBase { 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}'"); - } + try { + state.extensions.set(ChatState.fromName(element.tag)); + } catch (_) { + logger.finest('Ignoring invalid chat state ${element.tag}'); } - return state.copyWith(chatState: chatState); + return state; } /// Send a chat state notification to [to]. You can specify the type attribute /// of the message with [messageType]. - void sendChatState( + Future sendChatState( ChatState state, String to, { String messageType = 'chat', - }) { - final tagName = state.toString().split('.').last; - - getAttributes().sendStanza( + }) async { + await getAttributes().sendStanza( StanzaDetails( Stanza.message( to: to, type: messageType, children: [ - XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns), + state.toXML(), ], ), + awaitable: false, ), ); } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0115.dart b/packages/moxxmpp/lib/src/xeps/xep_0115.dart index 75fefb3..ab6f2ea 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0115.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0115.dart @@ -4,10 +4,13 @@ import 'package:meta/meta.dart'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/base.dart'; +import 'package:moxxmpp/src/managers/data.dart'; +import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; +import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/util/list.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart'; @@ -105,7 +108,20 @@ class EntityCapabilitiesManager extends XmppManagerBase { Future isSupported() async => true; @override - List getDiscoFeatures() => [capsXmlns]; + List getDiscoFeatures() => [ + capsXmlns, + ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'presence', + tagName: 'c', + tagXmlns: capsXmlns, + callback: onPresence, + priority: PresenceManager.presenceHandlerPriority + 1, + ), + ]; /// Computes, if required, the capability hash of the data provided by /// the DiscoManager. @@ -159,33 +175,38 @@ class EntityCapabilitiesManager extends XmppManagerBase { } @visibleForTesting - Future onPresence(PresenceReceivedEvent event) async { - final c = event.presence.firstTag('c', xmlns: capsXmlns); - if (c == null) { - return; + Future onPresence( + Stanza stanza, + StanzaHandlerData state, + ) async { + if (stanza.from == null) { + return state; } + final from = JID.fromString(stanza.from!); + final c = stanza.firstTag('c', xmlns: capsXmlns)!; + final hashFunctionName = c.attributes['hash'] as String?; final capabilityNode = c.attributes['node'] as String?; final ver = c.attributes['ver'] as String?; if (hashFunctionName == null || capabilityNode == null || ver == null) { - return; + return state; } // Check if we know of the hash final isCached = await _cacheLock.synchronized(() => _capHashCache.containsKey(ver)); if (isCached) { - return; + return state; } final dm = getAttributes().getManagerById(discoManager)!; final discoRequest = await dm.discoInfoQuery( - event.jid, + from, node: capabilityNode, ); if (discoRequest.isType()) { - return; + return state; } final discoInfo = discoRequest.get(); @@ -194,13 +215,13 @@ class EntityCapabilitiesManager extends XmppManagerBase { await dm.addCachedDiscoInfo( MapEntry( DiscoCacheKey( - event.jid, + from, null, ), discoInfo, ), ); - return; + return state; } // Validate the disco#info result according to XEP-0115 § 5.4 @@ -214,7 +235,7 @@ class EntityCapabilitiesManager extends XmppManagerBase { logger.warning( 'Malformed disco#info response: More than one equal identity', ); - return; + return state; } } @@ -225,7 +246,7 @@ class EntityCapabilitiesManager extends XmppManagerBase { logger.warning( 'Malformed disco#info response: More than one equal feature', ); - return; + return state; } } @@ -253,7 +274,7 @@ class EntityCapabilitiesManager extends XmppManagerBase { logger.warning( 'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.', ); - return; + return state; } } @@ -268,7 +289,7 @@ class EntityCapabilitiesManager extends XmppManagerBase { logger.warning( 'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value', ); - return; + return state; } // Check if the field type is hidden @@ -297,14 +318,16 @@ class EntityCapabilitiesManager extends XmppManagerBase { if (computedCapabilityHash == ver) { await _cacheLock.synchronized(() { - _jidToCapHashCache[event.jid.toString()] = ver; + _jidToCapHashCache[from.toString()] = ver; _capHashCache[ver] = newDiscoInfo; }); } else { logger.warning( - 'Capability hash mismatch from ${event.jid}: Received "$ver", expected "$computedCapabilityHash".', + 'Capability hash mismatch from $from: Received "$ver", expected "$computedCapabilityHash".', ); } + + return state; } @visibleForTesting @@ -315,9 +338,7 @@ class EntityCapabilitiesManager extends XmppManagerBase { @override Future onXmppEvent(XmppEvent event) async { - if (event is PresenceReceivedEvent) { - unawaited(onPresence(event)); - } else if (event is StreamNegotiationsDoneEvent) { + if (event is StreamNegotiationsDoneEvent) { // Clear the JID to cap. hash mapping. await _cacheLock.synchronized(_jidToCapHashCache.clear); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0184.dart b/packages/moxxmpp/lib/src/xeps/xep_0184.dart index 5a562d2..9bf4cc6 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0184.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0184.dart @@ -4,23 +4,43 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -XMLNode makeMessageDeliveryRequest() { - return XMLNode.xmlns( - tag: 'request', - xmlns: deliveryXmlns, - ); +class MessageDeliveryReceiptData implements StanzaHandlerExtension { + const MessageDeliveryReceiptData(this.receiptRequested); + + /// Indicates whether a delivery receipt is requested or not. + final bool receiptRequested; + + XMLNode toXML() { + assert( + receiptRequested, + 'This method makes little sense with receiptRequested == false', + ); + return XMLNode.xmlns( + tag: 'request', + xmlns: deliveryXmlns, + ); + } } -XMLNode makeMessageDeliveryResponse(String id) { - return XMLNode.xmlns( - tag: 'received', - xmlns: deliveryXmlns, - attributes: {'id': id}, - ); +class MessageDeliveryReceivedData implements StanzaHandlerExtension { + const MessageDeliveryReceivedData(this.id); + + /// The stanza id of the message we received. + final String id; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'received', + xmlns: deliveryXmlns, + attributes: {'id': id}, + ); + } } class MessageDeliveryReceiptManager extends XmppManagerBase { @@ -56,7 +76,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase { Stanza message, StanzaHandlerData state, ) async { - return state.copyWith(deliveryReceiptRequested: true); + return state..extensions.set(const MessageDeliveryReceiptData(true)); } Future _onDeliveryReceiptReceived( @@ -64,16 +84,16 @@ class MessageDeliveryReceiptManager extends XmppManagerBase { 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", - ); + // 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", + // ); - return state.copyWith(done: true); - } - } + // return state.copyWith(done: true); + // } + // } getAttributes().sendEvent( DeliveryReceiptReceivedEvent( @@ -81,6 +101,27 @@ class MessageDeliveryReceiptManager extends XmppManagerBase { id: received.attributes['id']! as String, ), ); - return state.copyWith(done: true); + return state..done = true; + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0191.dart b/packages/moxxmpp/lib/src/xeps/xep_0191.dart index c443d0d..dcb3395 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0191.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0191.dart @@ -70,7 +70,7 @@ class BlockingManager extends XmppManagerBase { ), ); - return state.copyWith(done: true); + return state..done = true; } Future _unblockPush( @@ -92,7 +92,7 @@ class BlockingManager extends XmppManagerBase { ); } - return state.copyWith(done: true); + return state..done = true; } Future block(List items) async { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/types.dart new file mode 100644 index 0000000..903417a --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/types.dart @@ -0,0 +1,8 @@ +import 'package:moxxmpp/src/managers/data.dart'; + +class StreamManagementData implements StanzaHandlerExtension { + const StreamManagementData(this.exclude); + + /// Whether the stanza should be exluded from the StreamManagement's resend queue. + final bool exclude; +} 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 fd3a741..9ed647f 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart @@ -15,6 +15,7 @@ import 'package:moxxmpp/src/xeps/xep_0198/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; import 'package:moxxmpp/src/xeps/xep_0198/state.dart'; +import 'package:moxxmpp/src/xeps/xep_0198/types.dart'; import 'package:synchronized/synchronized.dart'; const xmlUintMax = 4294967296; // 2**32 @@ -399,10 +400,14 @@ class StreamManagementManager extends XmppManagerBase { Stanza stanza, StanzaHandlerData state, ) async { - await _incrementC2S(); - _unackedStanzas[_state.c2s] = stanza; - if (isStreamManagementEnabled()) { + await _incrementC2S(); + + if (state.extensions.get()?.exclude ?? false) { + return state; + } + + _unackedStanzas[_state.c2s] = stanza; await _sendAckRequest(); } @@ -414,6 +419,8 @@ class StreamManagementManager extends XmppManagerBase { _unackedStanzas.clear(); for (final stanza in stanzas) { + logger + .finest('Resending ${stanza.tag} with id ${stanza.attributes["id"]}'); await getAttributes().sendStanza( StanzaDetails( stanza, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0203.dart b/packages/moxxmpp/lib/src/xeps/xep_0203.dart index cc33598..5b8b527 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0203.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0203.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; @@ -7,10 +8,14 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; @immutable -class DelayedDelivery { - const DelayedDelivery(this.from, this.timestamp); +class DelayedDeliveryData implements StanzaHandlerExtension { + const DelayedDeliveryData(this.from, this.timestamp); + + /// The timestamp the message was originally sent. final DateTime timestamp; - final String from; + + /// The JID that originally sent the message. + final JID from; } class DelayedDeliveryManager extends XmppManagerBase { @@ -23,6 +28,8 @@ class DelayedDeliveryManager extends XmppManagerBase { List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: 'message', + tagName: 'delay', + tagXmlns: delayedDeliveryXmlns, callback: _onIncomingMessage, priority: 200, ), @@ -32,14 +39,14 @@ class DelayedDeliveryManager extends XmppManagerBase { Stanza stanza, StanzaHandlerData state, ) async { - final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); - if (delay == null) return state; + final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns)!; - return state.copyWith( - delayedDelivery: DelayedDelivery( - delay.attributes['from']! as String, - DateTime.parse(delay.attributes['stamp']! as String), - ), - ); + return state + ..extensions.set( + DelayedDeliveryData( + JID.fromString(delay.attributes['from']! as String), + DateTime.parse(delay.attributes['stamp']! as String), + ), + ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0280.dart b/packages/moxxmpp/lib/src/xeps/xep_0280.dart index d24e8ca..ddddc72 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0280.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0280.dart @@ -14,6 +14,13 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0297.dart'; import 'package:moxxmpp/src/xeps/xep_0386.dart'; +class CarbonsData implements StanzaHandlerExtension { + const CarbonsData(this.isCarbon); + + /// Indicates whether this message is a carbon. + final bool isCarbon; +} + /// This manager class implements support for XEP-0280. class CarbonsManager extends XmppManagerBase { CarbonsManager() : super(carbonsManager); @@ -77,15 +84,14 @@ class CarbonsManager extends XmppManagerBase { ) 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); + if (!isCarbonValid(from)) return state..done = true; final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!; final carbon = unpackForwarded(forwarded); - return state.copyWith( - isCarbon: true, - stanza: carbon, - ); + return state + ..extensions.set(const CarbonsData(true)) + ..stanza = carbon; } Future _onMessageSent( @@ -94,15 +100,14 @@ class CarbonsManager extends XmppManagerBase { ) 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); + if (!isCarbonValid(from)) return state..done = true; final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!; final carbon = unpackForwarded(forwarded); - return state.copyWith( - isCarbon: true, - stanza: carbon, - ); + return state + ..extensions.set(const CarbonsData(true)) + ..stanza = carbon; } /// Send a request to the server, asking it to enable Message Carbons. diff --git a/packages/moxxmpp/lib/src/xeps/xep_0300.dart b/packages/moxxmpp/lib/src/xeps/xep_0300.dart index 16e8120..f70d104 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0300.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0300.dart @@ -66,7 +66,7 @@ enum HashFunction { return HashFunction.blake2b512; } - throw Exception(); + throw Exception('Invalid hash function $name'); } /// Like [HashFunction.fromName], but returns null if the hash function is unknown diff --git a/packages/moxxmpp/lib/src/xeps/xep_0308.dart b/packages/moxxmpp/lib/src/xeps/xep_0308.dart index 2cd9aa7..4871219 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0308.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0308.dart @@ -2,18 +2,27 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -XMLNode makeLastMessageCorrectionEdit(String id) { - return XMLNode.xmlns( - tag: 'replace', - xmlns: lmcXmlns, - attributes: { - 'id': id, - }, - ); +class LastMessageCorrectionData implements StanzaHandlerExtension { + const LastMessageCorrectionData(this.id); + + /// The id the LMC applies to. + final String id; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'replace', + xmlns: lmcXmlns, + attributes: { + 'id': id, + }, + ); + } } class LastMessageCorrectionManager extends XmppManagerBase { @@ -42,8 +51,30 @@ class LastMessageCorrectionManager extends XmppManagerBase { StanzaHandlerData state, ) async { final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!; - return state.copyWith( - lastMessageCorrectionSid: edit.attributes['id']! as String, - ); + return state + ..extensions.set( + LastMessageCorrectionData(edit.attributes['id']! as String), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0333.dart b/packages/moxxmpp/lib/src/xeps/xep_0333.dart index 76ae8e3..8a7adbb 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0333.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0333.dart @@ -4,27 +4,86 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -XMLNode makeChatMarkerMarkable() { - return XMLNode.xmlns( - tag: 'markable', - xmlns: chatMarkersXmlns, - ); +enum ChatMarker { + received, + displayed, + acknowledged; + + factory ChatMarker.fromName(String name) { + switch (name) { + case 'received': + return ChatMarker.received; + case 'displayed': + return ChatMarker.displayed; + case 'acknowledged': + return ChatMarker.acknowledged; + } + + throw Exception('Invalid chat marker $name'); + } + + XMLNode toXML() { + String tag; + switch (this) { + case ChatMarker.received: + tag = 'received'; + break; + case ChatMarker.displayed: + tag = 'displayed'; + break; + case ChatMarker.acknowledged: + tag = 'acknowledged'; + break; + } + + return XMLNode.xmlns( + tag: tag, + xmlns: chatMarkersXmlns, + ); + } } -XMLNode makeChatMarker(String tag, String id) { - assert( - ['received', 'displayed', 'acknowledged'].contains(tag), - 'Invalid chat marker', - ); - return XMLNode.xmlns( - tag: tag, - xmlns: chatMarkersXmlns, - attributes: {'id': id}, - ); +class MarkableData implements StanzaHandlerExtension { + const MarkableData(this.isMarkable); + + /// Indicates whether the message can be replied to with a chat marker. + final bool isMarkable; + + XMLNode toXML() { + assert(isMarkable, ''); + + return XMLNode.xmlns( + tag: 'markable', + xmlns: chatMarkersXmlns, + ); + } +} + +class ChatMarkerData implements StanzaHandlerExtension { + const ChatMarkerData(this.marker, this.id); + + /// The actual chat state + final ChatMarker marker; + + /// The ID the chat marker applies to + final String id; + + XMLNode toXML() { + final tag = marker.toXML(); + return XMLNode.xmlns( + tag: tag.tag, + xmlns: chatMarkersXmlns, + attributes: { + 'id': id, + }, + ); + } } class ChatMarkerManager extends XmppManagerBase { @@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase { Stanza message, StanzaHandlerData state, ) async { - final marker = message.firstTagByXmlns(chatMarkersXmlns)!; + final element = 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( - from: JID.fromString(message.from!), - type: marker.tag, - id: marker.attributes['id']! as String, - ), - ); + if (element.tag == 'markable') { + return state..extensions.set(const MarkableData(true)); } - return state.copyWith(done: true); + try { + getAttributes().sendEvent( + ChatMarkerEvent( + JID.fromString(message.from!), + ChatMarker.fromName(element.tag), + element.attributes['id']! as String, + ), + ); + } catch (_) { + logger.warning("Unknown message marker '${element.tag}' found."); + } + + return state..done = true; + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final children = List.empty(growable: true); + final marker = extensions.get(); + if (marker != null) { + children.add(marker.toXML()); + } + + final markable = extensions.get(); + if (markable != null) { + children.add(markable.toXML()); + } + + return children; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0334.dart b/packages/moxxmpp/lib/src/xeps/xep_0334.dart index 57b768e..dd76cec 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0334.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0334.dart @@ -1,31 +1,36 @@ +import 'package:moxxmpp/src/managers/base.dart'; +import 'package:moxxmpp/src/managers/data.dart'; +import 'package:moxxmpp/src/managers/handlers.dart'; +import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; enum MessageProcessingHint { noPermanentStore, noStore, noCopies, - store, -} + store; -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; + factory MessageProcessingHint.fromName(String name) { + switch (name) { + 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: $name'); + return MessageProcessingHint.noStore; } - assert(false, 'Invalid Message Processing Hint: ${element.tag}'); - return MessageProcessingHint.noStore; -} - -extension XmlExtension on MessageProcessingHint { - XMLNode toXml() { + XMLNode toXML() { String tag; switch (this) { case MessageProcessingHint.noPermanentStore: @@ -48,3 +53,60 @@ extension XmlExtension on MessageProcessingHint { ); } } + +class MessageProcessingHintData implements StanzaHandlerExtension { + const MessageProcessingHintData(this.hints); + + /// The attached message processing hints. + final List hints; +} + +class MessageProcessingHintManager extends XmppManagerBase { + MessageProcessingHintManager() : super(messageProcessingHintManager); + + @override + Future isSupported() async => true; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'message', + tagXmlns: messageProcessingHintsXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ), + ]; + + Future _onMessage( + Stanza stanza, + StanzaHandlerData state, + ) async { + final elements = stanza.findTagsByXmlns(messageProcessingHintsXmlns); + return state + ..extensions.set( + MessageProcessingHintData( + elements + .map((element) => MessageProcessingHint.fromName(element.tag)) + .toList(), + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null ? data.hints.map((hint) => hint.toXML()).toList() : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0359.dart b/packages/moxxmpp/lib/src/xeps/xep_0359.dart index 649edbe..ed4f07b 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0359.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0359.dart @@ -3,9 +3,11 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; /// Representation of a element. class StanzaId { @@ -20,7 +22,7 @@ class StanzaId { /// The JID the id was generated by. final JID by; - XMLNode toXml() { + XMLNode toXML() { return XMLNode.xmlns( tag: 'stanza-id', xmlns: stableIdXmlns, @@ -32,12 +34,38 @@ class StanzaId { } } -XMLNode makeOriginIdElement(String id) { - return XMLNode.xmlns( - tag: 'origin-id', - xmlns: stableIdXmlns, - attributes: {'id': id}, - ); +class StableIdData implements StanzaHandlerExtension { + const StableIdData(this.originId, this.stanzaIds); + + /// + final String? originId; + + /// Stanza ids + final List? stanzaIds; + + XMLNode toOriginIdElement() { + assert( + originId != null, + 'Can only build the XML element if originId != null', + ); + return XMLNode.xmlns( + tag: 'origin-id', + xmlns: stableIdXmlns, + attributes: {'id': originId!}, + ); + } + + List toXML() { + return [ + if (originId != null) + XMLNode.xmlns( + tag: 'origin-id', + xmlns: stableIdXmlns, + attributes: {'id': originId!}, + ), + if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()), + ]; + } } class StableIdManager extends XmppManagerBase { @@ -86,9 +114,29 @@ class StableIdManager extends XmppManagerBase { .toList(); } - return state.copyWith( - originId: originId, - stanzaIds: stanzaIds, - ); + return state + ..extensions.set( + StableIdData( + originId, + stanzaIds, + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null ? data.toXML() : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0380.dart b/packages/moxxmpp/lib/src/xeps/xep_0380.dart index 9022596..66303a8 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0380.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0380.dart @@ -6,64 +6,64 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -enum ExplicitEncryptionType { +enum ExplicitEncryptionType implements StanzaHandlerExtension { otr, legacyOpenPGP, openPGP, omemo, omemo1, omemo2, - unknown, -} + unknown; -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 ''; + factory ExplicitEncryptionType.fromNamespace(String namespace) { + switch (namespace) { + 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; + } } -} -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; + String toNamespace() { + switch (this) { + 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 ''; + } } -} -/// Create an element with [type] indicating which type of encryption was -/// used. -XMLNode buildEmeElement(ExplicitEncryptionType type) { - return XMLNode.xmlns( - tag: 'encryption', - xmlns: emeXmlns, - attributes: { - 'namespace': _explicitEncryptionTypeToString(type), - }, - ); + /// Create an element with an xmlns indicating what type of encryption was + /// used. + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'encryption', + xmlns: emeXmlns, + attributes: { + 'namespace': toNamespace(), + }, + ); + } } class EmeManager extends XmppManagerBase { @@ -91,10 +91,11 @@ class EmeManager extends XmppManagerBase { ) async { final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; - return state.copyWith( - encryptionType: _explicitEncryptionTypeFromString( - encryption.attributes['namespace']! as String, - ), - ); + return state + ..extensions.set( + ExplicitEncryptionType.fromNamespace( + encryption.attributes['namespace']! as String, + ), + ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart index 0b31d23..038598c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/types.dart @@ -1,6 +1,21 @@ +import 'package:omemo_dart/omemo_dart.dart'; + /// A simple wrapper class for defining elements that should not be encrypted. class DoNotEncrypt { const DoNotEncrypt(this.tag, this.xmlns); + + /// The tag of the element. final String tag; + + /// The xmlns attribute of the element. final String xmlns; } + +/// An encryption error caused by OMEMO. +class OmemoEncryptionError { + const OmemoEncryptionError(this.jids, this.devices); + + /// See omemo_dart's EncryptionResult for info on these fields. + final Map jids; + final Map devices; +} 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 a7fdf54..bdf618d 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart @@ -16,6 +16,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_0060/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; +import 'package:moxxmpp/src/xeps/xep_0203.dart'; import 'package:moxxmpp/src/xeps/xep_0280.dart'; import 'package:moxxmpp/src/xeps/xep_0334.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart'; @@ -276,7 +277,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { // Add a storage hint in case this is a message // Taken from the example at // https://xmpp.org/extensions/xep-0384.html#message-structure-description. - MessageProcessingHint.store.toXml(), + MessageProcessingHint.store.toXML(), ], ), awaitable: false, @@ -363,19 +364,18 @@ abstract class BaseOmemoManager extends XmppManagerBase { logger.finest('Encryption done'); if (!result.isSuccess(2)) { - final other = Map.from(state.other); - other['encryption_error_jids'] = result.jidEncryptionErrors; - other['encryption_error_devices'] = result.deviceEncryptionErrors; - return state.copyWith( - other: other, + return state + ..cancel = true // If we have no device list for toJid, then the contact most likely does not // support OMEMO:2 - cancelReason: result.jidEncryptionErrors[toJid.toString()] + ..cancelReason = result.jidEncryptionErrors[toJid.toString()] is NoKeyMaterialAvailableException ? OmemoNotSupportedForContactException() - : UnknownOmemoError(), - cancel: true, - ); + : UnknownOmemoError() + ..encryptionError = OmemoEncryptionError( + result.jidEncryptionErrors, + result.deviceEncryptionErrors, + ); } final encrypted = _buildEncryptedElement( @@ -389,19 +389,16 @@ abstract class BaseOmemoManager extends XmppManagerBase { if (stanza.tag == 'message') { children // Add EME data - ..add(buildEmeElement(ExplicitEncryptionType.omemo2)) + ..add(ExplicitEncryptionType.omemo2.toXML()) // Add a storage hint in case this is a message // Taken from the example at // https://xmpp.org/extensions/xep-0384.html#message-structure-description. - ..add(MessageProcessingHint.store.toXml()); + ..add(MessageProcessingHint.store.toXML()); } - return state.copyWith( - stanza: state.stanza.copyWith( - children: children, - ), - encrypted: true, - ); + return state + ..stanza = state.stanza.copyWith(children: children) + ..encrypted = true; } /// This function is called whenever a message is to be encrypted. If it returns true, @@ -444,17 +441,19 @@ abstract class BaseOmemoManager extends XmppManagerBase { OmemoIncomingStanza( fromJid.toString(), sid, - state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? + state.extensions + .get() + ?.timestamp + .millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch, keys, payloadElement?.innerText(), ), ); - final other = Map.from(state.other); var children = stanza.children; if (result.error != null) { - other['encryption_error'] = result.error; + state.encryptionError = result.error; } else { children = stanza.children .where( @@ -471,11 +470,9 @@ abstract class BaseOmemoManager extends XmppManagerBase { envelope = XMLNode.fromString(result.payload!); } on XmlParserException catch (_) { logger.warning('Failed to parse envelope payload: ${result.payload!}'); - other['encryption_error'] = InvalidEnvelopePayloadException(); - return state.copyWith( - encrypted: true, - other: other, - ); + return state + ..encrypted = true + ..encryptionError = InvalidEnvelopePayloadException(); } final envelopeChildren = envelope.firstTag('content')?.children; @@ -489,13 +486,13 @@ abstract class BaseOmemoManager extends XmppManagerBase { } if (!checkAffixElements(envelope, stanza.from!, ourJid)) { - other['encryption_error'] = InvalidAffixElementsException(); + state.encryptionError = InvalidAffixElementsException(); } } - return state.copyWith( - encrypted: true, - stanza: Stanza( + return state + ..encrypted = true + ..stanza = Stanza( to: stanza.to, from: stanza.from, id: stanza.id, @@ -503,9 +500,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { children: children, tag: stanza.tag, attributes: Map.from(stanza.attributes), - ), - other: other, - ); + ); } /// Convenience function that attempts to retrieve the raw XML payload from the @@ -516,8 +511,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { JID jid, ) async { final pm = getAttributes().getManagerById(pubsubManager)!; - final result = - await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns); + final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns); if (result.isType()) return Result(UnknownOmemoError()); return Result(result.get>().first.payload); } @@ -543,7 +537,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { ) async { // TODO(Unknown): Should we query the device list first? final pm = getAttributes().getManagerById(pubsubManager)!; - final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); + final bundlesRaw = await pm.getItems(jid, omemoBundlesXmlns); if (bundlesRaw.isType()) return Result(UnknownOmemoError()); final bundles = bundlesRaw @@ -639,7 +633,7 @@ abstract class BaseOmemoManager extends XmppManagerBase { /// Subscribes to the device list PubSub node of [jid]. Future subscribeToDeviceListImpl(String jid) async { final pm = getAttributes().getManagerById(pubsubManager)!; - await pm.subscribe(jid, omemoDevicesXmlns); + await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns); } /// Attempts to find out if [jid] supports omemo:2. diff --git a/packages/moxxmpp/lib/src/xeps/xep_0385.dart b/packages/moxxmpp/lib/src/xeps/xep_0385.dart index fdc6215..70b249b 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0385.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0385.dart @@ -7,7 +7,7 @@ import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; -class StatelessMediaSharingData { +class StatelessMediaSharingData implements StanzaHandlerExtension { const StatelessMediaSharingData({ required this.mediaType, required this.size, @@ -70,7 +70,9 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) { ); } +@Deprecated('Not maintained') class SIMSManager extends XmppManagerBase { + @Deprecated('Not maintained') SIMSManager() : super(simsManager); @override @@ -98,7 +100,7 @@ class SIMSManager extends XmppManagerBase { final references = message.findTags('reference', xmlns: referenceXmlns); for (final ref in references) { final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); - if (sims != null) return state.copyWith(sims: parseSIMSElement(sims)); + if (sims != null) return state..extensions.set(parseSIMSElement(sims)); } return state; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0424.dart b/packages/moxxmpp/lib/src/xeps/xep_0424.dart index a9cfc6f..859d84b 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0424.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0424.dart @@ -2,12 +2,19 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -class MessageRetractionData { +class MessageRetractionData implements StanzaHandlerExtension { MessageRetractionData(this.id, this.fallback); + + /// A potential fallback message to set the body to when retracting. final String? fallback; + + /// The id of the message that is retracted. final String id; } @@ -47,11 +54,55 @@ class MessageRetractionManager extends XmppManagerBase { final isFallbackBody = message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; - return state.copyWith( - messageRetraction: MessageRetractionData( - applyTo.attributes['id']! as String, - isFallbackBody ? message.firstTag('body')?.innerText() : null, - ), - ); + return state + ..extensions.set( + MessageRetractionData( + applyTo.attributes['id']! as String, + isFallbackBody ? message.firstTag('body')?.innerText() : null, + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + XMLNode.xmlns( + tag: 'apply-to', + xmlns: fasteningXmlns, + attributes: { + 'id': data.id, + }, + children: [ + XMLNode.xmlns( + tag: 'retract', + xmlns: messageRetractionXmlns, + ), + ], + ), + if (data.fallback != null) + XMLNode( + tag: 'body', + text: data.fallback, + ), + if (data.fallback != null) + XMLNode.xmlns( + tag: 'fallback', + xmlns: fallbackIndicationXmlns, + ), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0444.dart b/packages/moxxmpp/lib/src/xeps/xep_0444.dart index 15448ea..cfa844c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0444.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0444.dart @@ -2,16 +2,18 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -class MessageReactions { - const MessageReactions(this.messageId, this.emojis); +class MessageReactionsData implements StanzaHandlerExtension { + const MessageReactionsData(this.messageId, this.emojis); final String messageId; final List emojis; - XMLNode toXml() { + XMLNode toXML() { return XMLNode.xmlns( tag: 'reactions', xmlns: messageReactionsXmlns, @@ -55,14 +57,36 @@ class MessageReactionsManager extends XmppManagerBase { ) 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(), - ), - ); + return state + ..extensions.set( + MessageReactionsData( + reactionsElement.attributes['id']! as String, + reactionsElement.children + .where((c) => c.tag == 'reaction') + .map((c) => c.innerText()) + .toList(), + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0447.dart b/packages/moxxmpp/lib/src/xeps/xep_0447.dart index 81ee7d8..baed4f4 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0447.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0447.dart @@ -3,9 +3,12 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; +import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart'; @@ -70,8 +73,12 @@ List processStatelessFileSharingSources( return sources; } -class StatelessFileSharingData { - const StatelessFileSharingData(this.metadata, this.sources); +class StatelessFileSharingData implements StanzaHandlerExtension { + const StatelessFileSharingData( + this.metadata, + this.sources, { + this.includeOOBFallback = false, + }); /// Parse [node] as a StatelessFileSharingData element. factory StatelessFileSharingData.fromXML(XMLNode node) { @@ -88,6 +95,10 @@ class StatelessFileSharingData { final FileMetadataData metadata; final List sources; + /// Flag indicating whether an OOB fallback should be set. The value is only + /// relevant in the context of the messageSendingCallback. + final bool includeOOBFallback; + XMLNode toXML() { return XMLNode.xmlns( tag: 'file-sharing', @@ -122,23 +133,54 @@ class SFSManager extends XmppManagerBase { tagXmlns: sfsXmlns, callback: _onMessage, // Before the message handler - priority: -99, + priority: -98, ) ]; @override Future isSupported() async => true; + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + if (data == null) { + return []; + } + + // TODO(Unknown): Consider all sources? + final source = data.sources.first; + OOBData? oob; + if (source is StatelessFileSharingUrlSource && data.includeOOBFallback) { + // SFS recommends OOB as a fallback + oob = OOBData(source.url, null); + } + + return [ + data.toXML(), + if (oob != null) oob.toXML(), + ]; + } + Future _onMessage( Stanza message, StanzaHandlerData state, ) async { final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; - return state.copyWith( - sfs: StatelessFileSharingData.fromXML( - sfs, - ), - ); + return state + ..extensions.set( + StatelessFileSharingData.fromXML(sfs), + ); + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0449.dart b/packages/moxxmpp/lib/src/xeps/xep_0449.dart index fb95432..b32aa3c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0449.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0449.dart @@ -4,11 +4,13 @@ import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0300.dart'; @@ -227,6 +229,19 @@ class StickerPack { } } +class StickersData implements StanzaHandlerExtension { + const StickersData(this.stickerPackId, this.sticker, {this.addBody = true}); + + /// The id of the sticker pack the referenced sticker is from. + final String stickerPackId; + + /// The metadata of the sticker. + final StatelessFileSharingData sticker; + + /// If true, sets the sticker's metadata desc attribute as the message body. + final bool addBody; +} + class StickersManager extends XmppManagerBase { StickersManager() : super(stickersManager); @@ -249,9 +264,35 @@ class StickersManager extends XmppManagerBase { StanzaHandlerData state, ) async { final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; - return state.copyWith( - stickerPackId: sticker.attributes['pack']! as String, - ); + return state + ..extensions.set( + StickersData( + sticker.attributes['pack']! as String, + state.extensions.get()!, + ), + ); + } + + List _messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + XMLNode.xmlns( + tag: 'sticker', + xmlns: stickersXmlns, + attributes: { + 'pack': data.stickerPackId, + }, + ), + data.sticker.toXML(), + + // Add a body + if (data.addBody && data.sticker.metadata.desc != null) + MessageBodyData(data.sticker.metadata.desc).toXML(), + ] + : []; } /// Publishes the StickerPack [pack] to the PubSub node of [jid]. If specified, then @@ -319,4 +360,14 @@ class StickersManager extends XmppManagerBase { return Result(stickerPack); } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(_messageSendingCallback); + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0461.dart b/packages/moxxmpp/lib/src/xeps/xep_0461.dart index 11ee5bf..644b98a 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0461.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0461.dart @@ -1,24 +1,38 @@ -import 'package:meta/meta.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/message.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; -/// Data summarizing the XEP-0461 data. -class ReplyData { - const ReplyData({ - required this.id, - this.to, +/// A reply to a message. +class ReplyData implements StanzaHandlerExtension { + const ReplyData( + this.id, { + this.body, + this.jid, this.start, this.end, }); - /// The bare JID to whom the reply applies to - final String? to; + ReplyData.fromQuoteData( + this.id, + QuoteData quote, { + this.jid, + }) : body = quote.body, + start = 0, + end = quote.fallbackLength; - /// The stanza ID of the message that is replied to + /// The JID of the entity whose message we are replying to. + final JID? jid; + + /// The id of the message that is replied to. What id to use depends on what kind + /// of message you want to reply to. final String id; /// The start of the fallback body (inclusive) @@ -27,18 +41,21 @@ class ReplyData { /// The end of the fallback body (exclusive) final int? end; + /// The body of the message. + final String? body; + /// Applies the metadata to the received body [body] in order to remove the fallback. /// If either [ReplyData.start] or [ReplyData.end] are null, then body is returned as /// is. - String removeFallback(String body) { + String? get withoutFallback { + if (body == null) return null; if (start == null || end == null) return body; - return body.replaceRange(start!, end, ''); + return body!.replaceRange(start!, end, ''); } } /// Internal class describing how to build a message with a quote fallback body. -@visibleForTesting class QuoteData { const QuoteData(this.body, this.fallbackLength); @@ -85,13 +102,54 @@ class MessageRepliesManager extends XmppManagerBase { @override Future isSupported() async => true; + @visibleForTesting + List messageSendingCallback( + TypedMap extensions, + ) { + final data = extensions.get(); + return data != null + ? [ + XMLNode.xmlns( + tag: 'reply', + xmlns: replyXmlns, + attributes: { + // The to attribute is optional + if (data.jid != null) 'to': data.jid!.toString(), + + 'id': data.id, + }, + ), + if (data.body != null) + XMLNode( + tag: 'body', + text: data.body, + ), + if (data.body != null) + XMLNode.xmlns( + tag: 'fallback', + xmlns: fallbackXmlns, + attributes: {'for': replyXmlns}, + children: [ + XMLNode( + tag: 'body', + attributes: { + 'start': data.start!.toString(), + 'end': data.end!.toString(), + }, + ), + ], + ), + ] + : []; + } + 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?; + final jid = to != null ? JID.fromString(to) : null; int? start; int? end; @@ -103,13 +161,25 @@ class MessageRepliesManager extends XmppManagerBase { end = int.parse(body.attributes['end']! as String); } - return state.copyWith( - reply: ReplyData( - id: id, - to: to, - start: start, - end: end, - ), - ); + return state + ..extensions.set( + ReplyData( + reply.attributes['id']! as String, + jid: jid, + start: start, + end: end, + body: stanza.firstTag('body')?.innerText(), + ), + ); + } + + @override + Future postRegisterCallback() async { + await super.postRegisterCallback(); + + // Register the sending callback + getAttributes() + .getManagerById(messageManager) + ?.registerMessageSendingCallback(messageSendingCallback); } } diff --git a/packages/moxxmpp/pubspec.yaml b/packages/moxxmpp/pubspec.yaml index aae04d3..3198a24 100644 --- a/packages/moxxmpp/pubspec.yaml +++ b/packages/moxxmpp/pubspec.yaml @@ -1,6 +1,6 @@ name: moxxmpp description: A pure-Dart XMPP library -version: 0.3.2 +version: 0.4.0 homepage: https://codeberg.org/moxxy/moxxmpp publish_to: https://git.polynom.me/api/packages/Moxxy/pub diff --git a/packages/moxxmpp/test/helpers/manager.dart b/packages/moxxmpp/test/helpers/manager.dart index af64e56..b50381b 100644 --- a/packages/moxxmpp/test/helpers/manager.dart +++ b/packages/moxxmpp/test/helpers/manager.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connectivity.dart'; +import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/handlers/client.dart'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/reconnect.dart'; import 'package:moxxmpp/src/settings.dart'; -import 'package:moxxmpp/src/socket.dart'; +import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import '../helpers/xmpp.dart'; @@ -15,15 +16,15 @@ import '../helpers/xmpp.dart'; /// This class allows registering managers for easier testing. class TestingManagerHolder { TestingManagerHolder({ - BaseSocketWrapper? socket, - }) : _socket = socket ?? StubTCPSocket([]); + StubTCPSocket? stubSocket, + }) : socket = stubSocket ?? StubTCPSocket([]); - final BaseSocketWrapper _socket; + final StubTCPSocket socket; final Map _managers = {}; - // The amount of stanzas sent - int sentStanzas = 0; + /// A list of events that were triggered. + final List sentEvents = List.empty(growable: true); static final JID jid = JID.fromString('testuser@example.org/abc123'); static final ConnectionSettings settings = ConnectionSettings( @@ -31,42 +32,40 @@ class TestingManagerHolder { password: 'abc123', ); - Future _sendStanza( - stanza, { - bool addId = true, - bool awaitable = true, - bool encrypted = false, - bool forceEncryption = false, - }) async { - sentStanzas++; - return XMLNode.fromString(''); + Future _sendStanza(StanzaDetails details) async { + socket.write(details.stanza.toXml()); + return null; } T? _getManagerById(String id) { return _managers[id] as T?; } - Future register(XmppManagerBase manager) async { - manager.register( - XmppManagerAttributes( - sendStanza: _sendStanza, - getConnection: () => XmppConnection( - TestingReconnectionPolicy(), - AlwaysConnectedConnectivityManager(), - ClientToServerNegotiator(), - _socket, + Future register(List managers) async { + for (final manager in managers) { + manager.register( + XmppManagerAttributes( + sendStanza: _sendStanza, + getConnection: () => XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + socket, + ), + getConnectionSettings: () => settings, + sendNonza: (_) {}, + sendEvent: sentEvents.add, + getSocket: () => socket, + getNegotiatorById: getNegotiatorNullStub, + getFullJID: () => jid, + getManagerById: _getManagerById, ), - getConnectionSettings: () => settings, - sendNonza: (_) {}, - sendEvent: (_) {}, - getSocket: () => _socket, - getNegotiatorById: getNegotiatorNullStub, - getFullJID: () => jid, - getManagerById: _getManagerById, - ), - ); + ); + _managers[manager.id] = manager; + } - await manager.postRegisterCallback(); - _managers[manager.id] = manager; + for (final manager in managers) { + await manager.postRegisterCallback(); + } } } diff --git a/packages/moxxmpp/test/stanzahandler_test.dart b/packages/moxxmpp/test/stanzahandler_test.dart index 3c3bae8..cb62eee 100644 --- a/packages/moxxmpp/test/stanzahandler_test.dart +++ b/packages/moxxmpp/test/stanzahandler_test.dart @@ -16,8 +16,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), ); @@ -38,8 +38,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagXmlns: 'owo', ); @@ -59,8 +59,8 @@ void main() { return StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ); }, stanzaTag: 'iq', @@ -77,8 +77,8 @@ void main() { StanzaHandlerData( false, false, - null, stanza2, + TypedMap(), ), ); expect(run, true); @@ -89,8 +89,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagName: 'tag', ); @@ -107,8 +107,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagName: 'tag', stanzaTag: 'iq', @@ -127,8 +127,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), xmlns: componentAcceptXmlns, ); @@ -147,8 +147,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagName: '1', priority: 100, @@ -157,8 +157,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagName: '2', ), @@ -166,8 +166,8 @@ void main() { callback: (stanza, _) async => StanzaHandlerData( true, false, - null, stanza, + TypedMap(), ), tagName: '3', priority: 50, diff --git a/packages/moxxmpp/test/type_map_test.dart b/packages/moxxmpp/test/type_map_test.dart new file mode 100644 index 0000000..e214e56 --- /dev/null +++ b/packages/moxxmpp/test/type_map_test.dart @@ -0,0 +1,39 @@ +import 'package:moxxmpp/src/util/typed_map.dart'; +import 'package:test/test.dart'; + +abstract class BaseType {} + +class TestType1 implements BaseType { + const TestType1(this.i); + final int i; +} + +class TestType2 implements BaseType { + const TestType2(this.j); + final bool j; +} + +void main() { + test('Test storing data in the type map', () { + // Set + final map = TypedMap() + ..set(const TestType1(1)) + ..set(const TestType2(false)); + + // And access + expect(map.get()?.i, 1); + expect(map.get()?.j, false); + }); + + test('Test storing data in the type map using a list', () { + // Set + final map = TypedMap.fromList([ + const TestType1(1), + const TestType2(false), + ]); + + // And access + expect(map.get()?.i, 1); + expect(map.get()?.j, false); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index 134d9e6..69f8d24 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -120,8 +120,7 @@ void main() { final ecm = EntityCapabilitiesManager(''); final dm = DiscoManager([]); - await tm.register(dm); - await tm.register(ecm); + await tm.register([dm, ecm]); // Inject a capability hash into the cache final aliceJid = JID.fromString('alice@example.org/abc123'); @@ -140,7 +139,7 @@ void main() { // Query Alice's device final result = await dm.discoInfoQuery(aliceJid); expect(result.isType(), false); - expect(tm.sentStanzas, 0); + expect(tm.socket.getState(), 0); }); }); } diff --git a/packages/moxxmpp/test/xeps/xep_0060_test.dart b/packages/moxxmpp/test/xeps/xep_0060_test.dart index 0f1795b..da31d14 100644 --- a/packages/moxxmpp/test/xeps/xep_0060_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0060_test.dart @@ -55,8 +55,10 @@ void main() { () async { final manager = PubSubManager(); final tm = TestingManagerHolder(); - await tm.register(StubbedDiscoManager(false)); - await tm.register(manager); + await tm.register([ + StubbedDiscoManager(false), + manager, + ]); final result = await manager.preprocessPublishOptions( JID.fromString('pubsub.server.example.org'), @@ -72,8 +74,10 @@ void main() { () async { final manager = PubSubManager(); final tm = TestingManagerHolder(); - await tm.register(StubbedDiscoManager(true)); - await tm.register(manager); + await tm.register([ + StubbedDiscoManager(true), + manager, + ]); final result = await manager.preprocessPublishOptions( JID.fromString('pubsub.server.example.org'), @@ -168,7 +172,6 @@ void main() { PubSubManager(), DiscoManager([]), PresenceManager(), - MessageManager(), RosterManager(TestingRosterStateManager(null, [])), ]); await connection.registerFeatureNegotiators([ diff --git a/packages/moxxmpp/test/xeps/xep_0115_test.dart b/packages/moxxmpp/test/xeps/xep_0115_test.dart index 6febc7c..10ef205 100644 --- a/packages/moxxmpp/test/xeps/xep_0115_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0115_test.dart @@ -319,27 +319,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register(StubbedDiscoManager()); - await tm.register(manager); + await tm.register([ + StubbedDiscoManager(), + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( @@ -352,27 +353,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register(StubbedDiscoManager()); - await tm.register(manager); + await tm.register([ + StubbedDiscoManager(), + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94AAAAA=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94AAAAA=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( @@ -385,29 +387,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..multipleEqualIdentities = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( @@ -420,29 +421,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..multipleEqualFeatures = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( @@ -455,29 +455,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..multipleExtendedFormsWithSameType = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( @@ -490,29 +489,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..invalidExtension1 = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid); @@ -527,29 +525,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..invalidExtension2 = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid); @@ -564,29 +561,28 @@ void main() { final tm = TestingManagerHolder(); final manager = EntityCapabilitiesManager(''); - await tm.register( + await tm.register([ StubbedDiscoManager()..invalidExtension3 = true, - ); - await tm.register(manager); + manager, + ]); - await manager.onPresence( - PresenceReceivedEvent( - aliceJid, - Stanza.presence( - from: aliceJid.toString(), - children: [ - XMLNode.xmlns( - tag: 'c', - xmlns: capsXmlns, - attributes: { - 'hash': 'sha-1', - 'node': 'http://example.org/client', - 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', - }, - ), - ], + final stanza = Stanza.presence( + from: aliceJid.toString(), + children: [ + XMLNode.xmlns( + tag: 'c', + xmlns: capsXmlns, + attributes: { + 'hash': 'sha-1', + 'node': 'http://example.org/client', + 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=', + }, ), - ), + ], + ); + await manager.onPresence( + stanza, + StanzaHandlerData(false, false, stanza, TypedMap()), ); expect( diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 1ba8b78..24f0c9a 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -15,8 +15,8 @@ Future runIncomingStanzaHandlers( StanzaHandlerData( false, false, - null, stanza, + TypedMap(), ), ); } @@ -34,8 +34,8 @@ Future runOutgoingStanzaHandlers( StanzaHandlerData( false, false, - null, stanza, + TypedMap(), ), ); } diff --git a/packages/moxxmpp/test/xeps/xep_0334_test.dart b/packages/moxxmpp/test/xeps/xep_0334_test.dart new file mode 100644 index 0000000..3bb1a04 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0334_test.dart @@ -0,0 +1,147 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +import '../helpers/manager.dart'; +import '../helpers/xmpp.dart'; + +void main() { + test('Test receiving a message processing hint', () async { + final fakeSocket = StubTCPSocket( + [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + ], + ); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + fakeSocket, + )..connectionSettings = ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + ); + await conn.registerManagers([ + MessageManager(), + MessageProcessingHintManager(), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + await conn.connect( + shouldReconnect: false, + enableReconnectOnSuccess: false, + waitUntilLogin: true, + ); + + MessageEvent? messageEvent; + conn.asBroadcastStream().listen((event) { + if (event is MessageEvent) { + messageEvent = event; + } + }); + + // Send the fake message + fakeSocket.injectRawXml( + ''' + + + + +''', + ); + + await Future.delayed(const Duration(seconds: 2)); + expect( + messageEvent!.extensions + .get()! + .hints + .contains(MessageProcessingHint.noCopies), + true, + ); + expect( + messageEvent!.extensions + .get()! + .hints + .contains(MessageProcessingHint.noStore), + true, + ); + }); + + test('Test sending a message processing hint', () async { + final manager = MessageManager(); + final holder = TestingManagerHolder( + stubSocket: StubTCPSocket([ + StanzaExpectation( + ''' + + + + +''', + '', + ) + ]), + ); + + await holder.register([ + manager, + MessageProcessingHintManager(), + ]); + + await manager.sendMessage( + JID.fromString('user@example.org'), + TypedMap() + ..set( + const MessageProcessingHintData([ + MessageProcessingHint.noCopies, + MessageProcessingHint.noStore, + ]), + ), + ); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0447.dart b/packages/moxxmpp/test/xeps/xep_0447_test.dart similarity index 87% rename from packages/moxxmpp/test/xeps/xep_0447.dart rename to packages/moxxmpp/test/xeps/xep_0447_test.dart index 8459fb9..d24e627 100644 --- a/packages/moxxmpp/test/xeps/xep_0447.dart +++ b/packages/moxxmpp/test/xeps/xep_0447_test.dart @@ -13,7 +13,7 @@ void main() { 3032449 4096x2160 2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU= - 2AfMGH8O7UNPTvUVAM9aK13mpCY= + 2AfMGH8O7UNPTvUVAM9aK13mpCY= Photo from the summit. @@ -28,11 +28,11 @@ void main() { ); expect( - sfs.metadata.hashes['sha3-256'], + sfs.metadata.hashes[HashFunction.sha3_256], '2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=', ); expect( - sfs.metadata.hashes['id-blake2b256'], + sfs.metadata.hashes[HashFunction.blake2b256], '2AfMGH8O7UNPTvUVAM9aK13mpCY=', ); }); diff --git a/packages/moxxmpp/test/xeps/xep_0449_test.dart b/packages/moxxmpp/test/xeps/xep_0449_test.dart index 7e4165c..7c90d61 100644 --- a/packages/moxxmpp/test/xeps/xep_0449_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0449_test.dart @@ -1,7 +1,13 @@ import 'package:moxxmpp/moxxmpp.dart'; import 'package:test/test.dart'; +import '../helpers/logging.dart'; +import '../helpers/manager.dart'; +import '../helpers/xmpp.dart'; + void main() { + initLogger(); + test('Test parsing a large sticker pack', () { // Example sticker pack based on the "miho" sticker pack by Movim final rawPack = XMLNode.fromString(''' @@ -225,4 +231,186 @@ void main() { expect(pack.stickers.length, 16); }); + + test('Test sending a sticker', () async { + final manager = MessageManager(); + final holder = TestingManagerHolder( + stubSocket: StubTCPSocket([ + StanzaExpectation( + // Example taken from https://xmpp.org/extensions/xep-0449.html#send + // - Replaced with and + ''' + + + + + image/png + 😘 + 67016 + 512 + 512 + gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY= + + + + + + +''', + '', + ), + ]), + ); + await holder.register([ + manager, + StickersManager(), + SFSManager(), + ]); + + await manager.sendMessage( + JID.fromString('user@example.org'), + TypedMap() + ..set( + StickersData( + 'EpRv28DHHzFrE4zd+xaNpVb4', + StatelessFileSharingData( + const FileMetadataData( + mediaType: 'image/png', + desc: '😘', + size: 67016, + width: 512, + height: 512, + hashes: { + HashFunction.sha256: + 'gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=', + }, + thumbnails: [], + ), + [ + StatelessFileSharingUrlSource( + 'https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png', + ), + ], + ), + ), + ), + ); + await Future.delayed(const Duration(seconds: 1)); + + expect(holder.socket.getState(), 1); + }); + + test('Test receiving a sticker', () async { + final fakeSocket = StubTCPSocket( + [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + ], + ); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + fakeSocket, + )..connectionSettings = ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + ); + await conn.registerManagers([ + MessageManager(), + SFSManager(), + StickersManager(), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + await conn.connect( + shouldReconnect: false, + enableReconnectOnSuccess: false, + waitUntilLogin: true, + ); + + MessageEvent? messageEvent; + conn.asBroadcastStream().listen((event) { + if (event is MessageEvent) { + messageEvent = event; + } + }); + + // Send the fake message + fakeSocket.injectRawXml( + ''' + + + + + image/png + 😘 + 67016 + 512 + 512 + gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY= + + + + + + +''', + ); + + await Future.delayed(const Duration(seconds: 2)); + final sticker = messageEvent!.extensions.get()!; + final sfs = messageEvent!.extensions.get()!; + expect(sticker.stickerPackId, 'EpRv28DHHzFrE4zd+xaNpVb4'); + expect(sfs.metadata.desc, '😘'); + expect( + sfs.sources.first is StatelessFileSharingUrlSource, + true, + ); + }); } diff --git a/packages/moxxmpp/test/xeps/xep_0461_test.dart b/packages/moxxmpp/test/xeps/xep_0461_test.dart index ca4b4d2..2b2c26d 100644 --- a/packages/moxxmpp/test/xeps/xep_0461_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0461_test.dart @@ -1,6 +1,8 @@ import 'package:moxxmpp/moxxmpp.dart'; import 'package:test/test.dart'; +import '../helpers/xmpp.dart'; + void main() { test('Test building a singleline quote', () { final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!'); @@ -20,28 +22,250 @@ void main() { }); test('Applying a singleline quote', () { - const body = '> Hallo Welt\nHello right back!'; const reply = ReplyData( - to: '', - id: '', + '', start: 0, end: 13, + body: '> Hallo Welt\nHello right back!', ); - final bodyWithoutFallback = reply.removeFallback(body); - expect(bodyWithoutFallback, 'Hello right back!'); + expect(reply.withoutFallback, 'Hello right back!'); }); test('Applying a multiline quote', () { - const body = "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!"; const reply = ReplyData( - to: '', - id: '', + '', start: 0, end: 28, + body: "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!", ); - final bodyWithoutFallback = reply.removeFallback(body); - expect(bodyWithoutFallback, "I'm fine.\nThank you!"); + expect(reply.withoutFallback, "I'm fine.\nThank you!"); + }); + + test('Test calling the message sending callback', () { + final result = MessageRepliesManager().messageSendingCallback( + TypedMap() + ..set( + ReplyData.fromQuoteData( + 'some-random-id', + QuoteData.fromBodies( + 'Hello world', + 'How are you doing?', + ), + jid: JID.fromString('quoted-user@example.org'), + ), + ), + ); + + final reply = result.firstWhere((e) => e.tag == 'reply'); + final body = result.firstWhere((e) => e.tag == 'body'); + final fallback = result.firstWhere((e) => e.tag == 'fallback'); + + expect(reply.attributes['to'], 'quoted-user@example.org'); + expect(body.innerText(), '> Hello world\nHow are you doing?'); + expect(fallback.children.first.attributes['start'], '0'); + expect(fallback.children.first.attributes['end'], '14'); + }); + + test('Test parsing a reply without fallback', () async { + final fakeSocket = StubTCPSocket( + [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + ], + ); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + fakeSocket, + )..connectionSettings = ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + ); + await conn.registerManagers([ + MessageManager(), + MessageRepliesManager(), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + await conn.connect( + shouldReconnect: false, + enableReconnectOnSuccess: false, + waitUntilLogin: true, + ); + + MessageEvent? messageEvent; + conn.asBroadcastStream().listen((event) { + if (event is MessageEvent) { + messageEvent = event; + } + }); + + // Send the fake message + fakeSocket.injectRawXml( + ''' + + Great idea! + + +''', + ); + + await Future.delayed(const Duration(seconds: 2)); + final reply = messageEvent!.extensions.get()!; + expect(reply.withoutFallback, 'Great idea!'); + expect(reply.id, 'message-id1'); + expect(reply.jid, JID.fromString('anna@example.com/tablet')); + expect(reply.start, null); + expect(reply.end, null); + }); + + test('Test parsing a reply with a fallback', () async { + final fakeSocket = StubTCPSocket( + [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + ], + ); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + fakeSocket, + )..connectionSettings = ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + ); + await conn.registerManagers([ + MessageManager(), + MessageRepliesManager(), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + await conn.connect( + shouldReconnect: false, + enableReconnectOnSuccess: false, + waitUntilLogin: true, + ); + + MessageEvent? messageEvent; + conn.asBroadcastStream().listen((event) { + if (event is MessageEvent) { + messageEvent = event; + } + }); + + // Send the fake message + fakeSocket.injectRawXml( + ''' + + > Anna wrote:\n> We should bake a cake\nGreat idea! + + + + + +''', + ); + + await Future.delayed(const Duration(seconds: 2)); + final reply = messageEvent!.extensions.get()!; + expect(reply.withoutFallback, 'Great idea!'); + expect(reply.id, 'message-id1'); + expect(reply.jid, JID.fromString('anna@example.com/laptop')); + expect(reply.start, 0); + expect(reply.end, 38); }); } diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index 50675f4..65bf3e1 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -78,8 +78,8 @@ Future testRosterManager( StanzaHandlerData( false, false, - null, stanza, + TypedMap(), ), ); } @@ -335,8 +335,8 @@ void main() { StanzaHandlerData( false, false, - null, maliciousStanza, + TypedMap(), ), ); } @@ -793,4 +793,20 @@ void main() { expect(fakeSocket.getState(), 9); expect(await stanzaFuture != null, true); }); + + test('Test subscription pre-approval parsing', () async { + final rawFeatures = XMLNode.fromString( + ''' + + + + + + + + ''', + ); + + expect(PresenceNegotiator().matchesFeature(rawFeatures.children), true); + }); }