Compare commits

...

5 Commits

Author SHA1 Message Date
76a9f7be7a feat(xep): Allow adding MUCs to join later
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-22 21:43:56 +02:00
afa3927720 feat(xep): Rejoin groupchats on a new stream 2023-09-22 21:22:18 +02:00
5f36289f50 fix(all): Fix linter warnings 2023-09-22 20:57:05 +02:00
fbe3b90200 feat(xep): Implement ignoring the message reflection 2023-09-22 20:42:45 +02:00
d7c13abde6 feat(xep): Allow ignoring the discussion history
Also allow specifying the amount of stanzas of discussion history we
want.
2023-09-22 19:23:35 +02:00
40 changed files with 452 additions and 190 deletions

View File

@ -38,6 +38,7 @@ void main(List<String> args) async {
DiscoManager([]), DiscoManager([]),
PubSubManager(), PubSubManager(),
MessageManager(), MessageManager(),
StableIdManager(),
MUCManager(), MUCManager(),
]); ]);
await connection.registerFeatureNegotiators([ await connection.registerFeatureNegotiators([
@ -57,8 +58,28 @@ void main(List<String> args) async {
} }
Logger.root.info('Connected.'); Logger.root.info('Connected.');
// Print received messages.
connection
.asBroadcastStream()
.where((event) => event is MessageEvent)
.listen((event) {
event as MessageEvent;
// Ignore messages with no <body />
final body = event.get<MessageBodyData>()?.body;
if (body == null) {
return;
}
print('=====> [${event.from}] $body');
});
// Join room // Join room
await connection.getManagerById<MUCManager>(mucManager)!.joinRoom(muc, nick); await connection.getManagerById<MUCManager>(mucManager)!.joinRoom(
muc,
nick,
maxHistoryStanzas: 0,
);
final repl = Repl(prompt: '> '); final repl = Repl(prompt: '> ');
await for (final line in repl.runAsync()) { await for (final line in repl.runAsync()) {
@ -68,6 +89,11 @@ void main(List<String> args) async {
muc, muc,
TypedMap<StanzaHandlerExtension>.fromList([ TypedMap<StanzaHandlerExtension>.fromList([
MessageBodyData(line), MessageBodyData(line),
StableIdData(
// NOTE: Don't do this. Use a UUID.
DateTime.now().millisecondsSinceEpoch.toString(),
null,
),
]), ]),
type: 'groupchat'); type: 'groupchat');
} }

View File

@ -3,7 +3,7 @@ description: A collection of samples for moxxmpp.
version: 1.0.0 version: 1.0.0
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=3.0.0 <4.0.0'
dependencies: dependencies:
args: 2.4.1 args: 2.4.1

View File

@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1689798050, "lastModified": 1694377165,
"narHash": "sha256-ZyFPra7N0MF803o55dYQQyX9b/BmXr6QTCyN7slRThY=", "narHash": "sha256-NeIlZIElbkbKaNK5SZv6ULcFT/UGIICb3q7GPpkf9jk=",
"owner": "tadfisher", "owner": "tadfisher",
"repo": "android-nixpkgs", "repo": "android-nixpkgs",
"rev": "9aa0e2990da86de8ca203af313668851dcb9ea6e", "rev": "b020dc733ee69393841a50cf94d45735d5a5a57a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -29,11 +29,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1688380630, "lastModified": 1693833206,
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=", "narHash": "sha256-wHOY0nnD6gWj8u9uI85/YlsganYyWRK1hLFZulZwfmY=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205", "rev": "65114ea495a8d3cc1352368bf170d67ef005aa5a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -47,11 +47,11 @@
"systems": "systems_2" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1689068808, "lastModified": 1692799911,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -61,12 +61,15 @@
} }
}, },
"flake-utils_2": { "flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1692799911,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -77,11 +80,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1689679375, "lastModified": 1694183432,
"narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=", "narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "684c17c429c42515bafb3ad775d2a710947f3d67", "rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -93,11 +96,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1689752456, "lastModified": 1694343207,
"narHash": "sha256-VOChdECcEI8ixz8QY+YC4JaNEFwQd1V8bA0G4B28Ki0=", "narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "7f256d7da238cb627ef189d56ed590739f42f13b", "rev": "78058d810644f5ed276804ce7ea9e82d92bee293",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -143,6 +146,21 @@
"repo": "default", "repo": "default",
"type": "github" "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", "root": "root",

View File

@ -68,7 +68,7 @@
}; };
in pkgs.mkShell { in pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter37 pinnedJDK sdk dart # Dart flutter pinnedJDK sdk dart # Dart
gitlint # Code hygiene gitlint # Code hygiene
ripgrep # General utilities ripgrep # General utilities

View File

@ -475,7 +475,7 @@ class XmppConnection {
false, false,
false, false,
newStanza, newStanza,
TypedMap(), details.extensions ?? TypedMap(),
encrypted: details.encrypted, encrypted: details.encrypted,
shouldEncrypt: details.shouldEncrypt, shouldEncrypt: details.shouldEncrypt,
forceEncryption: details.forceEncryption, forceEncryption: details.forceEncryption,

View File

@ -66,7 +66,7 @@ class MessageManager extends XmppManagerBase {
stanzaTag: 'message', stanzaTag: 'message',
callback: _onMessage, callback: _onMessage,
priority: messageHandlerPriority, priority: messageHandlerPriority,
) ),
]; ];
@override @override
@ -117,6 +117,7 @@ class MessageManager extends XmppManagerBase {
.flattened .flattened
.toList(), .toList(),
), ),
extensions: extensions,
awaitable: false, awaitable: false,
), ),
); );

