diff --git a/examples_dart/bin/muc_client.dart b/examples_dart/bin/muc_client.dart index 4d2e45a..4e88c84 100644 --- a/examples_dart/bin/muc_client.dart +++ b/examples_dart/bin/muc_client.dart @@ -38,6 +38,7 @@ void main(List args) async { DiscoManager([]), PubSubManager(), MessageManager(), + StableIdManager(), MUCManager(), ]); await connection.registerFeatureNegotiators([ @@ -57,12 +58,28 @@ void main(List args) async { } Logger.root.info('Connected.'); + // Print received messages. + connection + .asBroadcastStream() + .where((event) => event is MessageEvent) + .listen((event) { + event as MessageEvent; + + // Ignore messages with no + final body = event.get()?.body; + if (body == null) { + return; + } + + print('=====> [${event.from}] $body'); + }); + // Join room await connection.getManagerById(mucManager)!.joinRoom( - muc, - nick, - maxHistoryStanzas: 0, - ); + muc, + nick, + maxHistoryStanzas: 0, + ); final repl = Repl(prompt: '> '); await for (final line in repl.runAsync()) { @@ -72,6 +89,11 @@ void main(List args) async { muc, TypedMap.fromList([ MessageBodyData(line), + StableIdData( + // NOTE: Don't do this. Use a UUID. + DateTime.now().millisecondsSinceEpoch.toString(), + null, + ), ]), type: 'groupchat'); } diff --git a/examples_dart/pubspec.yaml b/examples_dart/pubspec.yaml index 180d0a7..b2cd194 100644 --- a/examples_dart/pubspec.yaml +++ b/examples_dart/pubspec.yaml @@ -3,7 +3,7 @@ description: A collection of samples for moxxmpp. version: 1.0.0 environment: - sdk: '>=2.18.0 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: args: 2.4.1 diff --git a/flake.lock b/flake.lock index 2becad2..fa6756e 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1689798050, - "narHash": "sha256-ZyFPra7N0MF803o55dYQQyX9b/BmXr6QTCyN7slRThY=", + "lastModified": 1694377165, + "narHash": "sha256-NeIlZIElbkbKaNK5SZv6ULcFT/UGIICb3q7GPpkf9jk=", "owner": "tadfisher", "repo": "android-nixpkgs", - "rev": "9aa0e2990da86de8ca203af313668851dcb9ea6e", + "rev": "b020dc733ee69393841a50cf94d45735d5a5a57a", "type": "github" }, "original": { @@ -29,11 +29,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1688380630, - "narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=", + "lastModified": 1693833206, + "narHash": "sha256-wHOY0nnD6gWj8u9uI85/YlsganYyWRK1hLFZulZwfmY=", "owner": "numtide", "repo": "devshell", - "rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205", + "rev": "65114ea495a8d3cc1352368bf170d67ef005aa5a", "type": "github" }, "original": { @@ -47,11 +47,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", "owner": "numtide", "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", "type": "github" }, "original": { @@ -61,12 +61,15 @@ } }, "flake-utils_2": { + "inputs": { + "systems": "systems_3" + }, "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", "owner": "numtide", "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", "type": "github" }, "original": { @@ -77,11 +80,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1689679375, - "narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=", + "lastModified": 1694183432, + "narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "684c17c429c42515bafb3ad775d2a710947f3d67", + "rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b", "type": "github" }, "original": { @@ -93,11 +96,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1689752456, - "narHash": "sha256-VOChdECcEI8ixz8QY+YC4JaNEFwQd1V8bA0G4B28Ki0=", + "lastModified": 1694343207, + "narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7f256d7da238cb627ef189d56ed590739f42f13b", + "rev": "78058d810644f5ed276804ce7ea9e82d92bee293", "type": "github" }, "original": { @@ -143,6 +146,21 @@ "repo": "default", "type": "github" } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 7a32556..4bc839e 100644 --- a/flake.nix +++ b/flake.nix @@ -68,7 +68,7 @@ }; in pkgs.mkShell { buildInputs = with pkgs; [ - flutter37 pinnedJDK sdk dart # Dart + flutter pinnedJDK sdk dart # Dart gitlint # Code hygiene ripgrep # General utilities diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index c5e6e10..ee1e9c1 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -475,7 +475,7 @@ class XmppConnection { false, false, newStanza, - TypedMap(), + details.extensions ?? TypedMap(), encrypted: details.encrypted, shouldEncrypt: details.shouldEncrypt, forceEncryption: details.forceEncryption, diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index c6727a7..4209598 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -117,6 +117,7 @@ class MessageManager extends XmppManagerBase { .flattened .toList(), ), + extensions: extensions, awaitable: false, ), ); diff --git a/packages/moxxmpp/lib/src/stanza.dart b/packages/moxxmpp/lib/src/stanza.dart index 2e5804b..8439c27 100644 --- a/packages/moxxmpp/lib/src/stanza.dart +++ b/packages/moxxmpp/lib/src/stanza.dart @@ -7,6 +7,7 @@ import 'package:moxxmpp/src/util/typed_map.dart'; class StanzaDetails { const StanzaDetails( this.stanza, { + this.extensions, this.addId = true, this.awaitable = true, this.shouldEncrypt = true, @@ -19,6 +20,9 @@ class StanzaDetails { /// The stanza to send. final Stanza stanza; + /// The extension data used for constructing the stanza. + final TypedMap? extensions; + /// Flag indicating whether a stanza id should be added before sending. final bool addId; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart index 5c4a7fb..d26a080 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0045/types.dart @@ -33,8 +33,13 @@ class RoomInformation { final String name; } +/// The used message-id and an optional origin-id. +typedef PendingMessage = (String, String?); + class RoomState { - RoomState({required this.roomJid, this.nick, required this.joined}); + RoomState({required this.roomJid, this.nick, required this.joined}) { + pendingMessages = List.empty(growable: true); + } /// The JID of the room. final JID roomJid; @@ -45,14 +50,5 @@ class RoomState { /// Flag whether we're joined and can process messages bool joined; - RoomState copyWith({ - bool? joined, - String? nick, - }) { - return RoomState( - roomJid: roomJid, - joined: joined ?? this.joined, - nick: nick ?? this.nick, - ); - } + late final List pendingMessages; } 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 b84ac41..986df43 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:moxxmpp/src/xeps/xep_0359.dart'; import 'package:synchronized/extension.dart'; import 'package:synchronized/synchronized.dart'; @@ -33,7 +34,15 @@ class MUCManager extends XmppManagerBase { callback: _onMessage, // Before the message handler priority: -99, - ) + ), + ]; + + @override + List getOutgoingPreStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'message', + callback: _onMessageSent, + ), ]; /// Queries the information of a Multi-User Chat room. @@ -138,11 +147,33 @@ class MUCManager extends XmppManagerBase { return const Result(true); } + Future _onMessageSent( + Stanza message, + StanzaHandlerData state, + ) async { + if (message.to == null) { + return state; + } + final toJid = JID.fromString(message.to!); + + return _cacheLock.synchronized(() { + if (!_mucRoomCache.containsKey(toJid)) { + return state; + } + + _mucRoomCache[toJid]!.pendingMessages.add( + (message.id!, state.extensions.get()?.originId), + ); + return state; + }); + } + Future _onMessage( Stanza message, StanzaHandlerData state, ) async { - final roomJid = JID.fromString(message.from!).toBare(); + final fromJid = JID.fromString(message.from!); + final roomJid = fromJid.toBare(); return _mucRoomCache.synchronized(() { final roomState = _mucRoomCache[roomJid]; if (roomState == null) { @@ -153,7 +184,7 @@ class MUCManager extends XmppManagerBase { // The room subject marks the end of the join flow. if (!roomState.joined) { // Mark the room as joined. - _mucRoomCache[roomJid] = roomState.copyWith(joined: true); + _mucRoomCache[roomJid]!.joined = true; logger.finest('$roomJid is now joined'); } @@ -168,7 +199,23 @@ class MUCManager extends XmppManagerBase { } else { if (!roomState.joined) { // Ignore the discussion history. - // TODO: Implement a copyWith method + return StanzaHandlerData( + true, + false, + message, + state.extensions, + ); + } + + // Check if this is the message reflection. + final pending = + (message.id!, state.extensions.get()?.originId); + if (fromJid.resource == roomState.nick && + roomState.pendingMessages.contains(pending)) { + // Silently drop the message. + roomState.pendingMessages.remove(pending); + + // TODO(Unknown): Maybe send an event stating that we received the reflection. return StanzaHandlerData( true, false, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0359.dart b/packages/moxxmpp/lib/src/xeps/xep_0359.dart index ed4f07b..24108cc 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0359.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0359.dart @@ -127,7 +127,13 @@ class StableIdManager extends XmppManagerBase { TypedMap extensions, ) { final data = extensions.get(); - return data != null ? data.toXML() : []; + if (data?.originId != null) { + return [ + data!.toOriginIdElement(), + ]; + } + + return []; } @override diff --git a/packages/moxxmpp/lib/src/xeps/xep_0384/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0384/errors.dart index ea2169e..9a54334 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0384/errors.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0384/errors.dart @@ -2,10 +2,10 @@ abstract class OmemoError {} class UnknownOmemoError extends OmemoError {} -class InvalidAffixElementsException with Exception {} +class InvalidAffixElementsException implements Exception {} class OmemoNotSupportedForContactException extends OmemoError {} -class EncryptionFailedException with Exception {} +class EncryptionFailedException implements Exception {} -class InvalidEnvelopePayloadException with Exception {} +class InvalidEnvelopePayloadException implements Exception {} diff --git a/packages/moxxmpp/pubspec.yaml b/packages/moxxmpp/pubspec.yaml index 43f385e..686f02e 100644 --- a/packages/moxxmpp/pubspec.yaml +++ b/packages/moxxmpp/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://codeberg.org/moxxy/moxxmpp publish_to: https://git.polynom.me/api/packages/Moxxy/pub environment: - sdk: '>=2.17.5 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: collection: ^1.16.0