feat(xep): Implement the message sending callbacks

This commit is contained in:
PapaTutuWawa 2023-06-06 14:12:49 +02:00
parent 79d7e3ba64
commit 6f5de9c4dc
16 changed files with 772 additions and 89 deletions

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
@ -8,6 +9,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/staging/file_upload_notification.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_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.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_0449.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
class MessageBodyData {
const MessageBodyData(this.body);
/// The content of the <body /> element.
final String? body;
XMLNode toXML() {
return XMLNode(
tag: 'body',
text: body,
);
}
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
if (extensions.get<ReplyData>() != null) {
return [];
}
final data = extensions.get<MessageBodyData>();
return data != null ? [data.toXML()] : [];
}
}
class MessageIdData {
const MessageIdData(this.id);
/// The id attribute of the stanza.
final String id;
}
typedef MessageSendingCallback = List<XMLNode> Function(TypedMap);
/// Data used to build a message stanza. /// Data used to build a message stanza.
/// ///
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be /// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be
@ -79,7 +113,11 @@ class MessageDetails {
} }
class MessageManager extends XmppManagerBase { 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<MessageSendingCallback> messageSendingCallbacks;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@ -145,6 +183,23 @@ class MessageManager extends XmppManagerBase {
return state..done = true; return state..done = true;
} }
Future<void> sendMessage2(JID to, TypedMap extensions) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.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 /// 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. /// 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. /// 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) { // if (details.requestDeliveryReceipt) {
stanza.addChild(makeMessageDeliveryRequest()); // stanza.addChild(makeMessageDeliveryRequest());
} // }
if (details.requestChatMarkers) { if (details.requestChatMarkers) {
stanza.addChild(makeChatMarkerMarkable()); stanza.addChild(makeChatMarkerMarkable());
} }
@ -304,7 +359,7 @@ class MessageManager extends XmppManagerBase {
} }
if (details.messageReactions != null) { if (details.messageReactions != null) {
stanza.addChild(details.messageReactions!.toXml()); stanza.addChild(details.messageReactions!.toXML());
} }
if (details.messageProcessingHints != null) { if (details.messageProcessingHints != null) {

View File

@ -5,6 +5,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatState { enum ChatState {
active, active,
@ -52,6 +53,11 @@ enum ChatState {
xmlns: chatStateXmlns, xmlns: chatStateXmlns,
); );
} }
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<ChatState>();
return data != null ? [data.toXML()] : [];
}
} }
class ChatStateManager extends XmppManagerBase { class ChatStateManager extends XmppManagerBase {

View File

@ -7,28 +7,53 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageDeliveryReceiptData { class MessageDeliveryReceiptData {
const MessageDeliveryReceiptData(this.receiptRequested); const MessageDeliveryReceiptData(this.receiptRequested);
/// Indicates whether a delivery receipt is requested or not. /// Indicates whether a delivery receipt is requested or not.
final bool receiptRequested; final bool receiptRequested;
XMLNode toXML() {
assert(
receiptRequested,
'This method makes little sense with receiptRequested == false',
);
return XMLNode.xmlns(
tag: 'request',
xmlns: deliveryXmlns,
);
}
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<MessageDeliveryReceiptData>();
return data != null
? [
data.toXML(),
]
: [];
}
} }
// TODO: Merge those two functions into [MessageDeliveryReceiptData] class MessageDeliveryReceivedData {
XMLNode makeMessageDeliveryRequest() { const MessageDeliveryReceivedData(this.id);
return XMLNode.xmlns(
tag: 'request',
xmlns: deliveryXmlns,
);
}
XMLNode makeMessageDeliveryResponse(String id) { /// The stanza id of the message we received.
return XMLNode.xmlns( final String id;
tag: 'received',
xmlns: deliveryXmlns, XMLNode toXML() {
attributes: {'id': id}, return XMLNode.xmlns(
); tag: 'received',
xmlns: deliveryXmlns,
attributes: {'id': id},
);
}
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<MessageDeliveryReceivedData>();
return data != null ? [data.toXML()] : [];
}
} }
class MessageDeliveryReceiptManager extends XmppManagerBase { class MessageDeliveryReceiptManager extends XmppManagerBase {

View File

@ -5,6 +5,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class LastMessageCorrectionData { class LastMessageCorrectionData {
const LastMessageCorrectionData(this.id); const LastMessageCorrectionData(this.id);
@ -21,6 +22,15 @@ class LastMessageCorrectionData {
}, },
); );
} }
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<LastMessageCorrectionData>();
return data != null
? [
data.toXML(),
]
: [];
}
} }
class LastMessageCorrectionManager extends XmppManagerBase { class LastMessageCorrectionManager extends XmppManagerBase {

View File

@ -1,5 +1,6 @@
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum MessageProcessingHint { enum MessageProcessingHint {
noPermanentStore, noPermanentStore,
@ -45,4 +46,13 @@ enum MessageProcessingHint {
xmlns: messageProcessingHintsXmlns, xmlns: messageProcessingHintsXmlns,
); );
} }
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<MessageProcessingHint>();
return data != null
? [
data.toXML(),
]
: [];
}
} }