View File

@ -66,7 +66,7 @@ class PresenceManager extends XmppManagerBase {
stanzaTag: 'presence', stanzaTag: 'presence',
callback: _onPresence, callback: _onPresence,
priority: presenceHandlerPriority, priority: presenceHandlerPriority,
) ),
]; ];
@override @override

View File

@ -122,7 +122,7 @@ class RosterManager extends XmppManagerBase {
tagName: 'query', tagName: 'query',
tagXmlns: rosterXmlns, tagXmlns: rosterXmlns,
callback: _onRosterPush, callback: _onRosterPush,
) ),
]; ];
@override @override
@ -277,7 +277,7 @@ class RosterManager extends XmppManagerBase {
attributes: { attributes: {
'ver': await _stateManager.getRosterVersion() ?? '', 'ver': await _stateManager.getRosterVersion() ?? '',
}, },
) ),
], ],
), ),
), ),
@ -319,14 +319,12 @@ class RosterManager extends XmppManagerBase {
tag: 'item', tag: 'item',
attributes: <String, String>{ attributes: <String, String>{
'jid': jid, 'jid': jid,
...title == jid.split('@')[0] if (title == jid.split('@')[0]) 'name': title,
? <String, String>{}
: <String, String>{'name': title}
}, },
children: (groups ?? []) children: (groups ?? [])
.map((group) => XMLNode(tag: 'group', text: group)) .map((group) => XMLNode(tag: 'group', text: group))
.toList(), .toList(),
) ),
], ],
), ),
], ],
@ -357,13 +355,13 @@ class RosterManager extends XmppManagerBase {
children: [ children: [
XMLNode( XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{ attributes: {
'jid': jid, 'jid': jid,
'subscription': 'remove' 'subscription': 'remove',
}, },
) ),
], ],
) ),
], ],
), ),
), ),

View File

