From 6f5de9c4dca3b4a48cf06d77557f0bd2f280720f Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Tue, 6 Jun 2023 14:12:49 +0200 Subject: [PATCH] feat(xep): Implement the message sending callbacks --- packages/moxxmpp/lib/src/message.dart | 65 ++++- packages/moxxmpp/lib/src/xeps/xep_0085.dart | 6 + packages/moxxmpp/lib/src/xeps/xep_0184.dart | 51 +++- packages/moxxmpp/lib/src/xeps/xep_0308.dart | 10 + packages/moxxmpp/lib/src/xeps/xep_0334.dart | 10 + packages/moxxmpp/lib/src/xeps/xep_0359.dart | 62 +++-- packages/moxxmpp/lib/src/xeps/xep_0424.dart | 37 +++ packages/moxxmpp/lib/src/xeps/xep_0444.dart | 12 +- packages/moxxmpp/lib/src/xeps/xep_0447.dart | 34 ++- packages/moxxmpp/lib/src/xeps/xep_0449.dart | 27 +- packages/moxxmpp/lib/src/xeps/xep_0461.dart | 82 +++++- packages/moxxmpp/test/helpers/manager.dart | 31 +-- packages/moxxmpp/test/xeps/xep_0030_test.dart | 2 +- packages/moxxmpp/test/xeps/xep_0060_test.dart | 1 - packages/moxxmpp/test/xeps/xep_0449_test.dart | 186 +++++++++++++ packages/moxxmpp/test/xeps/xep_0461_test.dart | 245 +++++++++++++++++- 16 files changed, 772 insertions(+), 89 deletions(-) diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index 92d0eed..1995cb5 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/jid.dart'; @@ -8,6 +9,7 @@ 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/util/typed_map.dart'; import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0085.dart'; @@ -26,6 +28,38 @@ import 'package:moxxmpp/src/xeps/xep_0448.dart'; import 'package:moxxmpp/src/xeps/xep_0449.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; +class MessageBodyData { + const MessageBodyData(this.body); + + /// The content of the element. + final String? body; + + XMLNode toXML() { + return XMLNode( + tag: 'body', + text: body, + ); + } + + static List messageSendingCallback(TypedMap extensions) { + if (extensions.get() != null) { + return []; + } + + final data = extensions.get(); + return data != null ? [data.toXML()] : []; + } +} + +class MessageIdData { + const MessageIdData(this.id); + + /// The id attribute of the stanza. + final String id; +} + +typedef MessageSendingCallback = List Function(TypedMap); + /// Data used to build a message stanza. /// /// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be @@ -79,7 +113,11 @@ class MessageDetails { } class MessageManager extends XmppManagerBase { - MessageManager() : super(messageManager); + MessageManager(this.messageSendingCallbacks) : super(messageManager); + + /// A list of callbacks that are called when a message is sent in order to add + /// appropriate child elements. + final List messageSendingCallbacks; @override List getIncomingStanzaHandlers() => [ @@ -145,6 +183,23 @@ class MessageManager extends XmppManagerBase { return state..done = true; } + Future sendMessage2(JID to, TypedMap extensions) async { + await getAttributes().sendStanza( + StanzaDetails( + Stanza.message( + to: to.toString(), + id: extensions.get()?.id, + type: 'chat', + children: messageSendingCallbacks + .map((c) => c(extensions)) + .flattened + .toList(), + ), + awaitable: false, + ), + ); + } + /// 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. @@ -217,9 +272,9 @@ class MessageManager extends XmppManagerBase { } } - if (details.requestDeliveryReceipt) { - stanza.addChild(makeMessageDeliveryRequest()); - } + // if (details.requestDeliveryReceipt) { + // stanza.addChild(makeMessageDeliveryRequest()); + // } if (details.requestChatMarkers) { stanza.addChild(makeChatMarkerMarkable()); } @@ -304,7 +359,7 @@ class MessageManager extends XmppManagerBase { } if (details.messageReactions != null) { - stanza.addChild(details.messageReactions!.toXml()); + stanza.addChild(details.messageReactions!.toXML()); } if (details.messageProcessingHints != null) { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0085.dart b/packages/moxxmpp/lib/src/xeps/xep_0085.dart index f0d25f7..62c788e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0085.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0085.dart @@ -5,6 +5,7 @@ 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/util/typed_map.dart'; enum ChatState { active, @@ -52,6 +53,11 @@ enum ChatState { xmlns: chatStateXmlns, ); } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null ? [data.toXML()] : []; + } } class ChatStateManager extends XmppManagerBase { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0184.dart b/packages/moxxmpp/lib/src/xeps/xep_0184.dart index c0d5e46..aa8e5a5 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0184.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0184.dart @@ -7,28 +7,53 @@ 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/util/typed_map.dart'; class MessageDeliveryReceiptData { 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, + ); + } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } } -// TODO: Merge those two functions into [MessageDeliveryReceiptData] -XMLNode makeMessageDeliveryRequest() { - return XMLNode.xmlns( - tag: 'request', - xmlns: deliveryXmlns, - ); -} +class MessageDeliveryReceivedData { + const MessageDeliveryReceivedData(this.id); -XMLNode makeMessageDeliveryResponse(String id) { - return XMLNode.xmlns( - tag: 'received', - xmlns: deliveryXmlns, - attributes: {'id': id}, - ); + /// The stanza id of the message we received. + final String id; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'received', + xmlns: deliveryXmlns, + attributes: {'id': id}, + ); + } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null ? [data.toXML()] : []; + } } class MessageDeliveryReceiptManager extends XmppManagerBase { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0308.dart b/packages/moxxmpp/lib/src/xeps/xep_0308.dart index 1cc57b8..7621841 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0308.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0308.dart @@ -5,6 +5,7 @@ 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/util/typed_map.dart'; class LastMessageCorrectionData { const LastMessageCorrectionData(this.id); @@ -21,6 +22,15 @@ class LastMessageCorrectionData { }, ); } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } } class LastMessageCorrectionManager extends XmppManagerBase { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0334.dart b/packages/moxxmpp/lib/src/xeps/xep_0334.dart index e4bf1b0..5b372a6 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0334.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0334.dart @@ -1,5 +1,6 @@ import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/util/typed_map.dart'; enum MessageProcessingHint { noPermanentStore, @@ -45,4 +46,13 @@ enum MessageProcessingHint { xmlns: messageProcessingHintsXmlns, ); } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0359.dart b/packages/moxxmpp/lib/src/xeps/xep_0359.dart index a1cdfb9..4576efd 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0359.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0359.dart @@ -6,6 +6,32 @@ 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/util/typed_map.dart'; + +/// Representation of a element. +class StanzaId { + const StanzaId( + this.id, + this.by, + ); + + /// The unique stanza id. + final String id; + + /// The JID the id was generated by. + final JID by; + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'stanza-id', + xmlns: stableIdXmlns, + attributes: { + 'id': id, + 'by': by.toString(), + }, + ); + } +} class StableIdData { const StableIdData(this.originId, this.stanzaIds); @@ -27,30 +53,22 @@ class StableIdData { attributes: {'id': originId!}, ); } -} -/// Representation of a element. -class StanzaId { - const StanzaId( - this.id, - this.by, - ); + List toXML() { + return [ + if (originId != null) + XMLNode.xmlns( + tag: 'origin-id', + xmlns: stableIdXmlns, + attributes: {'id': originId!}, + ), + if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()), + ]; + } - /// The unique stanza id. - final String id; - - /// The JID the id was generated by. - final JID by; - - XMLNode toXml() { - return XMLNode.xmlns( - tag: 'stanza-id', - xmlns: stableIdXmlns, - attributes: { - 'id': id, - 'by': by.toString(), - }, - ); + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null ? data.toXML() : []; } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0424.dart b/packages/moxxmpp/lib/src/xeps/xep_0424.dart index ca5bc62..08e8bc5 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0424.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0424.dart @@ -4,11 +4,48 @@ 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/util/typed_map.dart'; class MessageRetractionData { 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; + + static 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, + ), + ] + : []; + } } class MessageRetractionManager extends XmppManagerBase { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0444.dart b/packages/moxxmpp/lib/src/xeps/xep_0444.dart index 0e8f9ac..282ec74 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0444.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0444.dart @@ -5,13 +5,14 @@ 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/util/typed_map.dart'; class MessageReactions { const MessageReactions(this.messageId, this.emojis); final String messageId; final List emojis; - XMLNode toXml() { + XMLNode toXML() { return XMLNode.xmlns( tag: 'reactions', xmlns: messageReactionsXmlns, @@ -26,6 +27,15 @@ class MessageReactions { }).toList(), ); } + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null + ? [ + data.toXML(), + ] + : []; + } } class MessageReactionsManager extends XmppManagerBase { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0447.dart b/packages/moxxmpp/lib/src/xeps/xep_0447.dart index f9c8cff..be9b474 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0447.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0447.dart @@ -6,6 +6,8 @@ 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/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'; @@ -71,7 +73,11 @@ List processStatelessFileSharingSources( } class StatelessFileSharingData { - const StatelessFileSharingData(this.metadata, this.sources); + const StatelessFileSharingData( + this.metadata, + this.sources, { + this.includeOOBFallback = false, + }); /// Parse [node] as a StatelessFileSharingData element. factory StatelessFileSharingData.fromXML(XMLNode node) { @@ -88,6 +94,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', @@ -109,6 +119,26 @@ class StatelessFileSharingData { source is StatelessFileSharingUrlSource, ) as StatelessFileSharingUrlSource?; } + + static 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(), + ]; + } } class SFSManager extends XmppManagerBase { @@ -122,7 +152,7 @@ class SFSManager extends XmppManagerBase { tagXmlns: sfsXmlns, callback: _onMessage, // Before the message handler - priority: -99, + priority: -98, ) ]; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0449.dart b/packages/moxxmpp/lib/src/xeps/xep_0449.dart index 1f95556..efda5a5 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0449.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0449.dart @@ -9,6 +9,7 @@ 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'; @@ -228,10 +229,29 @@ class StickerPack { } class StickersData { - const StickersData(this.stickerPackId); + const StickersData(this.stickerPackId, this.sticker); /// The id of the sticker pack the referenced sticker is from. final String stickerPackId; + + /// The metadata of the sticker. + final StatelessFileSharingData sticker; + + static List messageSendingCallback(TypedMap extensions) { + final data = extensions.get(); + return data != null + ? [ + XMLNode.xmlns( + tag: 'sticker', + xmlns: stickersXmlns, + attributes: { + 'pack': data.stickerPackId, + }, + ), + data.sticker.toXML(), + ] + : []; + } } class StickersManager extends XmppManagerBase { @@ -258,7 +278,10 @@ class StickersManager extends XmppManagerBase { final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; return state ..extensions.set( - StickersData(sticker.attributes['pack']! as String), + StickersData( + sticker.attributes['pack']! as String, + state.extensions.get()!, + ), ); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0461.dart b/packages/moxxmpp/lib/src/xeps/xep_0461.dart index 0415d6f..95af407 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0461.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0461.dart @@ -1,24 +1,36 @@ -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'; 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/util/typed_map.dart'; -/// Data summarizing the XEP-0461 data. +/// A reply to a message. class ReplyData { - const ReplyData({ - required this.id, - this.to, + 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 +39,59 @@ 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, ''); + } + + static 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(), + }, + ), + ], + ), + ] + : []; } } /// Internal class describing how to build a message with a quote fallback body. -@visibleForTesting class QuoteData { const QuoteData(this.body, this.fallbackLength); @@ -90,8 +143,8 @@ class MessageRepliesManager extends XmppManagerBase { 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; @@ -106,10 +159,11 @@ class MessageRepliesManager extends XmppManagerBase { return state ..extensions.set( ReplyData( - id: id, - to: to, + reply.attributes['id']! as String, + jid: jid, start: start, end: end, + body: stanza.firstTag('body')?.innerText(), ), ); } diff --git a/packages/moxxmpp/test/helpers/manager.dart b/packages/moxxmpp/test/helpers/manager.dart index af64e56..a3c02b7 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,15 +32,9 @@ 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) { @@ -54,12 +49,12 @@ class TestingManagerHolder { TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), ClientToServerNegotiator(), - _socket, + socket, ), getConnectionSettings: () => settings, sendNonza: (_) {}, - sendEvent: (_) {}, - getSocket: () => _socket, + sendEvent: sentEvents.add, + getSocket: () => socket, getNegotiatorById: getNegotiatorNullStub, getFullJID: () => jid, getManagerById: _getManagerById, diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index 134d9e6..f7ec19f 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -140,7 +140,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..44d56a0 100644 --- a/packages/moxxmpp/test/xeps/xep_0060_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0060_test.dart @@ -168,7 +168,6 @@ void main() { PubSubManager(), DiscoManager([]), PresenceManager(), - MessageManager(), RosterManager(TestingRosterStateManager(null, [])), ]); await connection.registerFeatureNegotiators([ diff --git a/packages/moxxmpp/test/xeps/xep_0449_test.dart b/packages/moxxmpp/test/xeps/xep_0449_test.dart index 7e4165c..a322e7f 100644 --- a/packages/moxxmpp/test/xeps/xep_0449_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0449_test.dart @@ -1,7 +1,14 @@ import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp/src/util/typed_map.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 +232,183 @@ void main() { expect(pack.stickers.length, 16); }); + + test('Test sending a sticker', () async { + final manager = MessageManager([ + StatelessFileSharingData.messageSendingCallback, + StickersData.messageSendingCallback, + ]); + 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); + + await manager.sendMessage2( + 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)); + expect(messageEvent?.stickerPackId, 'EpRv28DHHzFrE4zd+xaNpVb4'); + expect(messageEvent?.sfs!.metadata.desc, '😘'); + expect( + messageEvent?.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..07df8b0 100644 --- a/packages/moxxmpp/test/xeps/xep_0461_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0461_test.dart @@ -1,6 +1,9 @@ import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp/src/util/typed_map.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 +23,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 = ReplyData.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!.reply!; + 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!.reply!; + 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); }); }