View File

@ -6,6 +6,32 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Representation of a <stanza-id /> 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 { class StableIdData {
const StableIdData(this.originId, this.stanzaIds); const StableIdData(this.originId, this.stanzaIds);
@ -27,30 +53,22 @@ class StableIdData {
attributes: {'id': originId!}, attributes: {'id': originId!},
); );
} }
}
/// Representation of a <stanza-id /> element. List<XMLNode> toXML() {
class StanzaId { return [
const StanzaId( if (originId != null)
this.id, XMLNode.xmlns(
this.by, tag: 'origin-id',
); xmlns: stableIdXmlns,
attributes: {'id': originId!},
),
if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()),
];
}
/// The unique stanza id. static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final String id; final data = extensions.get<StableIdData>();
return data != null ? data.toXML() : [];
/// 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(),
},
);
} }
} }

View File

@ -4,11 +4,48 @@ import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.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 {
MessageRetractionData(this.id, this.fallback); MessageRetractionData(this.id, this.fallback);
/// A potential fallback message to set the body to when retracting.
final String? fallback; final String? fallback;
/// The id of the message that is retracted.
final String id; final String id;
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<MessageRetractionData>();
return data != null
? [
XMLNode.xmlns(
tag: 'apply-to',
xmlns: fasteningXmlns,
attributes: <String, String>{
'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 { class MessageRetractionManager extends XmppManagerBase {

View File

@ -5,13 +5,14 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageReactions { class MessageReactions {
const MessageReactions(this.messageId, this.emojis); const MessageReactions(this.messageId, this.emojis);
final String messageId; final String messageId;
final List<String> emojis; final List<String> emojis;
XMLNode toXml() { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'reactions', tag: 'reactions',
xmlns: messageReactionsXmlns, xmlns: messageReactionsXmlns,
@ -26,6 +27,15 @@ class MessageReactions {
}).toList(), }).toList(),
); );
} }
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<MessageReactions>();
return data != null
? [
data.toXML(),
]
: [];
}
} }
class MessageReactionsManager extends XmppManagerBase { class MessageReactionsManager extends XmppManagerBase {

View File

@ -6,6 +6,8 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart';
@ -71,7 +73,11 @@ List<StatelessFileSharingSource> processStatelessFileSharingSources(
} }
class StatelessFileSharingData { class StatelessFileSharingData {
const StatelessFileSharingData(this.metadata, this.sources); const StatelessFileSharingData(
this.metadata,
this.sources, {
this.includeOOBFallback = false,
});
/// Parse [node] as a StatelessFileSharingData element. /// Parse [node] as a StatelessFileSharingData element.
factory StatelessFileSharingData.fromXML(XMLNode node) { factory StatelessFileSharingData.fromXML(XMLNode node) {
@ -88,6 +94,10 @@ class StatelessFileSharingData {
final FileMetadataData metadata; final FileMetadataData metadata;
final List<StatelessFileSharingSource> sources; final List<StatelessFileSharingSource> 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() { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'file-sharing', tag: 'file-sharing',
@ -109,6 +119,26 @@ class StatelessFileSharingData {
source is StatelessFileSharingUrlSource, source is StatelessFileSharingUrlSource,
) as StatelessFileSharingUrlSource?; ) as StatelessFileSharingUrlSource?;
} }
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<StatelessFileSharingData>();
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 { class SFSManager extends XmppManagerBase {
@ -122,7 +152,7 @@ class SFSManager extends XmppManagerBase {
tagXmlns: sfsXmlns, tagXmlns: sfsXmlns,
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -98,
) )
]; ];

View File

@ -9,6 +9,7 @@ import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.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_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart'; import 'package:moxxmpp/src/xeps/xep_0300.dart';
@ -228,10 +229,29 @@ class StickerPack {
} }
class StickersData { class StickersData {
const StickersData(this.stickerPackId); const StickersData(this.stickerPackId, this.sticker);
/// The id of the sticker pack the referenced sticker is from. /// The id of the sticker pack the referenced sticker is from.
final String stickerPackId; final String stickerPackId;
/// The metadata of the sticker.
final StatelessFileSharingData sticker;
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<StickersData>();
return data != null
? [
XMLNode.xmlns(
tag: 'sticker',
xmlns: stickersXmlns,
attributes: {
'pack': data.stickerPackId,
},
),
data.sticker.toXML(),
]
: [];
}
} }
class StickersManager extends XmppManagerBase { class StickersManager extends XmppManagerBase {
@ -258,7 +278,10 @@ class StickersManager extends XmppManagerBase {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state return state
..extensions.set( ..extensions.set(
StickersData(sticker.attributes['pack']! as String), StickersData(
sticker.attributes['pack']! as String,
state.extensions.get<StatelessFileSharingData>()!,
),
); );
} }

View File

@ -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/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Data summarizing the XEP-0461 data. /// A reply to a message.
class ReplyData { class ReplyData {
const ReplyData({ const ReplyData(
required this.id, this.id, {
this.to, this.body,
this.jid,
this.start, this.start,
this.end, this.end,
}); });
/// The bare JID to whom the reply applies to ReplyData.fromQuoteData(
final String? to; 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; final String id;
/// The start of the fallback body (inclusive) /// The start of the fallback body (inclusive)
@ -27,18 +39,59 @@ class ReplyData {
/// The end of the fallback body (exclusive) /// The end of the fallback body (exclusive)
final int? end; 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. /// 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 /// If either [ReplyData.start] or [ReplyData.end] are null, then body is returned as
/// is. /// is.
String removeFallback(String body) { String? get withoutFallback {
if (body == null) return null;
if (start == null || end == null) return body; if (start == null || end == null) return body;
return body.replaceRange(start!, end, ''); return body!.replaceRange(start!, end, '');
}
static List<XMLNode> messageSendingCallback(TypedMap extensions) {
final data = extensions.get<ReplyData>();
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. /// Internal class describing how to build a message with a quote fallback body.
@visibleForTesting
class QuoteData { class QuoteData {
const QuoteData(this.body, this.fallbackLength); const QuoteData(this.body, this.fallbackLength);
@ -90,8 +143,8 @@ class MessageRepliesManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; final reply = stanza.firstTag('reply', xmlns: replyXmlns)!;
final id = reply.attributes['id']! as String;
final to = reply.attributes['to'] as String?; final to = reply.attributes['to'] as String?;
final jid = to != null ? JID.fromString(to) : null;
int? start; int? start;
int? end; int? end;
@ -106,10 +159,11 @@ class MessageRepliesManager extends XmppManagerBase {
return state return state
..extensions.set( ..extensions.set(
ReplyData( ReplyData(
id: id, reply.attributes['id']! as String,
to: to, jid: jid,
start: start, start: start,
end: end, end: end,
body: stanza.firstTag('body')?.innerText(),
), ),
); );
} }

View File

@ -1,13 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/connectivity.dart'; import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/handlers/client.dart'; import 'package:moxxmpp/src/handlers/client.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/reconnect.dart'; import 'package:moxxmpp/src/reconnect.dart';
import 'package:moxxmpp/src/settings.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 'package:moxxmpp/src/stringxml.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';
@ -15,15 +16,15 @@ import '../helpers/xmpp.dart';
/// This class allows registering managers for easier testing. /// This class allows registering managers for easier testing.
class TestingManagerHolder { class TestingManagerHolder {
TestingManagerHolder({ TestingManagerHolder({
BaseSocketWrapper? socket, StubTCPSocket? stubSocket,
}) : _socket = socket ?? StubTCPSocket([]); }) : socket = stubSocket ?? StubTCPSocket([]);
final BaseSocketWrapper _socket; final StubTCPSocket socket;
final Map<String, XmppManagerBase> _managers = {}; final Map<String, XmppManagerBase> _managers = {};
// The amount of stanzas sent /// A list of events that were triggered.
int sentStanzas = 0; final List<XmppEvent> sentEvents = List.empty(growable: true);
static final JID jid = JID.fromString('testuser@example.org/abc123'); static final JID jid = JID.fromString('testuser@example.org/abc123');
static final ConnectionSettings settings = ConnectionSettings( static final ConnectionSettings settings = ConnectionSettings(
@ -31,15 +32,9 @@ class TestingManagerHolder {
password: 'abc123', password: 'abc123',
); );
Future<XMLNode> _sendStanza( Future<XMLNode?> _sendStanza(StanzaDetails details) async {
stanza, { socket.write(details.stanza.toXml());
bool addId = true, return null;
bool awaitable = true,
bool encrypted = false,
bool forceEncryption = false,
}) async {
sentStanzas++;
return XMLNode.fromString('<iq />');
} }
T? _getManagerById<T extends XmppManagerBase>(String id) { T? _getManagerById<T extends XmppManagerBase>(String id) {
@ -54,12 +49,12 @@ class TestingManagerHolder {
TestingReconnectionPolicy(), TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(), AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(), ClientToServerNegotiator(),
_socket, socket,
), ),
getConnectionSettings: () => settings, getConnectionSettings: () => settings,
sendNonza: (_) {}, sendNonza: (_) {},
sendEvent: (_) {}, sendEvent: sentEvents.add,
getSocket: () => _socket, getSocket: () => socket,
getNegotiatorById: getNegotiatorNullStub, getNegotiatorById: getNegotiatorNullStub,
getFullJID: () => jid, getFullJID: () => jid,
getManagerById: _getManagerById, getManagerById: _getManagerById,

View File

@ -140,7 +140,7 @@ void main() {
// Query Alice's device // Query Alice's device
final result = await dm.discoInfoQuery(aliceJid); final result = await dm.discoInfoQuery(aliceJid);
expect(result.isType<DiscoError>(), false); expect(result.isType<DiscoError>(), false);
expect(tm.sentStanzas, 0); expect(tm.socket.getState(), 0);
}); });
}); });
} }

View File

@ -168,7 +168,6 @@ void main() {
PubSubManager(), PubSubManager(),
DiscoManager([]), DiscoManager([]),
PresenceManager(), PresenceManager(),
MessageManager(),
RosterManager(TestingRosterStateManager(null, [])), RosterManager(TestingRosterStateManager(null, [])),
]); ]);
await connection.registerFeatureNegotiators([ await connection.registerFeatureNegotiators([

View File

@ -1,7 +1,14 @@
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart';
void main() { void main() {
initLogger();
test('Test parsing a large sticker pack', () { test('Test parsing a large sticker pack', () {
// Example sticker pack based on the "miho" sticker pack by Movim // Example sticker pack based on the "miho" sticker pack by Movim
final rawPack = XMLNode.fromString(''' final rawPack = XMLNode.fromString('''
@ -225,4 +232,183 @@ void main() {
expect(pack.stickers.length, 16); 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 <dimensions /> with <width /> and <height />
'''
<message to="user@example.org" type="chat">
<sticker xmlns='urn:xmpp:stickers:0' pack='EpRv28DHHzFrE4zd+xaNpVb4' />
<file-sharing xmlns='urn:xmpp:sfs:0'>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/png</media-type>
<desc>😘</desc>
<size>67016</size>
<width>512</width>
<height>512</height>
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=</hash>
</file>
<sources>
<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png' />
</sources>
</file-sharing>
</message>
''',
'',
),
]),
);
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<void>.delayed(const Duration(seconds: 1));
expect(holder.socket.getState(), 1);
});
test('Test receiving a sticker', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
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(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<sticker xmlns='urn:xmpp:stickers:0' pack='EpRv28DHHzFrE4zd+xaNpVb4' />
<file-sharing xmlns='urn:xmpp:sfs:0'>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/png</media-type>
<desc>😘</desc>
<size>67016</size>
<width>512</width>
<height>512</height>
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=</hash>
</file>
<sources>
<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png' />
</sources>
</file-sharing>
</message>
''',
);
await Future<void>.delayed(const Duration(seconds: 2));
expect(messageEvent?.stickerPackId, 'EpRv28DHHzFrE4zd+xaNpVb4');
expect(messageEvent?.sfs!.metadata.desc, '😘');
expect(
messageEvent?.sfs!.sources.first is StatelessFileSharingUrlSource,
true,
);
});
} }

View File

@ -1,6 +1,9 @@
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/xmpp.dart';
void main() { void main() {
test('Test building a singleline quote', () { test('Test building a singleline quote', () {
final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!'); final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!');
@ -20,28 +23,250 @@ void main() {
}); });
test('Applying a singleline quote', () { test('Applying a singleline quote', () {
const body = '> Hallo Welt\nHello right back!';
const reply = ReplyData( const reply = ReplyData(
to: '', '',
id: '',
start: 0, start: 0,
end: 13, end: 13,
body: '> Hallo Welt\nHello right back!',
); );
final bodyWithoutFallback = reply.removeFallback(body); expect(reply.withoutFallback, 'Hello right back!');
expect(bodyWithoutFallback, 'Hello right back!');
}); });
test('Applying a multiline quote', () { test('Applying a multiline quote', () {
const body = "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!";
const reply = ReplyData( const reply = ReplyData(
to: '', '',
id: '',
start: 0, start: 0,
end: 28, end: 28,
body: "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!",
); );
final bodyWithoutFallback = reply.removeFallback(body); expect(reply.withoutFallback, "I'm fine.\nThank you!");
expect(bodyWithoutFallback, "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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
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(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<body>Great idea!</body>
<reply to='anna@example.com/tablet' id='message-id1' xmlns='urn:xmpp:reply:0' />
</message>
''',
);
await Future<void>.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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
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(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<body>> Anna wrote:\n> We should bake a cake\nGreat idea!</body>
<reply to='anna@example.com/laptop' id='message-id1' xmlns='urn:xmpp:reply:0' />
<fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="38" />
</fallback>
</message>
''',
);
await Future<void>.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);
}); });
} }