@ -7,6 +7,7 @@ import 'package:moxxmpp/src/util/typed_map.dart';
class StanzaDetails { class StanzaDetails {
const StanzaDetails( const StanzaDetails(
this.stanza, { this.stanza, {
this.extensions,
this.addId = true, this.addId = true,
this.awaitable = true, this.awaitable = true,
this.shouldEncrypt = true, this.shouldEncrypt = true,
@ -19,6 +20,9 @@ class StanzaDetails {
/// The stanza to send. /// The stanza to send.
final Stanza stanza; final Stanza stanza;
/// The extension data used for constructing the stanza.
final TypedMap<StanzaHandlerExtension>? extensions;
/// Flag indicating whether a stanza id should be added before sending. /// Flag indicating whether a stanza id should be added before sending.
final bool addId; final bool addId;
@ -244,15 +248,14 @@ XMLNode buildErrorElement(String type, String condition, {String? text}) {
XMLNode.xmlns( XMLNode.xmlns(
tag: condition, tag: condition,
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
children: text != null children: [
? [ if (text != null)
XMLNode.xmlns( XMLNode.xmlns(
tag: 'text', tag: 'text',
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
text: text, text: text,
) ),
] ],
: [],
), ),
], ],
); );

View File

@ -10,14 +10,14 @@ class DataFormOption {
XMLNode toXml() { XMLNode toXml() {
return XMLNode( return XMLNode(
tag: 'option', tag: 'option',
attributes: label != null attributes: {
? <String, dynamic>{'label': label} if (label != null) 'label': label,
: <String, dynamic>{}, },
children: [ children: [
XMLNode( XMLNode(
tag: 'value', tag: 'value',
text: value, text: value,
) ),
], ],
); );
} }
@ -45,19 +45,22 @@ class DataFormField {
return XMLNode( return XMLNode(
tag: 'field', tag: 'field',
attributes: <String, dynamic>{ attributes: <String, dynamic>{
...varAttr != null if (varAttr != null) 'var': varAttr,
? <String, dynamic>{'var': varAttr} if (type != null) 'type': type,
: <String, dynamic>{}, if (label != null) 'label': label,
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
...label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{}
}, },
children: [ children: [
...description != null ? [XMLNode(tag: 'desc', text: description)] : [], if (description != null)
...isRequired ? [XMLNode(tag: 'required')] : [], XMLNode(
tag: 'desc',
text: description,
),
if (isRequired)
XMLNode(
tag: 'required',
),
...values.map((value) => XMLNode(tag: 'value', text: value)), ...values.map((value) => XMLNode(tag: 'value', text: value)),
...options.map((option) => option.toXml()) ...options.map((option) => option.toXml()),
], ],
); );
} }

View File

@ -13,8 +13,10 @@ Stanza buildDiscoInfoQueryStanza(JID entity, String? node) {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoInfoXmlns, xmlns: discoInfoXmlns,
attributes: node != null ? {'node': node} : {}, attributes: {
) if (node != null) 'node': node,
},
),
], ],
); );
} }
@ -27,8 +29,10 @@ Stanza buildDiscoItemsQueryStanza(JID entity, {String? node}) {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: node != null ? {'node': node} : {}, attributes: {
) if (node != null) 'node': node,
},
),
], ],
); );
} }

View File

@ -19,13 +19,11 @@ class Identity {
XMLNode toXMLNode() { XMLNode toXMLNode() {
return XMLNode( return XMLNode(
tag: 'identity', tag: 'identity',
attributes: <String, dynamic>{ attributes: {
'category': category, 'category': category,
'type': type, 'type': type,
'name': name, 'name': name,
...lang == null if (lang != null) 'xml:lang': lang,
? <String, dynamic>{}
: <String, dynamic>{'xml:lang': lang}
}, },
); );
} }

View File

@ -33,11 +33,22 @@ class RoomInformation {
final String name; final String name;
} }
/// The used message-id and an optional origin-id.
typedef PendingMessage = (String, String?);
class RoomState { class RoomState {
RoomState({ RoomState({required this.roomJid, this.nick, required this.joined}) {
required this.roomJid, pendingMessages = List<PendingMessage>.empty(growable: true);
this.nick, }
});
/// The JID of the room.
final JID roomJid; final JID roomJid;
/// The nick we're joined with.
String? nick; String? nick;
/// Flag whether we're joined and can process messages
bool joined;
late final List<PendingMessage> pendingMessages;
} }

View File

