diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 66d5009..c57e057 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -73,6 +73,7 @@ export 'package:moxxmpp/src/xeps/xep_0384/types.dart'; export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart'; export 'package:moxxmpp/src/xeps/xep_0385.dart'; export 'package:moxxmpp/src/xeps/xep_0414.dart'; +export 'package:moxxmpp/src/xeps/xep_0424.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0447.dart'; export 'package:moxxmpp/src/xeps/xep_0448.dart'; diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 9fded44..1e49c55 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -8,6 +8,7 @@ import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0085.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_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; @@ -71,6 +72,7 @@ class MessageEvent extends XmppEvent { this.fun, this.funReplacement, this.funCancellation, + this.messageRetraction, }); final String body; final JID fromJid; @@ -90,6 +92,7 @@ class MessageEvent extends XmppEvent { final String? funReplacement; final String? funCancellation; final bool encrypted; + final MessageRetractionData? messageRetraction; final Map other; } diff --git a/packages/moxxmpp/lib/src/managers/data.dart b/packages/moxxmpp/lib/src/managers/data.dart index fca20f9..ed7eb73 100644 --- a/packages/moxxmpp/lib/src/managers/data.dart +++ b/packages/moxxmpp/lib/src/managers/data.dart @@ -6,6 +6,7 @@ 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_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; @@ -55,6 +56,9 @@ class StanzaHandlerData with _$StanzaHandlerData { // 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, } ) = _StanzaHandlerData; } diff --git a/packages/moxxmpp/lib/src/managers/data.freezed.dart b/packages/moxxmpp/lib/src/managers/data.freezed.dart index 966113a..fcf3da6 100644 --- a/packages/moxxmpp/lib/src/managers/data.freezed.dart +++ b/packages/moxxmpp/lib/src/managers/data.freezed.dart @@ -54,7 +54,11 @@ mixin _$StanzaHandlerData { 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; + 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; @JsonKey(ignore: true) $StanzaHandlerDataCopyWith get copyWith => @@ -87,7 +91,8 @@ abstract class $StanzaHandlerDataCopyWith<$Res> { bool encrypted, ExplicitEncryptionType? encryptionType, DelayedDelivery? delayedDelivery, - Map other}); + Map other, + MessageRetractionData? messageRetraction}); } /// @nodoc @@ -122,6 +127,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> Object? encryptionType = freezed, Object? delayedDelivery = freezed, Object? other = freezed, + Object? messageRetraction = freezed, }) { return _then(_value.copyWith( done: done == freezed @@ -208,6 +214,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> ? _value.other : other // ignore: cast_nullable_to_non_nullable as Map, + messageRetraction: messageRetraction == freezed + ? _value.messageRetraction + : messageRetraction // ignore: cast_nullable_to_non_nullable + as MessageRetractionData?, )); } } @@ -240,7 +250,8 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res> bool encrypted, ExplicitEncryptionType? encryptionType, DelayedDelivery? delayedDelivery, - Map other}); + Map other, + MessageRetractionData? messageRetraction}); } /// @nodoc @@ -277,6 +288,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> Object? encryptionType = freezed, Object? delayedDelivery = freezed, Object? other = freezed, + Object? messageRetraction = freezed, }) { return _then(_$_StanzaHandlerData( done == freezed @@ -363,6 +375,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> ? _value._other : other // ignore: cast_nullable_to_non_nullable as Map, + messageRetraction: messageRetraction == freezed + ? _value.messageRetraction + : messageRetraction // ignore: cast_nullable_to_non_nullable + as MessageRetractionData?, )); } } @@ -387,7 +403,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { this.encrypted = false, this.encryptionType, this.delayedDelivery, - final Map other = const {}}) + final Map other = const {}, + this.messageRetraction}) : _other = other; // Indicates to the runner that processing is now done. This means that all @@ -463,9 +480,14 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { return EqualUnmodifiableMapView(_other); } +// If non-null, then it indicates the origin Id of the message that should be +// retracted + @override + final MessageRetractionData? messageRetraction; + @override String toString() { - return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other)'; + return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction)'; } @override @@ -501,7 +523,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { .equals(other.encryptionType, encryptionType) && const DeepCollectionEquality() .equals(other.delayedDelivery, delayedDelivery) && - const DeepCollectionEquality().equals(other._other, this._other)); + const DeepCollectionEquality().equals(other._other, this._other) && + const DeepCollectionEquality() + .equals(other.messageRetraction, messageRetraction)); } @override @@ -527,7 +551,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { const DeepCollectionEquality().hash(encrypted), const DeepCollectionEquality().hash(encryptionType), const DeepCollectionEquality().hash(delayedDelivery), - const DeepCollectionEquality().hash(_other) + const DeepCollectionEquality().hash(_other), + const DeepCollectionEquality().hash(messageRetraction) ]); @JsonKey(ignore: true) @@ -556,7 +581,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { final bool encrypted, final ExplicitEncryptionType? encryptionType, final DelayedDelivery? delayedDelivery, - final Map other}) = _$_StanzaHandlerData; + final Map other, + final MessageRetractionData? messageRetraction}) = _$_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. @@ -606,6 +632,9 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { @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 @JsonKey(ignore: true) _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index afb4b33..51a018e 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -24,3 +24,4 @@ const omemoManager = 'org.moxxmpp.omemomanager'; const emeManager = 'org.moxxmpp.ememanager'; const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager'; const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager'; +const messageRetractionManager = 'org.moxxmpp.messageretractionmanager'; diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index 0ae1270..b029a11 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -13,12 +13,12 @@ import 'package:moxxmpp/src/xeps/xep_0085.dart'; import 'package:moxxmpp/src/xeps/xep_0184.dart'; import 'package:moxxmpp/src/xeps/xep_0333.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart'; +import 'package:moxxmpp/src/xeps/xep_0424.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'; class MessageDetails { - const MessageDetails({ required this.to, this.body, @@ -35,6 +35,7 @@ class MessageDetails { this.funReplacement, this.funCancellation, this.shouldEncrypt = false, + this.messageRetraction, }); final String to; final String? body; @@ -51,6 +52,7 @@ class MessageDetails { final String? funReplacement; final String? funCancellation; final bool shouldEncrypt; + final MessageRetractionData? messageRetraction; } class MessageManager extends XmppManagerBase { @@ -95,6 +97,7 @@ class MessageManager extends XmppManagerBase { funReplacement: state.funReplacement, funCancellation: state.funCancellation, encrypted: state.encrypted, + messageRetraction: state.messageRetraction, other: state.other, ),); @@ -159,6 +162,8 @@ class MessageManager extends XmppManagerBase { } else if (firstSource is StatelessFileSharingEncryptedSource) { body = firstSource.source.url; } + } else if (details.messageRetraction?.fallback != null) { + body = details.messageRetraction!.fallback; } stanza.addChild( @@ -216,6 +221,33 @@ class MessageManager extends XmppManagerBase { ), ); } + + 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, + ), + ); + } + } getAttributes().sendStanza(stanza, awaitable: false); } diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index e1e155f..fb92e16 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -114,6 +114,15 @@ const simsXmlns = 'urn:xmpp:sims:1'; // XEP-0420 const sceXmlns = 'urn:xmpp:sce:1'; +// XEP-0422 +const fasteningXmlns = 'urn:xmpp:fasten:0'; + +// XEP-0424 +const messageRetractionXmlns = 'urn:xmpp:message-retract:0'; + +// XEp-0428 +const fallbackIndicationXmlns = 'urn:xmpp:fallback:0'; + // XEP-0446 const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0424.dart b/packages/moxxmpp/lib/src/xeps/xep_0424.dart new file mode 100644 index 0000000..73dfe99 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0424.dart @@ -0,0 +1,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/namespaces.dart'; +import 'package:moxxmpp/src/stanza.dart'; + +class MessageRetractionData { + MessageRetractionData(this.id, this.fallback); + final String? fallback; + final String id; +} + +class MessageRetractionManager extends XmppManagerBase { + @override + String getName() => 'MessageRetractionManager'; + + @override + String getId() => messageRetractionManager; + + @override + List getDiscoFeatures() => [ messageRetractionXmlns ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'message', + callback: _onMessage, + // Before the MessageManager + priority: -99, + ) + ]; + + @override + Future isSupported() async => true; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final applyTo = message.firstTag('apply-to', xmlns: fasteningXmlns); + if (applyTo == null) { + return state; + } + + final retract = applyTo.firstTag('retract', xmlns: messageRetractionXmlns); + if (retract == null) { + return state; + } + + final isFallbackBody = message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; + + return state.copyWith( + messageRetraction: MessageRetractionData( + applyTo.attributes['id']! as String, + isFallbackBody ? + message.firstTag('body')?.innerText() : + null, + ), + ); + } +}