From 05c41d31858771f674ddb139b57d2b929914daf0 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 14 Jun 2023 09:59:46 +0530 Subject: [PATCH] feat(xep): Refactor sendMessage to allow groupchat Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/moxxmpp.dart | 2 + packages/moxxmpp/lib/src/message.dart | 386 +++++------------- .../moxxmpp/lib/src/xeps/xep_0045/errors.dart | 2 + .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 30 +- .../lib/src/xeps/xep_0045/xep_0045.dart | 52 +-- 5 files changed, 140 insertions(+), 332 deletions(-) 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()); }