@ -1,6 +1,9 @@
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
@ -9,8 +12,13 @@ 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_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0045/errors.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_0045/types.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:synchronized/extension.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// (Room JID, nickname)
typedef MUCRoomJoin = (JID, String);
class MUCManager extends XmppManagerBase { class MUCManager extends XmppManagerBase {
MUCManager() : super(mucManager); MUCManager() : super(mucManager);
@ -23,6 +31,78 @@ class MUCManager extends XmppManagerBase {
/// Cache lock /// Cache lock
final Lock _cacheLock = Lock(); final Lock _cacheLock = Lock();
/// Flag indicating whether we joined the rooms added to the room list with
/// [prepareRoomList].
bool _joinedPreparedRooms = true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
callback: _onMessage,
// Before the message handler
priority: -99,
),
];
@override
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
callback: _onMessageSent,
),
];
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is! StreamNegotiationsDoneEvent) {
return;
}
// Only attempt rejoining if we did not resume the stream and all
// prepared rooms are already joined.
if (event.resumed && _joinedPreparedRooms) {
return;
}
await _cacheLock.synchronized(() async {
// Mark all groupchats as not joined.
for (final jid in _mucRoomCache.keys) {
_mucRoomCache[jid]!.joined = false;
// Re-join all MUCs.
final state = _mucRoomCache[jid]!;
await _sendMucJoin(
jid,
state.nick!,
0,
);
}
_joinedPreparedRooms = true;
});
}
/// Prepares the internal room list to ensure that the rooms
/// [rooms] are joined once we are connected.
Future<void> prepareRoomList(List<MUCRoomJoin> rooms) async {
assert(
rooms.isNotEmpty,
'The room list should not be empty',
);
await _cacheLock.synchronized(() {
_joinedPreparedRooms = false;
for (final room in rooms) {
final (roomJid, nick) = room;
_mucRoomCache[roomJid] = RoomState(
roomJid: roomJid,
nick: nick,
joined: false,
);
}
});
}
/// Queries the information of a Multi-User Chat room. /// Queries the information of a Multi-User Chat room.
/// ///
/// Retrieves the information about the specified MUC room by performing a /// Retrieves the information about the specified MUC room by performing a
@ -55,11 +135,32 @@ class MUCManager extends XmppManagerBase {
/// if applicable. /// if applicable.
Future<Result<bool, MUCError>> joinRoom( Future<Result<bool, MUCError>> joinRoom(
JID roomJid, JID roomJid,
String nick, String nick, {
) async { int? maxHistoryStanzas,
}) async {
if (nick.isEmpty) { if (nick.isEmpty) {
return Result(NoNicknameSpecified()); return Result(NoNicknameSpecified());
} }
await _cacheLock.synchronized(
() {
_mucRoomCache[roomJid] = RoomState(
roomJid: roomJid,
nick: nick,
joined: false,
);
},
);
await _sendMucJoin(roomJid, nick, maxHistoryStanzas);
return const Result(true);
}
Future<void> _sendMucJoin(
JID roomJid,
String nick,
int? maxHistoryStanzas,
) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.presence( Stanza.presence(
@ -68,17 +169,20 @@ class MUCManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'x', tag: 'x',
xmlns: mucXmlns, xmlns: mucXmlns,
) children: [
if (maxHistoryStanzas != null)
XMLNode(
tag: 'history',
attributes: {
'maxstanzas': maxHistoryStanzas.toString(),
},
),
],
),
], ],
), ),
), ),
); );
await _cacheLock.synchronized(
() {
_mucRoomCache[roomJid] = RoomState(roomJid: roomJid, nick: nick);
},
);
return const Result(true);
} }
/// Leaves a Multi-User Chat room. /// Leaves a Multi-User Chat room.
@ -108,4 +212,86 @@ class MUCManager extends XmppManagerBase {
); );
return const Result(true); return const Result(true);
} }
Future<StanzaHandlerData> _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<StableIdData>()?.originId),
);
return state;
});
}
Future<StanzaHandlerData> _onMessage(
Stanza message,
StanzaHandlerData state,
) async {
final fromJid = JID.fromString(message.from!);
final roomJid = fromJid.toBare();
return _mucRoomCache.synchronized(() {
final roomState = _mucRoomCache[roomJid];
if (roomState == null) {
return state;
}
if (message.type == 'groupchat' && message.firstTag('subject') != null) {
// The room subject marks the end of the join flow.
if (!roomState.joined) {
// Mark the room as joined.
_mucRoomCache[roomJid]!.joined = true;
logger.finest('$roomJid is now joined');
}
// TODO(Unknown): Signal the subject?
return StanzaHandlerData(
true,
false,
message,
state.extensions,
);
} else {
if (!roomState.joined) {
// Ignore the discussion history.
return StanzaHandlerData(
true,
false,
message,
state.extensions,
);
}
// Check if this is the message reflection.
final pending =
(message.id!, state.extensions.get<StableIdData>()?.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,
message,
state.extensions,
);
}
}
return state;
});
}
} }

