diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart
index f719c21..a1dc9e1 100644
--- a/packages/moxxmpp/lib/moxxmpp.dart
+++ b/packages/moxxmpp/lib/moxxmpp.dart
@@ -48,6 +48,8 @@ export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0030/types.dart';
export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
+export 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
+export 'package:moxxmpp/src/xeps/xep_0045/types.dart';
export 'package:moxxmpp/src/xeps/xep_0045/xep_0045.dart';
export 'package:moxxmpp/src/xeps/xep_0054.dart';
export 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart
index 7b78b28..9566828 100644
--- a/packages/moxxmpp/lib/src/message.dart
+++ b/packages/moxxmpp/lib/src/message.dart
@@ -1,89 +1,72 @@
-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_0045/xep_0045.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,237 +77,72 @@ 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,
- stanzaId: state.stableId ?? const StableStanzaId(),
- isCarbon: state.isCarbon,
- deliveryReceiptRequested: state.deliveryReceiptRequested,
- isMarkable: state.isMarkable,
- type: message.attributes['type'] as String?,
- oob: state.oob,
- sfs: state.sfs,
- sims: state.sims,
- reply: state.reply,
- chatState: state.chatState,
- fun: state.fun,
- funReplacement: state.funReplacement,
- funCancellation: state.funCancellation,
- encrypted: state.encrypted,
- messageRetraction: state.messageRetraction,
- messageCorrectionId: state.lastMessageCorrectionSid,
- messageReactions: state.messageReactions,
- messageProcessingHints: hints.isEmpty ? null : hints,
- stickerPackId: state.stickerPackId,
- other: state.other,
- error: StanzaError.fromStanza(message),
+ 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: extensions.get()?.conversationType ==
+ ConversationType.groupchat
+ ? 'groupchat'
+ : '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/xeps/xep_0045/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
index db5bcbf..801b116 100644
--- a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
+++ b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
@@ -2,4 +2,6 @@ abstract class MUCError {}
class InvalidStanzaFormat extends MUCError {}
+class InvalidDiscoInfoResponse extends MUCError {}
+
class NoNicknameSpecified extends MUCError {}
diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
index 675a835..0397823 100644
--- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
+++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
@@ -8,29 +8,15 @@ class RoomInformation {
required this.name,
});
- factory RoomInformation.fromStanza({
- required JID roomJID,
- required XMLNode stanza,
- }) {
- final featureNodes = stanza.children[0].findTags('feature');
- final identityNodes = stanza.children[0].findTags('identity');
-
- if (featureNodes.isNotEmpty && identityNodes.isNotEmpty) {
- final features = featureNodes
- .map((xmlNode) => xmlNode.attributes['var'].toString())
- .toList();
- final name = identityNodes[0].attributes['name'].toString();
-
- return RoomInformation(
- jid: roomJID,
- features: features,
- name: name,
+ factory RoomInformation.fromDiscoInfo({
+ required DiscoInfo discoInfo,
+ }) =>
+ RoomInformation(
+ jid: discoInfo.jid!,
+ features: discoInfo.features,
+ name: discoInfo.identities[0].name!,
);
- } else {
- // ignore: only_throw_errors
- throw InvalidStanzaFormat();
- }
- }
+
final JID jid;
final List features;
final String name;
diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
index 0b3f61e..6ccf1df 100644
--- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
+++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
@@ -2,42 +2,42 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
+enum ConversationType { chat, groupchat, groupchatprivate }
+
+class ConversationTypeData extends StanzaHandlerExtension {
+ ConversationTypeData(this.conversationType);
+ final ConversationType conversationType;
+}
+
class MUCManager extends XmppManagerBase {
MUCManager() : super(mucManager);
@override
Future isSupported() async => true;
- Future> queryRoomInformation({
- required JID roomJID,
- }) async {
- final attrs = getAttributes();
+ Future> queryRoomInformation(
+ JID roomJID,
+ ) async {
try {
- final result = await attrs.sendStanza(
- StanzaDetails(
- Stanza.iq(
- type: 'get',
- to: roomJID.toString(),
- children: [
- XMLNode.xmlns(
- tag: 'query',
- xmlns: discoInfoXmlns,
- )
- ],
- ),
- ),
+ final attrs = getAttributes();
+ final result = await attrs
+ .getManagerById(discoManager)
+ ?.discoInfoQuery(roomJID);
+ if (result!.isType()) {
+ return Result(InvalidStanzaFormat());
+ }
+ final roomInformation = RoomInformation.fromDiscoInfo(
+ discoInfo: result.get(),
);
- final roomInformation =
- RoomInformation.fromStanza(roomJID: roomJID, stanza: result!);
return Result(roomInformation);
} catch (e) {
- return Result(InvalidStanzaFormat());
+ return Result(InvalidDiscoInfoResponse);
}
}
- Future> joinRoom({
- required JID roomJIDWithNickname,
- }) async {
+ Future> joinRoom(
+ JID roomJIDWithNickname,
+ ) async {
if (roomJIDWithNickname.resource.isEmpty) {
return Result(NoNicknameSpecified());
}
@@ -62,9 +62,9 @@ class MUCManager extends XmppManagerBase {
}
}
- Future> leaveRoom({
- required JID roomJIDWithNickname,
- }) async {
+ Future> leaveRoom(
+ JID roomJIDWithNickname,
+ ) async {
if (roomJIDWithNickname.resource.isEmpty) {
return Result(NoNicknameSpecified());
}