From 762cf1c77a3f74c8e1a4d0a212c7d4f48bbea259 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Mon, 29 May 2023 17:10:41 +0530 Subject: [PATCH 01/17] feat(xep): Set base for XEP 0045 implementation Signed-off-by: Ikjot Singh Dhody --- .../moxxmpp/lib/src/managers/namespaces.dart | 1 + .../moxxmpp/lib/src/xeps/xep_0045/errors.dart | 3 ++ .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 37 +++++++++++++++++++ .../lib/src/xeps/xep_0045/xep_0045.dart | 36 ++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0045/types.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index 03e40e0..0cb6b31 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -31,3 +31,4 @@ const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; const stickersManager = 'org.moxxmpp.stickersmanager'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; +const mucManager = 'org.moxxmpp.mucmanager'; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart new file mode 100644 index 0000000..4b07b68 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart @@ -0,0 +1,3 @@ +abstract class MUCError {} + +class InvalidStanzaFormat extends MUCError {} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart new file mode 100644 index 0000000..c0ef218 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -0,0 +1,37 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; + +class RoomInformation { + RoomInformation({ + required this.jid, + required this.features, + required this.name, + }); + + factory RoomInformation.fromStanza({ + required String 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, + ); + } else { + // ignore: only_throw_errors + throw InvalidStanzaFormat(); + } + } + final String 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 new file mode 100644 index 0000000..e04ab5e --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -0,0 +1,36 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; +import 'package:moxxmpp/src/xeps/xep_0045/types.dart'; + +class MUCManager extends XmppManagerBase { + MUCManager() : super(mucManager); + + @override + Future isSupported() async => true; + + Future> queryRoomInformation( + String roomJID) async { + final attrs = getAttributes(); + try { + final result = await attrs.sendStanza( + StanzaDetails( + Stanza.iq( + type: 'get', + to: roomJID, + children: [ + XMLNode.xmlns( + tag: 'query', + xmlns: discoInfoXmlns, + ) + ], + ), + ), + ); + final roomInformation = + RoomInformation.fromStanza(roomJID: roomJID, stanza: result!); + return Result(roomInformation); + } catch (e) { + return Result(InvalidStanzaFormat()); + } + } +} From 68809469f65771b69d2b2e72dd7ed4b805742845 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 31 May 2023 13:31:02 +0530 Subject: [PATCH 02/17] feat(xep): Add joinRoom, leaveRoom routines. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/namespaces.dart | 3 + .../moxxmpp/lib/src/xeps/xep_0045/errors.dart | 2 + .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 4 +- .../lib/src/xeps/xep_0045/xep_0045.dart | 56 ++++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index e44a60f..f719c21 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -48,6 +48,7 @@ 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/xep_0045.dart'; export 'package:moxxmpp/src/xeps/xep_0054.dart'; export 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index f0be542..c821412 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -20,6 +20,9 @@ const discoItemsXmlns = 'http://jabber.org/protocol/disco#items'; // XEP-0033 const extendedAddressingXmlns = 'http://jabber.org/protocol/address'; +// XEP-0045 +const mucXmlns = 'http://jabber.org/protocol/muc'; + // XEP-0054 const vCardTempXmlns = 'vcard-temp'; const vCardTempUpdate = 'vcard-temp:x:update'; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart index 4b07b68..db5bcbf 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart @@ -1,3 +1,5 @@ abstract class MUCError {} class InvalidStanzaFormat 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 c0ef218..675a835 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -9,7 +9,7 @@ class RoomInformation { }); factory RoomInformation.fromStanza({ - required String roomJID, + required JID roomJID, required XMLNode stanza, }) { final featureNodes = stanza.children[0].findTags('feature'); @@ -31,7 +31,7 @@ class RoomInformation { throw InvalidStanzaFormat(); } } - final String jid; + 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 e04ab5e..0b3f61e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -8,15 +8,16 @@ class MUCManager extends XmppManagerBase { @override Future isSupported() async => true; - Future> queryRoomInformation( - String roomJID) async { + Future> queryRoomInformation({ + required JID roomJID, + }) async { final attrs = getAttributes(); try { final result = await attrs.sendStanza( StanzaDetails( Stanza.iq( type: 'get', - to: roomJID, + to: roomJID.toString(), children: [ XMLNode.xmlns( tag: 'query', @@ -33,4 +34,53 @@ class MUCManager extends XmppManagerBase { return Result(InvalidStanzaFormat()); } } + + Future> joinRoom({ + required JID roomJIDWithNickname, + }) async { + if (roomJIDWithNickname.resource.isEmpty) { + return Result(NoNicknameSpecified()); + } + final attrs = getAttributes(); + try { + await attrs.sendStanza( + StanzaDetails( + Stanza.presence( + to: roomJIDWithNickname.toString(), + children: [ + XMLNode.xmlns( + tag: 'x', + xmlns: mucXmlns, + ) + ], + ), + ), + ); + return const Result(true); + } catch (e) { + return Result(InvalidStanzaFormat()); + } + } + + Future> leaveRoom({ + required JID roomJIDWithNickname, + }) async { + if (roomJIDWithNickname.resource.isEmpty) { + return Result(NoNicknameSpecified()); + } + final attrs = getAttributes(); + try { + await attrs.sendStanza( + StanzaDetails( + Stanza.presence( + to: roomJIDWithNickname.toString(), + type: 'unavailable', + ), + ), + ); + return const Result(true); + } catch (e) { + return Result(InvalidStanzaFormat()); + } + } } From 64a8de6caabaa5d73f3ced1bcab2ccb6c7adc205 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Mon, 29 May 2023 17:10:41 +0530 Subject: [PATCH 03/17] feat(xep): Set base for XEP 0045 implementation Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/managers/namespaces.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index 0cb6b31..49227ef 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -31,4 +31,6 @@ const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; const stickersManager = 'org.moxxmpp.stickersmanager'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; +const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint'; +const occupantIdManager = 'org.moxxmpp.occupantidmanager'; const mucManager = 'org.moxxmpp.mucmanager'; From 05c41d31858771f674ddb139b57d2b929914daf0 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 14 Jun 2023 09:59:46 +0530 Subject: [PATCH 04/17] 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()); } From cd73f89e630a77654ee37174477aa16996ddd281 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 14 Jun 2023 10:05:25 +0530 Subject: [PATCH 05/17] feat(xep): Remove duplicate manager string Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/managers/namespaces.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index 49227ef..bb2d241 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -33,4 +33,3 @@ const stickersManager = 'org.moxxmpp.stickersmanager'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint'; const occupantIdManager = 'org.moxxmpp.occupantidmanager'; -const mucManager = 'org.moxxmpp.mucmanager'; From 70fdfaf16d24a1fc463997dfa87cb97484ff2e54 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 14 Jun 2023 10:08:03 +0530 Subject: [PATCH 06/17] feat(xep): Fix imports for xep_0045 files. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/xeps/xep_0045/types.dart | 4 ++-- packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart index 0397823..140befd 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -1,5 +1,5 @@ -import 'package:moxxmpp/moxxmpp.dart'; -import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; +import 'package:moxxmpp/src/jid.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; class RoomInformation { RoomInformation({ 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 6ccf1df..248d537 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -1,4 +1,13 @@ -import 'package:moxxmpp/moxxmpp.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/namespaces.dart'; +import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0045/types.dart'; From 1f1321b26912d6eea4473f2f4075056c9df88e5f Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Sun, 18 Jun 2023 20:09:06 +0530 Subject: [PATCH 07/17] feat(xep): Small fixes - review cycle 1. Signed-off-by: Ikjot Singh Dhody --- example_flutter/lib/main.dart | 43 +++++++++++ .../moxxmpp/lib/src/managers/namespaces.dart | 1 + packages/moxxmpp/lib/src/message.dart | 11 +-- .../moxxmpp/lib/src/xeps/xep_0045/errors.dart | 7 ++ .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 13 +++- .../lib/src/xeps/xep_0045/xep_0045.dart | 73 ++++++++----------- 6 files changed, 99 insertions(+), 49 deletions(-) diff --git a/example_flutter/lib/main.dart b/example_flutter/lib/main.dart index c30ba5c..b85a122 100644 --- a/example_flutter/lib/main.dart +++ b/example_flutter/lib/main.dart @@ -83,6 +83,8 @@ class _MyHomePageState extends State { ), MessageManager(), PresenceManager(), + OccupantIdManager(), + MUCManager() ]) ..registerFeatureNegotiators([ ResourceBindingNegotiator(), @@ -158,6 +160,47 @@ class _MyHomePageState extends State { ), obscureText: true, ), + TextButton( + onPressed: () async { + // final muc = connection.getManagerById(mucManager); + // final roomInformationResult = await muc!.queryRoomInformation( + // JID.fromString('moxxmpp-muc-test@muc.moxxy.org')); + // if (roomInformationResult.isType()) { + // print('Room information received'); + // print(roomInformationResult.get().jid); + // print(roomInformationResult.get().name); + // print(roomInformationResult.get().features); + // } + + // final muc = connection.getManagerById(mucManager); + // print('joining room'); + // final roomInformationResult = await muc!.joinRoom( + // JID.fromString('moxxmpp-muc-test@muc.moxxy.org/test_1')); + // if (roomInformationResult.isType()) { + // print(roomInformationResult.get()); + // } else { + // print(roomInformationResult.get()); + // } + + print('HERE IS YOUR JID'); + print(connection.resource); + final sid = connection.generateId(); + final originId = connection.generateId(); + final message = + connection.getManagerById(messageManager); + message!.sendMessage( + JID.fromString('moxxmpp-muc-test@muc.moxxy.org/ISD'), + TypedMap.fromList([ + const MessageBodyData('Testing'), + const MarkableData(true), + MessageIdData(sid), + StableIdData(originId, null), + ConversationTypeData(ConversationType.groupchatprivate) + ]), + ); + }, + child: const Text('Test'), + ), ], ), ), diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index bb2d241..49227ef 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -33,3 +33,4 @@ const stickersManager = 'org.moxxmpp.stickersmanager'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint'; const occupantIdManager = 'org.moxxmpp.occupantidmanager'; +const mucManager = 'org.moxxmpp.mucmanager'; diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index 9566828..00bfbe7 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -8,7 +8,6 @@ import 'package:moxxmpp/src/managers/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_0045/xep_0045.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0449.dart'; @@ -97,17 +96,15 @@ class MessageManager extends XmppManagerBase { /// data for building the message. Future sendMessage( JID to, - TypedMap extensions, - ) async { + TypedMap extensions, { + String type = 'chat', + }) async { await getAttributes().sendStanza( StanzaDetails( Stanza.message( to: to.toString(), id: extensions.get()?.id, - type: extensions.get()?.conversationType == - ConversationType.groupchat - ? 'groupchat' - : 'chat', + type: type, children: _messageSendingCallbacks .map((c) => c(extensions)) .flattened diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart index 801b116..f1771ac 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart @@ -1,7 +1,14 @@ +/// Represents an error related to Multi-User Chat (MUC). abstract class MUCError {} +/// Error indicating an invalid (non-supported) stanza received while going +/// through normal operation/flow of an MUC. class InvalidStanzaFormat extends MUCError {} +/// Represents an error indicating an abnormal condition while parsing +/// the DiscoInfo response stanza in Multi-User Chat (MUC). class InvalidDiscoInfoResponse extends MUCError {} +/// Returned when no nickname was specified from the client side while trying to +/// perform some actions on the MUC, such as joining the room. 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 140befd..b8dc73e 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -2,22 +2,33 @@ import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; class RoomInformation { + /// Represents information about a Multi-User Chat (MUC) room. RoomInformation({ required this.jid, required this.features, required this.name, }); + /// Constructs a [RoomInformation] object from a [DiscoInfo] object. + /// The [DiscoInfo] object contains the necessary information to populate + /// the [RoomInformation] fields. factory RoomInformation.fromDiscoInfo({ required DiscoInfo discoInfo, }) => RoomInformation( jid: discoInfo.jid!, features: discoInfo.features, - name: discoInfo.identities[0].name!, + name: discoInfo.identities + .firstWhere((i) => i.category == 'conference') + .name!, ); + /// The JID (Jabber ID) of the Multi-User Chat (MUC) room. final JID jid; + + /// A list of features supported by the Multi-User Chat (MUC) room. final List features; + + /// The name or title of the Multi-User Chat (MUC) room. 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 248d537..54194c0 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -7,6 +7,7 @@ import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; +import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0045/types.dart'; @@ -27,16 +28,15 @@ class MUCManager extends XmppManagerBase { Future> queryRoomInformation( JID roomJID, ) async { + final result = await getAttributes() + .getManagerById(discoManager)! + .discoInfoQuery(roomJID); + if (result.isType()) { + return Result(InvalidStanzaFormat()); + } try { - final attrs = getAttributes(); - final result = await attrs - .getManagerById(discoManager) - ?.discoInfoQuery(roomJID); - if (result!.isType()) { - return Result(InvalidStanzaFormat()); - } final roomInformation = RoomInformation.fromDiscoInfo( - discoInfo: result.get(), + discoInfo: result.get(), ); return Result(roomInformation); } catch (e) { @@ -45,30 +45,26 @@ class MUCManager extends XmppManagerBase { } Future> joinRoom( - JID roomJIDWithNickname, + JID roomJID, + String nick, ) async { - if (roomJIDWithNickname.resource.isEmpty) { + if (nick.isEmpty) { return Result(NoNicknameSpecified()); } - final attrs = getAttributes(); - try { - await attrs.sendStanza( - StanzaDetails( - Stanza.presence( - to: roomJIDWithNickname.toString(), - children: [ - XMLNode.xmlns( - tag: 'x', - xmlns: mucXmlns, - ) - ], - ), + await getAttributes().sendStanza( + StanzaDetails( + Stanza.presence( + to: roomJID.withResource(nick).toString(), + children: [ + XMLNode.xmlns( + tag: 'x', + xmlns: mucXmlns, + ) + ], ), - ); - return const Result(true); - } catch (e) { - return Result(InvalidStanzaFormat()); - } + ), + ); + return const Result(true); } Future> leaveRoom( @@ -77,19 +73,14 @@ class MUCManager extends XmppManagerBase { if (roomJIDWithNickname.resource.isEmpty) { return Result(NoNicknameSpecified()); } - final attrs = getAttributes(); - try { - await attrs.sendStanza( - StanzaDetails( - Stanza.presence( - to: roomJIDWithNickname.toString(), - type: 'unavailable', - ), + await getAttributes().sendStanza( + StanzaDetails( + Stanza.presence( + to: roomJIDWithNickname.toString(), + type: 'unavailable', ), - ); - return const Result(true); - } catch (e) { - return Result(InvalidStanzaFormat()); - } + ), + ); + return const Result(true); } } From 8728166a4de3ad71c2fa43ca8c9804ef4bdb6b99 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Sun, 18 Jun 2023 20:49:13 +0530 Subject: [PATCH 08/17] feat(xep): Add cache and roomstate to MUC implementation. Signed-off-by: Ikjot Singh Dhody --- .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 11 ++++++ .../lib/src/xeps/xep_0045/xep_0045.dart | 37 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart index b8dc73e..8dcceed 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -32,3 +32,14 @@ class RoomInformation { /// The name or title of the Multi-User Chat (MUC) room. final String name; } + +class RoomState { + RoomState({ + required this.roomJid, + required this.roomInformation, + this.nick, + }); + final JID roomJid; + final RoomInformation roomInformation; + String? nick; +} 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 54194c0..39a3e4d 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -11,6 +11,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0045/types.dart'; +import 'package:synchronized/synchronized.dart'; enum ConversationType { chat, groupchat, groupchatprivate } @@ -25,6 +26,12 @@ class MUCManager extends XmppManagerBase { @override Future isSupported() async => true; + /// Map full JID to RoomState + final Map _mucRoomCache = {}; + + /// Cache lock + final Lock _cacheLock = Lock(); + Future> queryRoomInformation( JID roomJID, ) async { @@ -38,6 +45,14 @@ class MUCManager extends XmppManagerBase { final roomInformation = RoomInformation.fromDiscoInfo( discoInfo: result.get(), ); + await _cacheLock.synchronized( + () async { + _mucRoomCache[roomJID] = RoomState( + roomJid: roomJID, + roomInformation: roomInformation, + ); + }, + ); return Result(roomInformation); } catch (e) { return Result(InvalidDiscoInfoResponse); @@ -64,23 +79,39 @@ class MUCManager extends XmppManagerBase { ), ), ); + await _cacheLock.synchronized( + () async { + _mucRoomCache[roomJID]!.nick = nick; + }, + ); return const Result(true); } Future> leaveRoom( - JID roomJIDWithNickname, + JID roomJID, ) async { - if (roomJIDWithNickname.resource.isEmpty) { + String? nick; + await _cacheLock.synchronized( + () async { + nick = _mucRoomCache[roomJID]!.nick; + }, + ); + if (nick!.isEmpty) { return Result(NoNicknameSpecified()); } await getAttributes().sendStanza( StanzaDetails( Stanza.presence( - to: roomJIDWithNickname.toString(), + to: roomJID.withResource(nick!).toString(), type: 'unavailable', ), ), ); + await _cacheLock.synchronized( + () async { + _mucRoomCache.remove(roomJID); + }, + ); return const Result(true); } } From 51bca6c25d4d0ae20ed807ce4396d40656eed136 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Mon, 19 Jun 2023 18:40:39 +0530 Subject: [PATCH 09/17] feat(xep): XEP-0045 cache fixes. Signed-off-by: Ikjot Singh Dhody --- example_flutter/lib/main.dart | 43 ------------------- .../moxxmpp/lib/src/xeps/xep_0045/types.dart | 4 +- .../lib/src/xeps/xep_0045/xep_0045.dart | 30 ++----------- 3 files changed, 5 insertions(+), 72 deletions(-) diff --git a/example_flutter/lib/main.dart b/example_flutter/lib/main.dart index b85a122..c30ba5c 100644 --- a/example_flutter/lib/main.dart +++ b/example_flutter/lib/main.dart @@ -83,8 +83,6 @@ class _MyHomePageState extends State { ), MessageManager(), PresenceManager(), - OccupantIdManager(), - MUCManager() ]) ..registerFeatureNegotiators([ ResourceBindingNegotiator(), @@ -160,47 +158,6 @@ class _MyHomePageState extends State { ), obscureText: true, ), - TextButton( - onPressed: () async { - // final muc = connection.getManagerById(mucManager); - // final roomInformationResult = await muc!.queryRoomInformation( - // JID.fromString('moxxmpp-muc-test@muc.moxxy.org')); - // if (roomInformationResult.isType()) { - // print('Room information received'); - // print(roomInformationResult.get().jid); - // print(roomInformationResult.get().name); - // print(roomInformationResult.get().features); - // } - - // final muc = connection.getManagerById(mucManager); - // print('joining room'); - // final roomInformationResult = await muc!.joinRoom( - // JID.fromString('moxxmpp-muc-test@muc.moxxy.org/test_1')); - // if (roomInformationResult.isType()) { - // print(roomInformationResult.get()); - // } else { - // print(roomInformationResult.get()); - // } - - print('HERE IS YOUR JID'); - print(connection.resource); - final sid = connection.generateId(); - final originId = connection.generateId(); - final message = - connection.getManagerById(messageManager); - message!.sendMessage( - JID.fromString('moxxmpp-muc-test@muc.moxxy.org/ISD'), - TypedMap.fromList([ - const MessageBodyData('Testing'), - const MarkableData(true), - MessageIdData(sid), - StableIdData(originId, null), - ConversationTypeData(ConversationType.groupchatprivate) - ]), - ); - }, - child: const Text('Test'), - ), ], ), ), diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart index 8dcceed..31e2c80 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -23,7 +23,7 @@ class RoomInformation { .name!, ); - /// The JID (Jabber ID) of the Multi-User Chat (MUC) room. + /// The JID of the Multi-User Chat (MUC) room. final JID jid; /// A list of features supported by the Multi-User Chat (MUC) room. @@ -36,10 +36,8 @@ class RoomInformation { class RoomState { RoomState({ required this.roomJid, - required this.roomInformation, this.nick, }); final JID roomJid; - final RoomInformation roomInformation; String? nick; } 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 39a3e4d..1a32c79 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -13,13 +13,6 @@ import 'package:moxxmpp/src/xeps/xep_0045/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0045/types.dart'; import 'package:synchronized/synchronized.dart'; -enum ConversationType { chat, groupchat, groupchatprivate } - -class ConversationTypeData extends StanzaHandlerExtension { - ConversationTypeData(this.conversationType); - final ConversationType conversationType; -} - class MUCManager extends XmppManagerBase { MUCManager() : super(mucManager); @@ -45,14 +38,6 @@ class MUCManager extends XmppManagerBase { final roomInformation = RoomInformation.fromDiscoInfo( discoInfo: result.get(), ); - await _cacheLock.synchronized( - () async { - _mucRoomCache[roomJID] = RoomState( - roomJid: roomJID, - roomInformation: roomInformation, - ); - }, - ); return Result(roomInformation); } catch (e) { return Result(InvalidDiscoInfoResponse); @@ -80,7 +65,7 @@ class MUCManager extends XmppManagerBase { ), ); await _cacheLock.synchronized( - () async { + () { _mucRoomCache[roomJID]!.nick = nick; }, ); @@ -90,15 +75,8 @@ class MUCManager extends XmppManagerBase { Future> leaveRoom( JID roomJID, ) async { - String? nick; - await _cacheLock.synchronized( - () async { - nick = _mucRoomCache[roomJID]!.nick; - }, - ); - if (nick!.isEmpty) { - return Result(NoNicknameSpecified()); - } + final nick = + await _cacheLock.synchronized(() => _mucRoomCache[roomJID]!.nick); await getAttributes().sendStanza( StanzaDetails( Stanza.presence( @@ -108,7 +86,7 @@ class MUCManager extends XmppManagerBase { ), ); await _cacheLock.synchronized( - () async { + () { _mucRoomCache.remove(roomJID); }, ); From 217c3ac236572fd557c31db1e7fe091757479520 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Tue, 20 Jun 2023 17:38:30 +0530 Subject: [PATCH 10/17] feat(xep): Fix cache issue with join/leaveRoom. Signed-off-by: Ikjot Singh Dhody --- .../lib/src/xeps/xep_0045/xep_0045.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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 1a32c79..66e1150 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -45,7 +45,7 @@ class MUCManager extends XmppManagerBase { } Future> joinRoom( - JID roomJID, + JID roomJid, String nick, ) async { if (nick.isEmpty) { @@ -54,7 +54,7 @@ class MUCManager extends XmppManagerBase { await getAttributes().sendStanza( StanzaDetails( Stanza.presence( - to: roomJID.withResource(nick).toString(), + to: roomJid.withResource(nick).toString(), children: [ XMLNode.xmlns( tag: 'x', @@ -66,28 +66,31 @@ class MUCManager extends XmppManagerBase { ); await _cacheLock.synchronized( () { - _mucRoomCache[roomJID]!.nick = nick; + _mucRoomCache[roomJid] = RoomState(roomJid: roomJid, nick: nick); }, ); return const Result(true); } Future> leaveRoom( - JID roomJID, + JID roomJid, ) async { - final nick = - await _cacheLock.synchronized(() => _mucRoomCache[roomJID]!.nick); + final nick = await _cacheLock.synchronized(() { + final nick = _mucRoomCache[roomJid]?.nick; + _mucRoomCache.remove(roomJid); + return nick; + }); await getAttributes().sendStanza( StanzaDetails( Stanza.presence( - to: roomJID.withResource(nick!).toString(), + to: roomJid.withResource(nick!).toString(), type: 'unavailable', ), ), ); await _cacheLock.synchronized( () { - _mucRoomCache.remove(roomJID); + _mucRoomCache.remove(roomJid); }, ); return const Result(true); From b7d53b8f478d7f9ca836cba31d4cfa5242bf7c83 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Tue, 20 Jun 2023 17:44:24 +0530 Subject: [PATCH 11/17] feat(xep): Add docstings for the XEP-0045 routines Signed-off-by: Ikjot Singh Dhody --- .../moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 66e1150..26a2e6c 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -25,6 +25,11 @@ class MUCManager extends XmppManagerBase { /// Cache lock final Lock _cacheLock = Lock(); + /// Queries the information of a Multi-User Chat room. + /// + /// Retrieves the information about the specified MUC room by performing a + /// disco info query. Returns a [Result] with the [RoomInformation] on success + /// or an appropriate [MUCError] on failure. Future> queryRoomInformation( JID roomJID, ) async { @@ -44,6 +49,12 @@ class MUCManager extends XmppManagerBase { } } + /// Joins a Multi-User Chat room. + /// + /// Joins the specified MUC room using the provided nickname. Sends a presence + /// stanza with the appropriate attributes to join the room. Returns a [Result] + /// with a boolean value indicating success or failure, or an [MUCError] + /// if applicable. Future> joinRoom( JID roomJid, String nick, @@ -72,6 +83,12 @@ class MUCManager extends XmppManagerBase { return const Result(true); } + /// Leaves a Multi-User Chat room. + /// + /// Leaves the specified MUC room by sending an 'unavailable' presence stanza. + /// Removes the corresponding room entry from the cache. Returns a [Result] + /// with a boolean value indicating success or failure, or an [MUCError] + /// if applicable. Future> leaveRoom( JID roomJid, ) async { From e6bd6d05cd2f3a97e0930f76f63541efd4d46b61 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 21 Jun 2023 00:41:51 +0530 Subject: [PATCH 12/17] feat(xep): Remove NOOP cache access. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 5 ----- 1 file changed, 5 deletions(-) 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 26a2e6c..b6a7859 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -105,11 +105,6 @@ class MUCManager extends XmppManagerBase { ), ), ); - await _cacheLock.synchronized( - () { - _mucRoomCache.remove(roomJid); - }, - ); return const Result(true); } } From a873edb9eca38829186c097d3ba19271d383efb4 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 21 Jun 2023 12:20:14 +0530 Subject: [PATCH 13/17] feat(xep): Check for null nick before leaveRoom. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart | 5 +++++ packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 3 +++ 2 files changed, 8 insertions(+) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart index f1771ac..7cc2b8f 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart @@ -12,3 +12,8 @@ class InvalidDiscoInfoResponse extends MUCError {} /// Returned when no nickname was specified from the client side while trying to /// perform some actions on the MUC, such as joining the room. class NoNicknameSpecified extends MUCError {} + +/// This error occurs when a user attempts to perform an action that requires +/// them to be a member of a room, but they are not currently joined to +/// that room. +class RoomNotJoinedError extends MUCError {} 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 b6a7859..80c50a4 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -97,6 +97,9 @@ class MUCManager extends XmppManagerBase { _mucRoomCache.remove(roomJid); return nick; }); + if (nick == null) { + return Result(RoomNotJoinedError); + } await getAttributes().sendStanza( StanzaDetails( Stanza.presence( From 3ebd9b86ec3a53ba967b4d6c41446da7862b2cd1 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Wed, 21 Jun 2023 23:34:21 +0530 Subject: [PATCH 14/17] feat(xep): Fix lint issues and use moxlib for result. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 80c50a4..6886f0b 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -1,11 +1,10 @@ +import 'package:moxlib/moxlib.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/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; @@ -103,7 +102,7 @@ class MUCManager extends XmppManagerBase { await getAttributes().sendStanza( StanzaDetails( Stanza.presence( - to: roomJid.withResource(nick!).toString(), + to: roomJid.withResource(nick).toString(), type: 'unavailable', ), ), From 04dfc6d2acd1a322d06a3be32144b0a5d795ccb9 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Fri, 30 Jun 2023 18:33:00 +0530 Subject: [PATCH 15/17] feat(xep): Replace DiscoError with StanzaError. Signed-off-by: Ikjot Singh Dhody --- packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6886f0b..64a5976 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart @@ -35,7 +35,7 @@ class MUCManager extends XmppManagerBase { final result = await getAttributes() .getManagerById(discoManager)! .discoInfoQuery(roomJID); - if (result.isType()) { + if (result.isType()) { return Result(InvalidStanzaFormat()); } try { From 8b00e8516743c7e5cb73634d42680e3d752298c8 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Sat, 1 Jul 2023 09:16:51 +0530 Subject: [PATCH 16/17] feat(xep): Add example for XEP 0045 Moxxmpp. Signed-off-by: Ikjot Singh Dhody --- examples_dart/bin/muc_client.dart | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 examples_dart/bin/muc_client.dart diff --git a/examples_dart/bin/muc_client.dart b/examples_dart/bin/muc_client.dart new file mode 100644 index 0000000..7455c78 --- /dev/null +++ b/examples_dart/bin/muc_client.dart @@ -0,0 +1,87 @@ +import 'package:cli_repl/cli_repl.dart'; +import 'package:example_dart/arguments.dart'; +import 'package:example_dart/socket.dart'; +import 'package:logging/logging.dart'; +import 'package:moxxmpp/moxxmpp.dart'; + +void main(List args) async { + // Set up logging + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print( + '[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}', + ); + }); + + final parser = ArgumentParser() + ..parser.addOption('to', help: 'The JID to send messages to') + ..parser.addOption('nick', help: 'The nickname with which to join the MUC'); + final options = parser.handleArguments(args); + if (options == null) { + return; + } + + // Connect + final jid = parser.jid; + final to = JID.fromString(options['to']! as String).toBare(); + final nick = options['nick']! as String; + print(parser.connectionSettings); + final connection = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + ExampleTCPSocketWrapper(parser.srvRecord), + )..connectionSettings = parser.connectionSettings; + + print(parser.connectionSettings.host); + print(parser.connectionSettings.jid); + print(parser.connectionSettings.password); + print(parser.connectionSettings.port); + + // Register the managers and negotiators + await connection.registerManagers([ + PresenceManager(), + DiscoManager([]), + PubSubManager(), + MessageManager(), + MUCManager(), + ]); + await connection.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StartTlsNegotiator(), + SaslScramNegotiator(10, '', '', ScramHashType.sha1), + ]); + + // Connect + Logger.root.info('Connecting...'); + final result = + await connection.connect(shouldReconnect: false, waitUntilLogin: true); + if (!result.isType()) { + Logger.root.severe('Authentication failed!'); + return; + } + Logger.root.info('Connected.'); + + // Join room + await connection.getManagerById(mucManager)!.joinRoom(to, nick); + + final repl = Repl(prompt: '> '); + await for (final line in repl.runAsync()) { + await connection + .getManagerById(messageManager)! + .sendMessage( + to, + TypedMap.fromList([ + MessageBodyData(line), + ]), + type: 'groupchat'); + } + + // Leave room + await connection.getManagerById(mucManager)!.leaveRoom(to); + + // Disconnect + await connection.disconnect(); +} From d3742ea156ec64b065c16cb38538f29407f853b0 Mon Sep 17 00:00:00 2001 From: Ikjot Singh Dhody Date: Sat, 1 Jul 2023 17:29:51 +0530 Subject: [PATCH 17/17] feat(xep): Small fixes - MUC Example. Signed-off-by: Ikjot Singh Dhody --- examples_dart/bin/muc_client.dart | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/examples_dart/bin/muc_client.dart b/examples_dart/bin/muc_client.dart index 7455c78..3b874cd 100644 --- a/examples_dart/bin/muc_client.dart +++ b/examples_dart/bin/muc_client.dart @@ -15,7 +15,7 @@ void main(List args) async { }); final parser = ArgumentParser() - ..parser.addOption('to', help: 'The JID to send messages to') + ..parser.addOption('muc', help: 'The MUC to send messages to') ..parser.addOption('nick', help: 'The nickname with which to join the MUC'); final options = parser.handleArguments(args); if (options == null) { @@ -23,10 +23,8 @@ void main(List args) async { } // Connect - final jid = parser.jid; - final to = JID.fromString(options['to']! as String).toBare(); + final muc = JID.fromString(options['muc']! as String).toBare(); final nick = options['nick']! as String; - print(parser.connectionSettings); final connection = XmppConnection( TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), @@ -34,11 +32,6 @@ void main(List args) async { ExampleTCPSocketWrapper(parser.srvRecord), )..connectionSettings = parser.connectionSettings; - print(parser.connectionSettings.host); - print(parser.connectionSettings.jid); - print(parser.connectionSettings.password); - print(parser.connectionSettings.port); - // Register the managers and negotiators await connection.registerManagers([ PresenceManager(), @@ -65,14 +58,14 @@ void main(List args) async { Logger.root.info('Connected.'); // Join room - await connection.getManagerById(mucManager)!.joinRoom(to, nick); + await connection.getManagerById(mucManager)!.joinRoom(muc, nick); final repl = Repl(prompt: '> '); await for (final line in repl.runAsync()) { await connection .getManagerById(messageManager)! .sendMessage( - to, + muc, TypedMap.fromList([ MessageBodyData(line), ]), @@ -80,7 +73,7 @@ void main(List args) async { } // Leave room - await connection.getManagerById(mucManager)!.leaveRoom(to); + await connection.getManagerById(mucManager)!.leaveRoom(muc); // Disconnect await connection.disconnect();