View File

@ -38,7 +38,7 @@ class VCardManager extends XmppManagerBase {
tagName: 'x', tagName: 'x',
tagXmlns: vCardTempUpdate, tagXmlns: vCardTempUpdate,
callback: _onPresence, callback: _onPresence,
) ),
]; ];
@override @override
@ -108,7 +108,7 @@ class VCardManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'vCard', tag: 'vCard',
xmlns: vCardTempXmlns, xmlns: vCardTempXmlns,
) ),
], ],
), ),
encrypted: true, encrypted: true,

View File

@ -38,26 +38,20 @@ class PubSubPublishOptions {
varAttr: 'FORM_TYPE', varAttr: 'FORM_TYPE',
type: 'hidden', type: 'hidden',
), ),
...accessModel != null if (accessModel != null)
? [ DataFormField(
DataFormField( options: [],
options: [], isRequired: false,
isRequired: false, values: [accessModel!],
values: [accessModel!], varAttr: 'pubsub#access_model',
varAttr: 'pubsub#access_model', ),
) if (maxItems != null)
] DataFormField(
: [], options: [],
...maxItems != null isRequired: false,
? [ values: [maxItems!],
DataFormField( varAttr: 'pubsub#max_items',
options: [], ),
isRequired: false,
values: [maxItems!],
varAttr: 'pubsub#max_items',
),
]
: [],
], ],
).toXml(); ).toXml();
} }
@ -87,7 +81,7 @@ class PubSubManager extends XmppManagerBase {
tagName: 'event', tagName: 'event',
tagXmlns: pubsubEventXmlns, tagXmlns: pubsubEventXmlns,
callback: _onPubsubMessage, callback: _onPubsubMessage,
) ),
]; ];
@override @override
@ -314,11 +308,11 @@ class PubSubManager extends XmppManagerBase {
children: [ children: [
XMLNode( XMLNode(
tag: 'item', tag: 'item',
attributes: id != null attributes: {
? <String, String>{'id': id} if (id != null) 'id': id,
: <String, String>{}, },
children: [payload], children: [payload],
) ),
], ],
), ),
if (pubOptions != null) if (pubOptions != null)
@ -327,7 +321,7 @@ class PubSubManager extends XmppManagerBase {
children: [pubOptions.toXml()], children: [pubOptions.toXml()],
), ),
], ],
) ),
], ],
), ),
shouldEncrypt: false, shouldEncrypt: false,
@ -422,7 +416,7 @@ class PubSubManager extends XmppManagerBase {
}, },
), ),
], ],
) ),
], ],
), ),
shouldEncrypt: false, shouldEncrypt: false,

View File

@ -45,7 +45,7 @@ class OOBManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message manager // Before the message manager
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -69,7 +69,7 @@ class ChatStateManager extends XmppManagerBase {
callback: _onChatStateReceived, callback: _onChatStateReceived,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -66,7 +66,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
callback: _onDeliveryRequestReceived, callback: _onDeliveryRequestReceived,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -27,7 +27,7 @@ class BlockingManager extends XmppManagerBase {
tagName: 'block', tagName: 'block',
tagXmlns: blockingXmlns, tagXmlns: blockingXmlns,
callback: _blockPush, callback: _blockPush,
) ),
]; ];
@override @override
@ -107,10 +107,12 @@ class BlockingManager extends XmppManagerBase {
children: items.map((item) { children: items.map((item) {
return XMLNode( return XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{'jid': item}, attributes: {
'jid': item,
},
); );
}).toList(), }).toList(),
) ),
], ],
), ),
), ),
@ -128,7 +130,7 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'unblock', tag: 'unblock',
xmlns: blockingXmlns, xmlns: blockingXmlns,
) ),
], ],
), ),
), ),
@ -152,11 +154,13 @@ class BlockingManager extends XmppManagerBase {
.map( .map(
(item) => XMLNode( (item) => XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{'jid': item}, attributes: {
'jid': item,
},
), ),
) )
.toList(), .toList(),
) ),
], ],
), ),
), ),
@ -174,7 +178,7 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'blocklist', tag: 'blocklist',
xmlns: blockingXmlns, xmlns: blockingXmlns,
) ),
], ],
), ),
), ),

View File

@ -5,7 +5,10 @@ class StreamManagementEnableNonza extends XMLNode {
StreamManagementEnableNonza() StreamManagementEnableNonza()
: super( : super(
tag: 'enable', tag: 'enable',
attributes: <String, String>{'xmlns': smXmlns, 'resume': 'true'}, attributes: {
'xmlns': smXmlns,
'resume': 'true',
},
); );
} }
@ -13,10 +16,10 @@ class StreamManagementResumeNonza extends XMLNode {
StreamManagementResumeNonza(String id, int h) StreamManagementResumeNonza(String id, int h)
: super( : super(
tag: 'resume', tag: 'resume',
attributes: <String, String>{ attributes: {
'xmlns': smXmlns, 'xmlns': smXmlns,
'previd': id, 'previd': id,
'h': h.toString() 'h': h.toString(),
}, },
); );
} }
@ -25,7 +28,10 @@ class StreamManagementAckNonza extends XMLNode {
StreamManagementAckNonza(int h) StreamManagementAckNonza(int h)
: super( : super(
tag: 'a', tag: 'a',
attributes: <String, String>{'xmlns': smXmlns, 'h': h.toString()}, attributes: {
'xmlns': smXmlns,
'h': h.toString(),
},
); );
} }
@ -33,7 +39,7 @@ class StreamManagementRequestNonza extends XMLNode {
StreamManagementRequestNonza() StreamManagementRequestNonza()
: super( : super(
tag: 'r', tag: 'r',
attributes: <String, String>{ attributes: {
'xmlns': smXmlns, 'xmlns': smXmlns,
}, },
); );

View File

@ -140,7 +140,7 @@ class StreamManagementManager extends XmppManagerBase {
nonzaTag: 'a', nonzaTag: 'a',
nonzaXmlns: smXmlns, nonzaXmlns: smXmlns,
callback: _handleAckResponse, callback: _handleAckResponse,
) ),
]; ];
@override @override
@ -148,14 +148,14 @@ class StreamManagementManager extends XmppManagerBase {
StanzaHandler( StanzaHandler(
callback: _onServerStanzaReceived, callback: _onServerStanzaReceived,
priority: 9999, priority: 9999,
) ),
]; ];
@override @override
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [ List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
StanzaHandler( StanzaHandler(
callback: _onClientStanzaSent, callback: _onClientStanzaSent,
) ),
]; ];
@override @override

View File

@ -49,7 +49,7 @@ class CarbonsManager extends XmppManagerBase {
tagXmlns: carbonsXmlns, tagXmlns: carbonsXmlns,
callback: _onMessageSent, callback: _onMessageSent,
priority: -98, priority: -98,
) ),
]; ];
@override @override
@ -124,7 +124,7 @@ class CarbonsManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'enable', tag: 'enable',
xmlns: carbonsXmlns, xmlns: carbonsXmlns,
) ),
], ],
), ),
), ),
@ -154,7 +154,7 @@ class CarbonsManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'disable', tag: 'disable',
xmlns: carbonsXmlns, xmlns: carbonsXmlns,
) ),
], ],
), ),
), ),

View File

@ -40,7 +40,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -100,7 +100,7 @@ class ChatMarkerManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -81,7 +81,7 @@ class StableIdManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the MessageManager // Before the MessageManager
priority: -99, priority: -99,
) ),
]; ];
@override @override
@ -127,7 +127,13 @@ class StableIdManager extends XmppManagerBase {
TypedMap<StanzaHandlerExtension> extensions, TypedMap<StanzaHandlerExtension> extensions,
) { ) {
final data = extensions.get<StableIdData>(); final data = extensions.get<StableIdData>();
return data != null ? data.toXML() : []; if (data?.originId != null) {
return [
data!.toOriginIdElement(),
];
}
return [];
} }
@override @override

View File

@ -161,9 +161,9 @@ class HttpFileUploadManager extends XmppManagerBase {
attributes: { attributes: {
'filename': filename, 'filename': filename,
'size': filesize.toString(), 'size': filesize.toString(),
...contentType != null ? {'content-type': contentType} : {} if (contentType != null) 'content-type': contentType,
}, },
) ),
], ],
), ),
), ),

View File

@ -2,10 +2,10 @@ abstract class OmemoError {}
class UnknownOmemoError extends OmemoError {} class UnknownOmemoError extends OmemoError {}
class InvalidAffixElementsException with Exception {} class InvalidAffixElementsException implements Exception {}
class OmemoNotSupportedForContactException extends OmemoError {} class OmemoNotSupportedForContactException extends OmemoError {}
class EncryptionFailedException with Exception {} class EncryptionFailedException implements Exception {}
class InvalidEnvelopePayloadException with Exception {} class InvalidEnvelopePayloadException implements Exception {}

View File

@ -82,7 +82,7 @@ class SIMSManager extends XmppManagerBase {
tagXmlns: referenceXmlns, tagXmlns: referenceXmlns,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -31,7 +31,7 @@ class MessageRetractionManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the MessageManager // Before the MessageManager
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -133,7 +133,7 @@ class SFSManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -98, priority: -98,
) ),
]; ];
@override @override

View File

@ -96,7 +96,7 @@ class MessageRepliesManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override

View File

@ -5,7 +5,7 @@ homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
environment: environment:
sdk: '>=2.17.5 <3.0.0' sdk: '>=3.0.0 <4.0.0'
dependencies: dependencies:
collection: ^1.16.0 collection: ^1.16.0

View File

@ -14,9 +14,9 @@ final scramSha1StreamFeatures = XMLNode(
XMLNode( XMLNode(
tag: 'mechanism', tag: 'mechanism',
text: 'SCRAM-SHA-1', text: 'SCRAM-SHA-1',
) ),
], ],
) ),
], ],
); );
final scramSha256StreamFeatures = XMLNode( final scramSha256StreamFeatures = XMLNode(
@ -29,9 +29,9 @@ final scramSha256StreamFeatures = XMLNode(
XMLNode( XMLNode(
tag: 'mechanism', tag: 'mechanism',
text: 'SCRAM-SHA-256', text: 'SCRAM-SHA-256',
) ),
], ],
) ),
], ],
); );

View File

@ -171,7 +171,7 @@ void main() {
), ),
tagName: '3', tagName: '3',
priority: 50, priority: 50,
) ),
]..sort(stanzaHandlerSortComparator); ]..sort(stanzaHandlerSortComparator);
expect(handlerList[0].tagName, '1'); expect(handlerList[0].tagName, '1');

View File

@ -158,7 +158,7 @@ void main() {
</iq>''', </iq>''',
ignoreId: true, ignoreId: true,
adjustId: true, adjustId: true,
) ),
], ],
); );

View File

@ -69,7 +69,7 @@ class StubbedDiscoManager extends DiscoManager {
isRequired: false, isRequired: false,
varAttr: 'FORM_TYPE', varAttr: 'FORM_TYPE',
type: 'hidden', type: 'hidden',
) ),
], ],
reported: [], reported: [],
items: [], items: [],
@ -153,14 +153,14 @@ void main() {
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc',
], ],
const [ const [
Identity( Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Exodus 0.9.1', name: 'Exodus 0.9.1',
) ),
], ],
const [], const [],
null, null,
@ -179,7 +179,7 @@ void main() {
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc',
], ],
const [ const [
Identity( Identity(
@ -295,14 +295,14 @@ void main() {
'urn:xmpp:message-correct:0', 'urn:xmpp:message-correct:0',
'urn:xmpp:ping', 'urn:xmpp:ping',
'urn:xmpp:receipts', 'urn:xmpp:receipts',
'urn:xmpp:time' 'urn:xmpp:time',
], ],
const [ const [
Identity( Identity(
category: 'client', category: 'client',
type: 'phone', type: 'phone',
name: 'Conversations', name: 'Conversations',
) ),
], ],
const [], const [],
null, null,

View File

@ -433,10 +433,11 @@ void main() {
}); });
test('Test a failed stream resumption', () async { test('Test a failed stream resumption', () async {
final fakeSocket = StubTCPSocket([ final fakeSocket = StubTCPSocket(
StringExpectation( [
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>", StringExpectation(
''' "<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream <stream:stream
xmlns="jabber:client" xmlns="jabber:client"
version="1.0" version="1.0"
@ -448,14 +449,14 @@ void main() {
<mechanism>PLAIN</mechanism> <mechanism>PLAIN</mechanism>
</mechanisms> </mechanisms>
</stream:features>''', </stream:features>''',
), ),
StringExpectation( StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>", "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />', '<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
), ),
StringExpectation( StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>", "<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
''' '''
<stream:stream <stream:stream
xmlns="jabber:client" xmlns="jabber:client"
version="1.0" version="1.0"
@ -473,21 +474,22 @@ void main() {
<sm xmlns="urn:xmpp:sm:3"/> <sm xmlns="urn:xmpp:sm:3"/>
</stream:features> </stream:features>
''', ''',
), ),
StringExpectation( StringExpectation(
"<resume xmlns='urn:xmpp:sm:3' previd='id-1' h='10' />", "<resume xmlns='urn:xmpp:sm:3' previd='id-1' h='10' />",
"<failed xmlns='urn:xmpp:sm:3' h='another-sequence-number'><item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></failed>", "<failed xmlns='urn:xmpp:sm:3' h='another-sequence-number'><item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></failed>",
), ),
StanzaExpectation( StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>', '<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>', '<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true, ignoreId: true,
), ),
StringExpectation( StringExpectation(
"<enable xmlns='urn:xmpp:sm:3' resume='true' />", "<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="id-2" resume="true" />', '<enabled xmlns="urn:xmpp:sm:3" id="id-2" resume="true" />',
) ),
]); ],
);
final conn = XmppConnection( final conn = XmppConnection(
TestingReconnectionPolicy(), TestingReconnectionPolicy(),

View File

@ -115,17 +115,19 @@ void main() {
test('Test sending a message processing hint', () async { test('Test sending a message processing hint', () async {
final manager = MessageManager(); final manager = MessageManager();
final holder = TestingManagerHolder( final holder = TestingManagerHolder(
stubSocket: StubTCPSocket([ stubSocket: StubTCPSocket(
StanzaExpectation( [
''' StanzaExpectation(
'''
<message to="user@example.org" type="chat"> <message to="user@example.org" type="chat">
<no-copy xmlns="urn:xmpp:hints"/> <no-copy xmlns="urn:xmpp:hints"/>
<no-store xmlns="urn:xmpp:hints"/> <no-store xmlns="urn:xmpp:hints"/>
</message> </message>
''', ''',
'', '',
) ),
]), ],
),
); );
await holder.register([ await holder.register([

View File

@ -6,7 +6,7 @@ void main() {
test('invariance', () { test('invariance', () {
final headers = { final headers = {
'authorization': 'Basic Base64String==', 'authorization': 'Basic Base64String==',
'cookie': 'foo=bar; user=romeo' 'cookie': 'foo=bar; user=romeo',
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
@ -16,7 +16,7 @@ void main() {
test('invariance through uppercase', () { test('invariance through uppercase', () {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo' 'Cookie': 'foo=bar; user=romeo',
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
@ -27,7 +27,7 @@ void main() {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
'X-Tracking': 'Base64String==' 'X-Tracking': 'Base64String==',
}; };
expect(prepareHeaders(headers), { expect(prepareHeaders(headers), {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',