32 Commits

Author SHA1 Message Date
7ce6703c5b tests: Fix the XEP-0198 test 2023-01-09 12:53:22 +01:00
37261cddbb fix: Run Stream Management very early 2023-01-09 12:48:30 +01:00
d8c2ef6f3b Merge pull request 'Roster Rework' (#15) from fix/roster-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/15
2023-01-07 21:23:12 +00:00
98e5324409 fix: Copy the pre handler's encryption state 2023-01-07 22:18:20 +01:00
a69c2a23f2 fix: Bump omemo_dart version 2023-01-07 22:18:05 +01:00
d8de093e4d fix: Somewhat fix OMEMO 2023-01-07 21:57:56 +01:00
678564dbb3 fix: Roster request being treated as a roster push 2023-01-07 21:23:45 +01:00
09d2601e85 fix: Compare groups 2023-01-07 19:58:46 +01:00
41560682a1 fix: Handle roster items staying the same 2023-01-07 19:03:35 +01:00
473f8e4bb6 tests: Fix tests 2023-01-07 18:40:36 +01:00
67446285c1 feat: Refactor RosterPushEvent to RosterPushResult 2023-01-07 18:36:18 +01:00
e12f4688d3 feat: Trigger an event when the roster changes 2023-01-07 18:32:15 +01:00
2581bbe203 feat: Document the RosterStateManager better 2023-01-07 18:11:41 +01:00
995f2e0248 feat: Integrate the BaseRosterStateManager with the RosterManager 2023-01-07 16:26:15 +01:00
e2c8f79429 feat: Add a management class for roster state 2023-01-07 15:51:10 +01:00
763c93857d feat: Simplify the JID parser
Fixes #13.
2023-01-04 17:19:37 +01:00
55d2ef9c25 style: Remove newline 2023-01-02 17:53:06 +01:00
f37cbd1616 feat: Allow specifying XEP-0449's access model 2023-01-02 17:52:55 +01:00
2a3449d0f2 fix: Fix user avatar update being triggered for every PubSub event 2023-01-02 17:35:47 +01:00
596693c206 feat: Update to omemo_dart 0.4.1 2023-01-02 13:58:27 +01:00
22aa07c4ba feat: Propagate errors and encrypt to self if carbons are enabled 2023-01-02 13:52:06 +01:00
62001c1e29 feat: Upgrade omemo_dart to 0.4.0 2023-01-01 18:17:41 +01:00
ca85c94fe5 fix: Fix wrong XML serialisation 2023-01-01 16:38:54 +01:00
637e1e25a6 feat: Migrate to the new omemo_dart API 2023-01-01 16:19:25 +01:00
09696c1c4d fix: Fix VCard and User Avatar queries being encrypted 2022-12-25 13:05:16 +01:00
298a8342b8 docs: Add funding.yml 2022-12-23 15:18:53 +01:00
d64220426b feat: Implement XEP-0449 2022-12-19 14:14:05 +01:00
88efdc361c fix: Only add a <body> element when specified 2022-12-09 12:52:00 +01:00
cc1b371198 feat: Allow clients to read Message Processing Hints 2022-12-09 12:46:17 +01:00
d9e4a3c1d4 feat: Implement XEP-0444 2022-12-06 14:09:07 +01:00
0ae13acca0 chore(release): publish packages
- moxxmpp@0.1.6+1
 - moxxmpp_socket_tcp@0.1.2+9
2022-11-26 15:48:48 +01:00
d383fa31ae fix: Fix LMC not working 2022-11-26 15:48:29 +01:00
47 changed files with 1468 additions and 1073 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
ko_fi: papatutuwawa

View File

@@ -25,3 +25,9 @@ the development shell provided by the NixOS Flake, ensure that `ANDROID_HOME` an
## License
See `./LICENSE`.
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -16,10 +16,10 @@ dependencies:
version: 0.1.4+1
moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.6
version: 0.1.6+1
moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.2+8
version: 0.1.2+9
dev_dependencies:
flutter_test:

View File

@@ -1,3 +1,7 @@
## 0.1.6+1
- **FIX**: Fix LMC not working.
## 0.1.6
- **FEAT**: Implement XEP-0308.

View File

@@ -5,3 +5,9 @@ A pure-Dart XMPP library written for Moxxy.
## License
See `./LICENSE`.
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -29,6 +29,7 @@ export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
export 'package:moxxmpp/src/roster/errors.dart';
export 'package:moxxmpp/src/roster/roster.dart';
export 'package:moxxmpp/src/roster/state.dart';
export 'package:moxxmpp/src/settings.dart';
export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart';
@@ -59,6 +60,7 @@ export 'package:moxxmpp/src/xeps/xep_0203.dart';
export 'package:moxxmpp/src/xeps/xep_0280.dart';
export 'package:moxxmpp/src/xeps/xep_0297.dart';
export 'package:moxxmpp/src/xeps/xep_0300.dart';
export 'package:moxxmpp/src/xeps/xep_0308.dart';
export 'package:moxxmpp/src/xeps/xep_0333.dart';
export 'package:moxxmpp/src/xeps/xep_0334.dart';
export 'package:moxxmpp/src/xeps/xep_0352.dart';
@@ -74,7 +76,9 @@ export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
export 'package:moxxmpp/src/xeps/xep_0385.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart';
export 'package:moxxmpp/src/xeps/xep_0446.dart';
export 'package:moxxmpp/src/xeps/xep_0447.dart';
export 'package:moxxmpp/src/xeps/xep_0448.dart';
export 'package:moxxmpp/src/xeps/xep_0449.dart';
export 'package:moxxmpp/src/xeps/xep_0461.dart';

View File

@@ -94,6 +94,7 @@ class XmppConnection {
_awaitingResponseLock = Lock(),
_xmppManagers = {},
_incomingStanzaHandlers = List.empty(growable: true),
_incomingPreStanzaHandlers = List.empty(growable: true),
_outgoingPreStanzaHandlers = List.empty(growable: true),
_outgoingPostStanzaHandlers = List.empty(growable: true),
_reconnectionPolicy = reconnectionPolicy,
@@ -133,6 +134,7 @@ class XmppConnection {
/// Helpers
///
final List<StanzaHandler> _incomingStanzaHandlers;
final List<StanzaHandler> _incomingPreStanzaHandlers;
final List<StanzaHandler> _outgoingPreStanzaHandlers;
final List<StanzaHandler> _outgoingPostStanzaHandlers;
final StreamController<XmppEvent> _eventStreamController;
@@ -223,11 +225,13 @@ class XmppConnection {
}
_incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers());
_incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers());
_outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers());
_outgoingPostStanzaHandlers.addAll(manager.getOutgoingPostStanzaHandlers());
if (sortHandlers) {
_incomingStanzaHandlers.sort(stanzaHandlerSortComparator);
_incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator);
}
@@ -630,8 +634,12 @@ class XmppConnection {
return state;
}
Future<StanzaHandlerData> _runIncomingStanzaHandlers(Stanza stanza) async {
return _runStanzaHandlers(_incomingStanzaHandlers, stanza);
Future<StanzaHandlerData> _runIncomingStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async {
return _runStanzaHandlers(_incomingStanzaHandlers, stanza, initial: initial);
}
Future<StanzaHandlerData> _runIncomingPreStanzaHandlers(Stanza stanza) async {
return _runStanzaHandlers(_incomingPreStanzaHandlers, stanza);
}
Future<StanzaHandlerData> _runOutgoingPreStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async {
@@ -673,18 +681,18 @@ class XmppConnection {
// Run the incoming stanza handlers and bounce with an error if no manager handled
// it.
final incomingHandlers = await _runIncomingStanzaHandlers(stanza);
final prefix = incomingHandlers.encrypted ?
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted && incomingPreHandlers.other['encryption_error'] == null ?
'(Encrypted) ' :
'';
_log.finest('<== $prefix${incomingHandlers.stanza.toXml()}');
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
// See if we are waiting for this stanza
final id = stanza.attributes['id'] as String?;
final id = incomingPreHandlers.stanza.attributes['id'] as String?;
var awaited = false;
await _awaitingResponseLock.synchronized(() async {
if (id != null && _awaitingResponse.containsKey(id)) {
_awaitingResponse[id]!.complete(incomingHandlers.stanza);
_awaitingResponse[id]!.complete(incomingPreHandlers.stanza);
_awaitingResponse.remove(id);
awaited = true;
}
@@ -695,8 +703,19 @@ class XmppConnection {
}
// Only bounce if the stanza has neither been awaited, nor handled.
final incomingHandlers = await _runIncomingStanzaHandlers(
incomingPreHandlers.stanza,
initial: StanzaHandlerData(
false,
incomingPreHandlers.cancel,
incomingPreHandlers.cancelReason,
incomingPreHandlers.stanza,
encrypted: incomingPreHandlers.encrypted,
other: incomingPreHandlers.other,
),
);
if (!incomingHandlers.done) {
handleUnhandledStanza(this, stanza);
handleUnhandledStanza(this, incomingPreHandlers.stanza);
}
}

View File

@@ -1,14 +1,17 @@
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.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_0461.dart';
@@ -51,6 +54,22 @@ class StreamResumedEvent extends XmppEvent {
/// Triggered when stream resumption failed
class StreamResumeFailedEvent extends XmppEvent {}
/// Triggered when the roster has been modified
class RosterUpdatedEvent extends XmppEvent {
RosterUpdatedEvent(this.removed, this.modified, this.added);
/// A list of bare JIDs that are removed from the roster
final List<String> removed;
/// A list of XmppRosterItems that are modified. Can be correlated with one's cache
/// using the jid attribute.
final List<XmppRosterItem> modified;
/// A list of XmppRosterItems that are added to the roster.
final List<XmppRosterItem> added;
}
/// Triggered when a message is received
class MessageEvent extends XmppEvent {
MessageEvent({
required this.body,
@@ -75,6 +94,9 @@ class MessageEvent extends XmppEvent {
this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
});
final StanzaError? error;
final String body;
@@ -97,6 +119,9 @@ class MessageEvent extends XmppEvent {
final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId;
final MessageReactions? messageReactions;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId;
final Map<String, dynamic> other;
}

View File

@@ -6,67 +6,38 @@ class JID {
const JID(this.local, this.domain, this.resource);
factory JID.fromString(String jid) {
// 0: Parsing either the local or domain part
// 1: Parsing the domain part
// 2: Parsing the resource
var state = 0;
var buffer = '';
var local_ = '';
var domain_ = '';
var resource_ = '';
for (var i = 0; i < jid.length; i++) {
final c = jid[i];
final eol = i == jid.length - 1;
switch (state) {
case 0: {
if (c == '@') {
local_ = buffer;
buffer = '';
state = 1;
} else if (c == '/') {
domain_ = buffer;
buffer = '';
state = 2;
} else if (eol) {
domain_ = buffer + c;
} else {
buffer += c;
}
}
break;
case 1: {
if (c == '/') {
domain_ = buffer;
buffer = '';
state = 2;
} else if (eol) {
domain_ = buffer;
// Algorithm taken from here: https://blog.samwhited.com/2021/02/xmpp-addresses/
var localPart = '';
var domainPart = '';
var resourcePart = '';
if (c != ' ') {
domain_ = domain_ + c;
}
} else if (c != ' ') {
buffer += c;
}
}
break;
case 2: {
if (eol) {
resource_ = buffer;
final slashParts = jid.split('/');
if (slashParts.length == 1) {
resourcePart = '';
} else {
resourcePart = slashParts.sublist(1).join('/');
if (c != ' ') {
resource_ = resource_ + c;
}
} else if (c != ''){
buffer += c;
}
}
}
assert(resourcePart.isNotEmpty, 'Resource part cannot be there and empty');
}
return JID(local_, domain_, resource_);
final atParts = slashParts.first.split('@');
if (atParts.length == 1) {
localPart = '';
domainPart = atParts.first;
} else {
localPart = atParts.first;
domainPart = atParts.sublist(1).join('@');
assert(localPart.isNotEmpty, 'Local part cannot be there and empty');
}
return JID(
localPart,
domainPart.endsWith('.') ?
domainPart.substring(0, domainPart.length - 1) :
domainPart,
resourcePart,
);
}
final String local;
final String domain;

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
@@ -11,7 +10,6 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
class XmppManagerAttributes {
XmppManagerAttributes({
required this.sendStanza,
required this.sendNonza,

View File

@@ -21,18 +21,28 @@ abstract class XmppManagerBase {
}
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// send. These are run before the stanza is sent.
/// send. These are run before the stanza is sent. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [];
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// send. These are run after the stanza is sent.
/// send. These are run after the stanza is sent. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [];
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// receive.
/// receive. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getIncomingStanzaHandlers() => [];
/// Return the StanzaHandlers associated with this manager that deal with stanza handlers
/// that have to run before the main ones run. This is useful, for example, for OMEMO
/// as we have to decrypt the stanza before we do anything else. The higher the value
/// of the handler's priority, the earlier it is run.
List<StanzaHandler> getIncomingPreStanzaHandlers() => [];
/// Return the NonzaHandlers associated with this manager.
/// Return the NonzaHandlers associated with this manager. The higher the value of the
/// handler's priority, the earlier it is run.
List<NonzaHandler> getNonzaHandlers() => [];
/// Return a list of features that should be included in a disco response.

View File

@@ -7,6 +7,7 @@ import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.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_0461.dart';
@@ -61,6 +62,10 @@ class StanzaHandlerData with _$StanzaHandlerData {
MessageRetractionData? messageRetraction,
// If non-null, then the message is a correction for the specified stanza Id
String? lastMessageCorrectionSid,
// Reactions data
MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to
String? stickerPackId,
}
) = _StanzaHandlerData;
}

View File

@@ -59,7 +59,11 @@ mixin _$StanzaHandlerData {
// retracted
MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid => throw _privateConstructorUsedError;
String? get lastMessageCorrectionSid =>
throw _privateConstructorUsedError; // Reactions data
MessageReactions? get messageReactions =>
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
String? get stickerPackId => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@@ -94,7 +98,9 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid});
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
}
/// @nodoc
@@ -131,6 +137,8 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_value.copyWith(
done: done == freezed
@@ -225,6 +233,14 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
@@ -259,7 +275,9 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid});
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
}
/// @nodoc
@@ -298,6 +316,8 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_$_StanzaHandlerData(
done == freezed
@@ -392,6 +412,14 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
@@ -418,7 +446,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction,
this.lastMessageCorrectionSid})
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId})
: _other = other;
// Indicates to the runner that processing is now done. This means that all
@@ -501,10 +531,16 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
// If non-null, then the message is a correction for the specified stanza Id
@override
final String? lastMessageCorrectionSid;
// Reactions data
@override
final MessageReactions? messageReactions;
// The Id of the sticker pack this sticker belongs to
@override
final String? stickerPackId;
@override
String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid)';
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
}
@override
@@ -544,7 +580,11 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid));
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions) &&
const DeepCollectionEquality()
.equals(other.stickerPackId, stickerPackId));
}
@override
@@ -572,7 +612,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid)
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]);
@JsonKey(ignore: true)
@@ -603,7 +645,9 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid}) = _$_StanzaHandlerData;
final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions,
final String? stickerPackId}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
@@ -658,6 +702,10 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
MessageRetractionData? get messageRetraction;
@override // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid;
@override // Reactions data
MessageReactions? get messageReactions;
@override // The Id of the sticker pack this sticker belongs to
String? get stickerPackId;
@override
@JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>

View File

@@ -5,7 +5,6 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
abstract class Handler {
const Handler(this.matchStanzas, { this.nonzaTag, this.nonzaXmlns });
final String? nonzaTag;
final String? nonzaXmlns;
@@ -32,7 +31,6 @@ abstract class Handler {
}
class NonzaHandler extends Handler {
NonzaHandler({
required this.callback,
String? nonzaTag,
@@ -46,7 +44,6 @@ class NonzaHandler extends Handler {
}
class StanzaHandler extends Handler {
StanzaHandler({
required this.callback,
this.tagXmlns,

View File

@@ -26,3 +26,5 @@ const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager';
const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager';
const messageRetractionManager = 'org.moxxmpp.messageretractionmanager';
const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager';

View File

@@ -13,12 +13,19 @@ 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';
/// 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,
@@ -38,6 +45,10 @@ class MessageDetails {
this.shouldEncrypt = false,
this.messageRetraction,
this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
});
final String to;
final String? body;
@@ -56,6 +67,10 @@ class MessageDetails {
final bool shouldEncrypt;
final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
}
class MessageManager extends XmppManagerBase {
@@ -81,6 +96,11 @@ class MessageManager extends XmppManagerBase {
final message = state.stanza;
final body = message.firstTag('body');
final hints = List<MessageProcessingHint>.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),
@@ -102,6 +122,11 @@ class MessageManager extends XmppManagerBase {
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),
),);
@@ -159,7 +184,7 @@ class MessageManager extends XmppManagerBase {
);
} else {
var body = details.body;
if (details.sfs != null) {
if (details.sfs != null && details.setOOBFallbackBody) {
// TODO(Unknown): Maybe find a better solution
final firstSource = details.sfs!.sources.first;
if (firstSource is StatelessFileSharingUrlSource) {
@@ -171,9 +196,11 @@ class MessageManager extends XmppManagerBase {
body = details.messageRetraction!.fallback;
}
stanza.addChild(
XMLNode(tag: 'body', text: body),
);
if (body != null) {
stanza.addChild(
XMLNode(tag: 'body', text: body),
);
}
}
if (details.requestDeliveryReceipt) {
@@ -190,7 +217,7 @@ class MessageManager extends XmppManagerBase {
stanza.addChild(details.sfs!.toXML());
final source = details.sfs!.sources.first;
if (source is StatelessFileSharingUrlSource) {
if (source is StatelessFileSharingUrlSource && details.setOOBFallbackBody) {
// SFS recommends OOB as a fallback
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
}
@@ -261,6 +288,28 @@ class MessageManager extends XmppManagerBase {
),
);
}
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(stanza, awaitable: false);
}

View File

@@ -123,9 +123,12 @@ const fasteningXmlns = 'urn:xmpp:fasten:0';
// XEP-0424
const messageRetractionXmlns = 'urn:xmpp:message-retract:0';
// XEp-0428
// XEP-0428
const fallbackIndicationXmlns = 'urn:xmpp:fallback:0';
// XEP-0444
const messageReactionsXmlns = 'urn:xmpp:reactions:0';
// XEP-0446
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';
@@ -138,6 +141,9 @@ const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopad
const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0';
const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0';
// XEP-0449
const stickersXmlns = 'urn:xmpp:stickers:0';
// XEP-0461
const replyXmlns = 'urn:xmpp:reply:0';
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';

View File

@@ -24,3 +24,28 @@ int ioctetSortComparator(String a, String b) {
return 1;
}
int ioctetSortComparatorRaw(List<int> a, List<int> b) {
if (a.isEmpty && b.isEmpty) {
return 0;
}
if (a.isEmpty && b.isNotEmpty) {
return -1;
}
if (a.isNotEmpty && b.isEmpty) {
return 1;
}
if (a[0] == b[0]) {
return ioctetSortComparatorRaw(a.sublist(1), b.sublist(1));
}
// TODO(Unknown): Is this correct?
if (a[0] < b[0]) {
return -1;
}
return 1;
}

View File

@@ -1,5 +1,8 @@
import 'package:moxxmpp/src/events.dart';
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
@@ -8,17 +11,42 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/roster/errors.dart';
import 'package:moxxmpp/src/roster/state.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
@immutable
class XmppRosterItem {
XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] });
const XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] });
final String jid;
final String? name;
final String subscription;
final String? ask;
final List<String> groups;
@override
bool operator==(Object other) {
return other is XmppRosterItem &&
other.jid == jid &&
other.name == name &&
other.subscription == subscription &&
other.ask == ask &&
const ListEquality<String>().equals(other.groups, groups);
}
@override
int get hashCode => jid.hashCode ^ name.hashCode ^ subscription.hashCode ^ ask.hashCode ^ groups.hashCode;
@override
String toString() {
return 'XmppRosterItem('
'jid: $jid, '
'name: $name, '
'subscription: $subscription, '
'ask: $ask, '
'groups: $groups)';
}
}
enum RosterRemovalResult {
@@ -28,13 +56,13 @@ enum RosterRemovalResult {
}
class RosterRequestResult {
RosterRequestResult({ required this.items, this.ver });
RosterRequestResult(this.items, this.ver);
List<XmppRosterItem> items;
String? ver;
}
class RosterPushEvent extends XmppEvent {
RosterPushEvent({ required this.item, this.ver });
class RosterPushResult {
RosterPushResult(this.item, this.ver);
final XmppRosterItem item;
final String? ver;
}
@@ -65,9 +93,16 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
/// This manager requires a RosterFeatureNegotiator to be registered.
class RosterManager extends XmppManagerBase {
RosterManager(this._stateManager) : super();
RosterManager() : _rosterVersion = null, super();
String? _rosterVersion;
/// The class managing the entire roster state.
final BaseRosterStateManager _stateManager;
@override
void register(XmppManagerAttributes attributes) {
super.register(attributes);
_stateManager.register(attributes.sendEvent);
}
@override
String getId() => rosterManager;
@@ -87,17 +122,7 @@ class RosterManager extends XmppManagerBase {
@override
Future<bool> isSupported() async => true;
/// Override-able functions
Future<void> commitLastRosterVersion(String version) async {}
Future<void> loadLastRosterVersion() async {}
void setRosterVersion(String ver) {
assert(_rosterVersion == null, 'A roster version must not be empty');
_rosterVersion = ver;
}
Future<StanzaHandlerData> _onRosterPush(Stanza stanza, StanzaHandlerData state) async {
final attrs = getAttributes();
final from = stanza.attributes['from'] as String?;
@@ -114,6 +139,7 @@ class RosterManager extends XmppManagerBase {
}
final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
logger.fine('Roster push: ${query.toXml()}');
final item = query.firstTag('item');
if (item == null) {
@@ -121,21 +147,20 @@ class RosterManager extends XmppManagerBase {
return state.copyWith(done: true);
}
if (query.attributes['ver'] != null) {
final ver = query.attributes['ver']! as String;
await commitLastRosterVersion(ver);
_rosterVersion = ver;
}
attrs.sendEvent(RosterPushEvent(
item: XmppRosterItem(
jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?,
name: item.attributes['name'] as String?,
unawaited(
_stateManager.handleRosterPush(
RosterPushResult(
XmppRosterItem(
jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?,
name: item.attributes['name'] as String?,
),
query.attributes['ver'] as String?,
),
),
ver: query.attributes['ver'] as String?,
),);
);
await attrs.sendStanza(stanza.reply());
return state.copyWith(done: true);
@@ -145,71 +170,69 @@ class RosterManager extends XmppManagerBase {
/// the server deems a regular roster response more efficient than n roster pushes.
Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(XMLNode? query) async {
final List<XmppRosterItem> items;
String? rosterVersion;
if (query != null) {
items = query.children.map((item) => XmppRosterItem(
name: item.attributes['name'] as String?,
jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?,
groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(),
),).toList();
items = query.children.map(
(item) => XmppRosterItem(
name: item.attributes['name'] as String?,
jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?,
groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(),
),
).toList();
if (query.attributes['ver'] != null) {
final ver_ = query.attributes['ver']! as String;
await commitLastRosterVersion(ver_);
_rosterVersion = ver_;
}
rosterVersion = query.attributes['ver'] as String?;
} else {
logger.warning('Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121');
return Result(NoQueryError());
}
final ver = query.attributes['ver'] as String?;
if (ver != null) {
_rosterVersion = ver;
await commitLastRosterVersion(ver);
}
return Result(
RosterRequestResult(
items: items,
ver: ver,
),
final result = RosterRequestResult(
items,
rosterVersion,
);
unawaited(
_stateManager.handleRosterFetch(result),
);
return Result(result);
}
/// Requests the roster following RFC 6121 without using roster versioning.
/// Requests the roster following RFC 6121.
Future<Result<RosterRequestResult, RosterError>> requestRoster() async {
final attrs = getAttributes();
final query = XMLNode.xmlns(
tag: 'query',
xmlns: rosterXmlns,
);
final rosterVersion = await _stateManager.getRosterVersion();
if (rosterVersion != null && rosterVersioningAvailable()) {
query.attributes['ver'] = rosterVersion;
}
final response = await attrs.sendStanza(
Stanza.iq(
type: 'get',
children: [
XMLNode.xmlns(
tag: 'query',
xmlns: rosterXmlns,
)
query,
],
),
);
if (response.attributes['type'] != 'result') {
logger.warning('Error requesting roster without roster versioning: ${response.toXml()}');
logger.warning('Error requesting roster: ${response.toXml()}');
return Result(UnknownError());
}
final query = response.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(query);
final responseQuery = response.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(responseQuery);
}
/// Requests a series of roster pushes according to RFC6121. Requires that the server
/// advertises urn:xmpp:features:rosterver in the stream features.
Future<Result<RosterRequestResult?, RosterError>> requestRosterPushes() async {
if (_rosterVersion == null) {
await loadLastRosterVersion();
}
final attrs = getAttributes();
final result = await attrs.sendStanza(
Stanza.iq(
@@ -219,7 +242,7 @@ class RosterManager extends XmppManagerBase {
tag: 'query',
xmlns: rosterXmlns,
attributes: {
'ver': _rosterVersion ?? ''
'ver': await _stateManager.getRosterVersion() ?? '',
},
)
],

View File

@@ -0,0 +1,220 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/roster/roster.dart';
import 'package:synchronized/synchronized.dart';
class _RosterProcessTriple {
const _RosterProcessTriple(this.removed, this.modified, this.added);
final String? removed;
final XmppRosterItem? modified;
final XmppRosterItem? added;
}
class RosterCacheLoadResult {
const RosterCacheLoadResult(this.version, this.roster);
final String? version;
final List<XmppRosterItem> roster;
}
/// This class manages the roster state in order to correctly process and persist
/// roster pushes and facilitate roster versioning requests.
abstract class BaseRosterStateManager {
/// The cached version of the roster. If null, then it has not been loaded yet.
List<XmppRosterItem>? _currentRoster;
/// The cached version of the roster version.
String? _currentVersion;
/// A critical section locking both _currentRoster and _currentVersion.
final Lock _lock = Lock();
/// A function to send an XmppEvent to moxxmpp's main event bus
late void Function(XmppEvent) _sendEvent;
/// Overrideable function
/// Loads the old cached version of the roster and optionally that roster version
/// from persistent storage into a RosterCacheLoadResult object.
Future<RosterCacheLoadResult> loadRosterCache();
/// Overrideable function
/// Commits the roster data to persistent storage.
///
/// [version] is the roster version string. If none was provided, then this value
/// is null.
///
/// [removed] is a (possibly empty) list of bare JIDs that are removed from the
/// roster.
///
/// [modified] is a (possibly empty) list of XmppRosterItems that are modified. Correlation with
/// the cache is done using its jid attribute.
///
/// [added] is a (possibly empty) list of XmppRosterItems that are added by the
/// roster push or roster fetch request.
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added);
/// Internal function. Registers functions from the RosterManger against this
/// instance.
void register(void Function(XmppEvent) sendEvent) {
_sendEvent = sendEvent;
}
/// Load and cache or return the cached roster version.
Future<String?> getRosterVersion() async {
return _lock.synchronized(() async {
await _loadRosterCache();
return _currentVersion;
});
}
/// A wrapper around _commitRoster that also sends an event to moxxmpp's event
/// bus.
Future<void> _commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added) async {
_sendEvent(
RosterUpdatedEvent(
removed,
modified,
added,
),
);
await commitRoster(version, removed, modified, added);
}
/// Loads the cached roster data into memory, if that has not already happened.
/// NOTE: Must be called from within the _lock critical section.
Future<void> _loadRosterCache() async {
if (_currentRoster == null) {
final result = await loadRosterCache();
_currentRoster = result.roster;
_currentVersion = result.version;
}
}
/// Processes only single XmppRosterItem [item].
/// NOTE: Requires to be called from within the _lock critical section.
_RosterProcessTriple _handleRosterItem(XmppRosterItem item) {
if (item.subscription == 'remove') {
// The item has been removed
_currentRoster!.removeWhere((i) => i.jid == item.jid);
return _RosterProcessTriple(
item.jid,
null,
null,
);
}
final index = _currentRoster!.indexWhere((i) => i.jid == item.jid);
if (index == -1) {
// The item does not exist
_currentRoster!.add(item);
return _RosterProcessTriple(
null,
null,
item,
);
} else if (_currentRoster![index] != item) {
// The item is updated
_currentRoster![index] = item;
return _RosterProcessTriple(
null,
item,
null,
);
}
// Item has not been modified or added
return const _RosterProcessTriple(
null,
null,
null,
);
}
/// Handles a roster push from the RosterManager.
Future<void> handleRosterPush(RosterPushResult event) async {
await _lock.synchronized(() async {
await _loadRosterCache();
_currentVersion = event.ver;
final result = _handleRosterItem(event.item);
if (result.removed != null) {
return _commitRoster(
_currentVersion,
[result.removed!],
[],
[],
);
} else if (result.modified != null) {
return _commitRoster(
_currentVersion,
[],
[result.modified!],
[],
);
} else if (result.added != null) {
return _commitRoster(
_currentVersion,
[],
[],
[result.added!],
);
}
});
}
/// Handles the result from a roster fetch.
Future<void> handleRosterFetch(RosterRequestResult result) async {
await _lock.synchronized(() async {
final removed = List<String>.empty(growable: true);
final modified = List<XmppRosterItem>.empty(growable: true);
final added = List<XmppRosterItem>.empty(growable: true);
await _loadRosterCache();
_currentVersion = result.ver;
for (final item in result.items) {
final result = _handleRosterItem(item);
if (result.removed != null) removed.add(result.removed!);
if (result.modified != null) modified.add(result.modified!);
if (result.added != null) added.add(result.added!);
}
await _commitRoster(
_currentVersion,
removed,
modified,
added,
);
});
}
@visibleForTesting
List<XmppRosterItem> getRosterItems() => _currentRoster!;
}
@visibleForTesting
class TestingRosterStateManager extends BaseRosterStateManager {
TestingRosterStateManager(
this.initialRosterVersion,
this.initialRoster,
);
final String? initialRosterVersion;
final List<XmppRosterItem> initialRoster;
int loadCount = 0;
@override
Future<RosterCacheLoadResult> loadRosterCache() async {
loadCount++;
return RosterCacheLoadResult(
initialRosterVersion,
initialRoster,
);
}
@override
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added) async {}
}

View File

@@ -129,6 +129,12 @@ class XMLNode {
}).toList();
}
List<XMLNode> findTagsByXmlns(String xmlns) {
return children
.where((element) => element.attributes['xmlns'] == xmlns)
.toList();
}
/// Returns the inner text of the node. If none is set, returns the "".
String innerText() {
return text ?? '';

View File

@@ -289,7 +289,7 @@ class DiscoManager extends XmppManagerBase {
}
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node}) async {
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final cacheKey = DiscoCacheKey(entity, node);
DiscoInfo? info;
Completer<Result<DiscoError, DiscoInfo>>? completer;
@@ -316,6 +316,7 @@ class DiscoManager extends XmppManagerBase {
final stanza = await getAttributes().sendStanza(
buildDiscoInfoQueryStanza(entity, node),
encrypted: !shouldEncrypt,
);
final query = stanza.firstTag('query');
if (query == null) {
@@ -359,9 +360,12 @@ class DiscoManager extends XmppManagerBase {
}
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node }) async {
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final stanza = await getAttributes()
.sendStanza(buildDiscoItemsQueryStanza(entity, node: node)) as Stanza;
.sendStanza(
buildDiscoItemsQueryStanza(entity, node: node),
encrypted: !shouldEncrypt,
) as Stanza;
final query = stanza.firstTag('query');
if (query == null) return Result(InvalidResponseDiscoError());

View File

@@ -7,15 +7,20 @@ 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';
abstract class VCardError {}
class UnknownVCardError extends VCardError {}
class InvalidVCardError extends VCardError {}
class VCardPhoto {
const VCardPhoto({ this.binval });
final String? binval;
}
class VCard {
const VCard({ this.nickname, this.url, this.photo });
final String? nickname;
final String? url;
@@ -23,7 +28,6 @@ class VCard {
}
class VCardManager extends XmppManagerBase {
VCardManager() : _lastHash = {}, super();
final Map<String, String> _lastHash;
@@ -59,12 +63,18 @@ class VCardManager extends XmppManagerBase {
final lastHash = _lastHash[from];
if (lastHash != hash) {
_lastHash[from] = hash;
final vcard = await requestVCard(from);
final vcardResult = await requestVCard(from);
if (vcard != null) {
final binval = vcard.photo?.binval;
if (vcardResult.isType<VCard>()) {
final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) {
getAttributes().sendEvent(AvatarUpdatedEvent(jid: from, base64: binval, hash: hash));
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
);
} else {
logger.warning('No avatar data found');
}
@@ -95,7 +105,7 @@ class VCardManager extends XmppManagerBase {
);
}
Future<VCard?> requestVCard(String jid) async {
Future<Result<VCardError, VCard>> requestVCard(String jid) async {
final result = await getAttributes().sendStanza(
Stanza.iq(
to: jid,
@@ -107,12 +117,13 @@ class VCardManager extends XmppManagerBase {
)
],
),
encrypted: true,
);
if (result.attributes['type'] != 'result') return null;
if (result.attributes['type'] != 'result') return Result(UnknownVCardError());
final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns);
if (vcard == null) return null;
if (vcard == null) return Result(UnknownVCardError());
return _parseVCard(vcard);
return Result(_parseVCard(vcard));
}
}

View File

@@ -16,7 +16,6 @@ import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
class PubSubPublishOptions {
const PubSubPublishOptions({
this.accessModel,
this.maxItems,
@@ -60,7 +59,6 @@ class PubSubPublishOptions {
}
class PubSubItem {
const PubSubItem({ required this.id, required this.node, required this.payload });
final String id;
final String node;

View File

@@ -3,21 +3,24 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.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_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
class UserAvatar {
abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {}
class UserAvatar {
const UserAvatar({ required this.base64, required this.hash });
final String base64;
final String hash;
}
class UserAvatarMetadata {
const UserAvatarMetadata(
this.id,
this.length,
@@ -49,6 +52,14 @@ class UserAvatarManager extends XmppManagerBase {
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is PubSubNotificationEvent) {
if (event.item.node != userAvatarDataXmlns) return;
if (event.item.payload.tag != 'data' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
logger.warning('Received avatar update from ${event.from} but the payload is invalid. Ignoring...');
return;
}
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: event.from,
@@ -62,30 +73,30 @@ class UserAvatarManager extends XmppManagerBase {
// TODO(PapaTutuWawa): Check for PEP support
@override
Future<bool> isSupported() async => true;
/// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise
// TODO(Unknown): Migrate to Resultsv2
Future<UserAvatar?> getUserAvatar(String jid) async {
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
if (resultsRaw.isType<PubSubError>()) return null;
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
final results = resultsRaw.get<List<PubSubItem>>();
if (results.isEmpty) return null;
if (results.isEmpty) return Result(UnknownAvatarError());
final item = results[0];
return UserAvatar(
base64: item.payload.innerText(),
hash: item.id,
return Result(
UserAvatar(
base64: item.payload.innerText(),
hash: item.id,
),
);
}
/// Publish the avatar data, [base64], on the pubsub node using [hash] as
/// the item id. [hash] must be the SHA-1 hash of the image data, while
/// [base64] must be the base64-encoded version of the image data.
// TODO(Unknown): Migrate to Resultsv2
Future<bool> publishUserAvatar(String base64, String hash, bool public) async {
Future<Result<AvatarError, bool>> publishUserAvatar(String base64, String hash, bool public) async {
final pubsub = _getPubSubManager();
final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(),
@@ -101,14 +112,15 @@ class UserAvatarManager extends XmppManagerBase {
),
);
return !result.isType<PubSubError>();
if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
return const Result(true);
}
/// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public]
/// is true, then the node will be set to an 'open' access model. If [public] is false,
/// then the node will be set to an 'roster' access model.
// TODO(Unknown): Migrate to Resultsv2
Future<bool> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async {
Future<Result<AvatarError, bool>> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async {
final pubsub = _getPubSubManager();
final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(),
@@ -135,39 +147,37 @@ class UserAvatarManager extends XmppManagerBase {
),
);
return result.isType<PubSubError>();
if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
return const Result(true);
}
/// Subscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2
Future<bool> subscribe(String jid) async {
Future<Result<AvatarError, bool>> subscribe(String jid) async {
await _getPubSubManager().subscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true;
return const Result(true);
}
/// Unsubscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2
Future<bool> unsubscribe(String jid) async {
Future<Result<AvatarError, bool>> unsubscribe(String jid) async {
await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true;
return const Result(true);
}
/// Returns the PubSub Id of an avatar after doing a disco#items query.
/// Note that this assumes that there is only one (1) item published on
/// the node.
// TODO(Unknown): Migrate to Resultsv2
Future<String?> getAvatarId(String jid) async {
Future<Result<AvatarError, String>> getAvatarId(String jid) async {
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns);
if (response.isType<DiscoError>()) return null;
final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns, shouldEncrypt: false);
if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
final items = response.get<List<DiscoItem>>();
if (items.isEmpty) return null;
if (items.isEmpty) return Result(UnknownAvatarError());
return items.first.name;
return Result(items.first.name);
}
}

View File

@@ -142,7 +142,7 @@ class StreamManagementManager extends XmppManagerBase {
];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
StanzaHandler(
callback: _onServerStanzaReceived,
priority: 9999,

View File

@@ -8,14 +8,12 @@ import 'package:moxxmpp/src/stanza.dart';
@immutable
class DelayedDelivery {
const DelayedDelivery(this.from, this.timestamp);
final DateTime timestamp;
final String from;
}
class DelayedDeliveryManager extends XmppManagerBase {
@override
String getId() => delayedDeliveryManager;

View File

@@ -13,11 +13,10 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart';
class CarbonsManager extends XmppManagerBase {
CarbonsManager() : _isEnabled = false, _supported = false, _gotSupported = false, super();
bool _isEnabled;
bool _supported;
bool _gotSupported;
CarbonsManager() : super();
bool _isEnabled = false;
bool _supported = false;
bool _gotSupported = false;
@override
String getId() => carbonsManager;
@@ -26,13 +25,12 @@ class CarbonsManager extends XmppManagerBase {
String getName() => 'CarbonsManager';
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'received',
tagXmlns: carbonsXmlns,
callback: _onMessageReceived,
// Before all managers the message manager depends on
priority: -98,
),
StanzaHandler(
@@ -40,7 +38,6 @@ class CarbonsManager extends XmppManagerBase {
tagName: 'sent',
tagXmlns: carbonsXmlns,
callback: _onMessageSent,
// Before all managers the message manager depends on
priority: -98,
)
];
@@ -159,6 +156,9 @@ class CarbonsManager extends XmppManagerBase {
return true;
}
/// True if Message Carbons are enabled. False, if not.
bool get isEnabled => _isEnabled;
@visibleForTesting
void forceEnable() {
_isEnabled = true;

View File

@@ -30,8 +30,8 @@ class LastMessageCorrectionManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'reply',
tagXmlns: replyXmlns,
tagName: 'replace',
tagXmlns: lmcXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
@@ -42,9 +42,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns);
if (edit == null) return state;
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
return state.copyWith(
lastMessageCorrectionSid: edit.attributes['id']! as String,
);

View File

@@ -8,8 +8,18 @@ enum MessageProcessingHint {
store,
}
/// NOTE: We do not define a function for turning a Message Processing Hint element into
/// an enum value since the elements do not concern us as a client.
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
switch (element.tag) {
case 'no-permanent-store': return MessageProcessingHint.noPermanentStore;
case 'no-store': return MessageProcessingHint.noStore;
case 'no-copy': return MessageProcessingHint.noCopies;
case 'store': return MessageProcessingHint.store;
}
assert(false, 'Invalid Message Processing Hint: ${element.tag}');
return MessageProcessingHint.noStore;
}
extension XmlExtension on MessageProcessingHint {
XMLNode toXml() {
String tag;

View File

@@ -7,3 +7,5 @@ class InvalidAffixElementsException with Exception {}
class OmemoNotSupportedForContactException extends OmemoError {}
class EncryptionFailedException with Exception {}
class InvalidEnvelopePayloadException with Exception {}

View File

@@ -1,8 +1,6 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
@@ -18,6 +16,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_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
@@ -25,7 +24,7 @@ import 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:synchronized/synchronized.dart';
import 'package:xml/xml.dart';
const _doNotEncryptList = [
// XEP-0033
@@ -43,17 +42,7 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns),
];
abstract class OmemoManager extends XmppManagerBase {
OmemoManager() : _handlerLock = Lock(), _handlerFutures = {}, super();
final Lock _handlerLock;
final Map<JID, Queue<Completer<void>>> _handlerFutures;
final Map<JID, List<int>> _deviceMap = {};
// Mapping whether we already tried to subscribe to the JID's devices node
final Map<JID, bool> _subscriptionMap = {};
abstract class BaseOmemoManager extends XmppManagerBase {
@override
String getId() => omemoManager;
@@ -65,27 +54,24 @@ abstract class OmemoManager extends XmppManagerBase {
Future<bool> isSupported() async => true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'iq',
tagXmlns: omemoXmlns,
tagName: 'encrypted',
callback: _onIncomingStanza,
priority: 9999,
),
StanzaHandler(
stanzaTag: 'presence',
tagXmlns: omemoXmlns,
tagName: 'encrypted',
callback: _onIncomingStanza,
priority: 9999,
),
StanzaHandler(
stanzaTag: 'message',
tagXmlns: omemoXmlns,
tagName: 'encrypted',
callback: _onIncomingStanza,
priority: -98,
),
];
@@ -127,60 +113,31 @@ abstract class OmemoManager extends XmppManagerBase {
} else {
// Someone published to their device list node
logger.finest('Got devices $ids');
_deviceMap[jid] = ids;
}
// Tell the OmemoManager
(await getOmemoManager())
.onDeviceListUpdate(jid.toString(), ids);
// Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
}
}
@visibleForOverriding
Future<OmemoSessionManager> getSessionManager();
Future<OmemoManager> getOmemoManager();
/// Wrapper around using getSessionManager and then calling encryptToJids on it.
Future<EncryptionResult> _encryptToJids(List<String> jids, String? plaintext, { List<OmemoBundle>? newSessions }) async {
final session = await getSessionManager();
return session.encryptToJids(jids, plaintext, newSessions: newSessions);
}
/// Wrapper around using getSessionManager and then calling encryptToJids on it.
Future<String?> _decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys, int sendTimestamp) async {
final session = await getSessionManager();
return session.decryptMessage(
ciphertext,
senderJid,
senderDeviceId,
keys,
sendTimestamp,
);
}
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async {
final session = await getSessionManager();
return session.getDeviceId();
}
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async {
final session = await getSessionManager();
return session.getDeviceBundle();
final om = await getOmemoManager();
final device = await om.getDevice();
return device.toBundle();
}
/// Wrapper around using getSessionManager and then calling isRatchetAcknowledged on it.
Future<bool> _isRatchetAcknowledged(String jid, int deviceId) async {
final session = await getSessionManager();
return session.isRatchetAcknowledged(jid, deviceId);
}
/// Wrapper around checking if [jid] appears in the session manager's device map.
Future<bool> _hasSessionWith(String jid) async {
final session = await getSessionManager();
final deviceMap = await session.getDeviceMap();
return deviceMap.containsKey(jid);
}
/// Determines what child elements of a stanza should be encrypted. If shouldEncrypt
/// returns true for [element], then [element] will be encrypted. If shouldEncrypt
/// returns false, then [element] won't be encrypted.
@@ -205,56 +162,51 @@ abstract class OmemoManager extends XmppManagerBase {
/// an attached payload, if [children] is not null, or an empty OMEMO message if
/// [children] is null. This function takes care of creating the affix elements as
/// specified by both XEP-0420 and XEP-0384.
/// [jids] is the list of JIDs the payload should be encrypted for.
Future<XMLNode> _encryptChildren(List<XMLNode>? children, List<String> jids, String toJid, List<OmemoBundle> newSessions) async {
XMLNode? payload;
if (children != null) {
payload = XMLNode.xmlns(
tag: 'envelope',
xmlns: sceXmlns,
children: [
XMLNode(
tag: 'content',
children: children,
),
/// [toJid] is the list of JIDs the payload should be encrypted for.
String _buildEnvelope(List<XMLNode> children, String toJid) {
final payload = XMLNode.xmlns(
tag: 'envelope',
xmlns: sceXmlns,
children: [
XMLNode(
tag: 'content',
children: children,
),
XMLNode(
tag: 'rpad',
text: generateRpad(),
),
XMLNode(
tag: 'to',
attributes: <String, String>{
'jid': toJid,
},
),
XMLNode(
tag: 'from',
attributes: <String, String>{
'jid': getAttributes().getFullJID().toString(),
},
),
/*
XMLNode(
tag: 'time',
// TODO(Unknown): Implement
attributes: <String, String>{
'stamp': '',
},
),
*/
],
);
}
final encryptedEnvelope = await _encryptToJids(
jids,
payload?.toXml(),
newSessions: newSessions,
XMLNode(
tag: 'rpad',
text: generateRpad(),
),
XMLNode(
tag: 'to',
attributes: <String, String>{
'jid': toJid,
},
),
XMLNode(
tag: 'from',
attributes: <String, String>{
'jid': getAttributes().getFullJID().toString(),
},
),
/*
XMLNode(
tag: 'time',
// TODO(Unknown): Implement
attributes: <String, String>{
'stamp': '',
},
),
*/
],
);
return payload.toXml();
}
XMLNode _buildEncryptedElement(EncryptionResult result, String recipientJid, int deviceId) {
final keyElements = <String, List<XMLNode>>{};
for (final key in encryptedEnvelope.encryptedKeys) {
for (final key in result.encryptedKeys) {
final keyElement = XMLNode(
tag: 'key',
attributes: <String, String>{
@@ -282,11 +234,11 @@ abstract class OmemoManager extends XmppManagerBase {
}).toList();
var payloadElement = <XMLNode>[];
if (payload != null) {
if (result.ciphertext != null) {
payloadElement = [
XMLNode(
tag: 'payload',
text: base64.encode(encryptedEnvelope.ciphertext!),
text: base64.encode(result.ciphertext!),
),
];
}
@@ -299,7 +251,7 @@ abstract class OmemoManager extends XmppManagerBase {
XMLNode(
tag: 'header',
attributes: <String, String>{
'sid': (await _getDeviceId()).toString(),
'sid': deviceId.toString(),
},
children: keysElements,
),
@@ -307,136 +259,18 @@ abstract class OmemoManager extends XmppManagerBase {
);
}
/// A logging wrapper around acking the ratchet with [jid] with identifier [deviceId].
Future<void> _ackRatchet(String jid, int deviceId) async {
logger.finest('Acking ratchet $jid:$deviceId');
final session = await getSessionManager();
await session.ratchetAcknowledged(jid, deviceId);
}
/// Figure out if new sessions need to be built. [toJid] is the JID of the entity we
/// want to send a message to. [children] refers to the unencrypted children of the
/// message. They are required to be passed because shouldIgnoreUnackedRatchets is
/// called here.
///
/// Either returns a list of bundles we "need" to build a session with or an OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> _findNewSessions(JID toJid, List<XMLNode> children) async {
final ownJid = getAttributes().getFullJID().toBare();
final session = await getSessionManager();
final ownId = await session.getDeviceId();
// Ignore our own device if it is the only published device on our devices node
if (toJid.toBare() == ownJid) {
final deviceList = await getDeviceList(ownJid);
if (deviceList.isType<List<int>>()) {
final devices = deviceList.get<List<int>>();
if (devices.length == 1 && devices.first == ownId) {
return const Result(<OmemoBundle>[]);
}
}
}
final newSessions = List<OmemoBundle>.empty(growable: true);
final sessionAvailable = await _hasSessionWith(toJid.toString());
if (!sessionAvailable) {
logger.finest('No session for $toJid. Retrieving bundles to build a new session.');
final result = await retrieveDeviceBundles(toJid);
if (result.isType<List<OmemoBundle>>()) {
final bundles = result.get<List<OmemoBundle>>();
if (ownJid == toJid) {
logger.finest('Requesting bundles for own JID. Ignoring current device');
newSessions.addAll(bundles.where((bundle) => bundle.id != ownId));
} else {
newSessions.addAll(bundles);
}
} else {
logger.warning('Failed to retrieve device bundles for $toJid');
return Result(OmemoNotSupportedForContactException());
}
if (!_subscriptionMap.containsKey(toJid)) {
await subscribeToDeviceList(toJid);
}
} else {
final toBare = toJid.toBare();
final ratchetSessions = (await session.getDeviceMap())[toBare.toString()]!;
final deviceMapRaw = await getDeviceList(toBare);
if (!_subscriptionMap.containsKey(toBare)) {
unawaited(subscribeToDeviceList(toBare));
}
if (deviceMapRaw.isType<OmemoError>()) {
logger.warning('Failed to get device list');
return Result(UnknownOmemoError());
}
final deviceList = deviceMapRaw.get<List<int>>();
for (final id in deviceList) {
// We already have a session with that device
if (ratchetSessions.contains(id)) continue;
// Ignore requests for our own device.
if (toJid == ownJid && id == ownId) {
logger.finest('Attempted to request bundle for our own device $id, which is the current device. Skipping request...');
continue;
}
logger.finest('Retrieving bundle for $toJid:$id');
final bundle = await retrieveDeviceBundle(toJid, id);
if (bundle.isType<OmemoBundle>()) {
newSessions.add(bundle.get<OmemoBundle>());
} else {
logger.warning('Failed to retrieve bundle for $toJid:$id');
}
}
}
return Result(newSessions);
}
/// Sends an empty Omemo message to [toJid].
///
/// If [findNewSessions] is true, then
/// new devices will be looked for first before sending the message. This means that
/// the new sessions will be included in the empty Omemo message. If false, then no
/// new sessions will be looked for before encrypting.
///
/// [calledFromCriticalSection] MUST NOT be used from outside the manager. If true, then
/// sendEmptyMessage will not attempt to enter the critical section guarding the
/// encryption and decryption. If false, then the critical section will be entered before
/// encryption and left after sending the message.
Future<void> sendEmptyMessage(JID toJid, {
bool findNewSessions = false,
@protected
bool calledFromCriticalSection = false,
}) async {
if (!calledFromCriticalSection) {
final completer = await _handlerEntry(toJid);
if (completer != null) {
await completer.future;
}
}
var newSessions = <OmemoBundle>[];
if (findNewSessions) {
final result = await _findNewSessions(toJid, <XMLNode>[]);
if (!result.isType<OmemoError>()) newSessions = result.get<List<OmemoBundle>>();
}
final empty = await _encryptChildren(
null,
[toJid.toString()],
toJid.toString(),
newSessions,
);
/// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl(EncryptionResult result, String toJid) async {
await getAttributes().sendStanza(
Stanza.message(
to: toJid.toString(),
to: toJid,
type: 'chat',
children: [
empty,
_buildEncryptedElement(
result,
toJid,
await _getDeviceId(),
),
// Add a storage hint in case this is a message
// Taken from the example at
@@ -447,10 +281,28 @@ abstract class OmemoManager extends XmppManagerBase {
awaitable: false,
encrypted: true,
);
}
if (!calledFromCriticalSection) {
await _handlerExit(toJid);
}
/// Send a heartbeat message to [jid].
Future<void> sendOmemoHeartbeat(String jid) async {
final om = await getOmemoManager();
await om.sendOmemoHeartbeat(jid);
}
/// For usage with omemo_dart's OmemoManager
Future<List<int>?> fetchDeviceList(String jid) async {
final result = await getDeviceList(JID.fromString(jid));
if (result.isType<OmemoError>()) return null;
return result.get<List<int>>();
}
/// For usage with omemo_dart's OmemoManager
Future<OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
final result = await retrieveDeviceBundle(JID.fromString(jid), id);
if (result.isType<OmemoError>()) return null;
return result.get<OmemoBundle>();
}
Future<StanzaHandlerData> _onOutgoingStanza(Stanza stanza, StanzaHandlerData state) async {
@@ -461,6 +313,7 @@ abstract class OmemoManager extends XmppManagerBase {
if (stanza.to == null) {
// We cannot encrypt in this case.
logger.finest('Not encrypting since stanza.to is null');
return state;
}
@@ -471,34 +324,7 @@ abstract class OmemoManager extends XmppManagerBase {
} else {
logger.finest('shouldEncryptStanza returned true for message to $toJid.');
}
final completer = await _handlerEntry(toJid);
if (completer != null) {
await completer.future;
}
final newSessions = List<OmemoBundle>.empty(growable: true);
// Try to find new sessions for [toJid].
final resultToJid = await _findNewSessions(toJid, stanza.children);
if (resultToJid.isType<List<OmemoBundle>>()) {
newSessions.addAll(resultToJid.get<List<OmemoBundle>>());
} else {
if (resultToJid.isType<OmemoNotSupportedForContactException>()) {
await _handlerExit(toJid);
return state.copyWith(
cancel: true,
cancelReason: resultToJid.get<OmemoNotSupportedForContactException>(),
);
}
}
// Try to find new sessions for our own Jid.
final ownJid = getAttributes().getFullJID().toBare();
final resultOwnJid = await _findNewSessions(ownJid, stanza.children);
if (resultOwnJid.isType<List<OmemoBundle>>()) {
newSessions.addAll(resultOwnJid.get<List<OmemoBundle>>());
}
final toEncrypt = List<XMLNode>.empty(growable: true);
final children = List<XMLNode>.empty(growable: true);
for (final child in stanza.children) {
@@ -508,76 +334,56 @@ abstract class OmemoManager extends XmppManagerBase {
toEncrypt.add(child);
}
}
logger.finest('Beginning encryption');
final carbonsEnabled = getAttributes()
.getManagerById<CarbonsManager>(carbonsManager)?.isEnabled ?? false;
final om = await getOmemoManager();
final result = await om.onOutgoingStanza(
OmemoOutgoingStanza(
[
toJid.toString(),
if (carbonsEnabled)
getAttributes().getFullJID().toBare().toString(),
],
_buildEnvelope(toEncrypt, toJid.toString()),
),
);
logger.finest('Encryption done');
final jidsToEncryptFor = <String>[JID.fromString(stanza.to!).toBare().toString()];
// Prevent encrypting to self if there is only one device (ours).
if (await _hasSessionWith(ownJid.toString())) {
jidsToEncryptFor.add(ownJid.toString());
}
try {
logger.finest('Encrypting stanza');
final encrypted = await _encryptChildren(
toEncrypt,
jidsToEncryptFor,
stanza.to!,
newSessions,
);
logger.finest('Encryption done');
children.add(encrypted);
// Only add EME when sending a message
if (stanza.tag == 'message') {
children.add(buildEmeElement(ExplicitEncryptionType.omemo2));
}
// Add a storage hint in case this is a message
// Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
if (stanza.tag == 'message') {
children.add(MessageProcessingHint.store.toXml());
}
await _handlerExit(toJid);
return state.copyWith(
stanza: state.stanza.copyWith(
children: children,
),
encrypted: true,
);
} catch (ex) {
logger.severe('Encryption failed! $ex');
await _handlerExit(toJid);
if (!result.isSuccess(2)) {
final other = Map<String, dynamic>.from(state.other);
other['encryption_error_jids'] = result.jidEncryptionErrors;
other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith(
other: other,
cancel: true,
cancelReason: EncryptionFailedException(),
other: {
...state.other,
'encryption_error': ex,
},
);
}
}
/// This function returns true if the encryption scheme should ignore unacked ratchets
/// and don't try to build a new ratchet even though there are unacked ones.
/// The current logic is that chat states with no body ignore the "ack" state of the
/// ratchets.
///
/// This function may be overriden. By default, the ack status of the ratchet is ignored
/// if we're sending a message containing chatstates or chat markers and the message does
/// not contain a <body /> element.
@visibleForOverriding
bool shouldIgnoreUnackedRatchets(List<XMLNode> children) {
return listContains(
children,
(XMLNode child) {
return child.attributes['xmlns'] == chatStateXmlns || child.attributes['xmlns'] == chatMarkersXmlns;
},
) && !listContains(
children,
(XMLNode child) => child.tag == 'body',
final encrypted = _buildEncryptedElement(
result,
toJid.toString(),
await _getDeviceId(),
);
children.add(encrypted);
// Only add message specific metadata when actually sending a message
if (stanza.tag == 'message') {
children
// Add EME data
..add(buildEmeElement(ExplicitEncryptionType.omemo2))
// Add a storage hint in case this is a message
// Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
..add(MessageProcessingHint.store.toXml());
}
return state.copyWith(
stanza: state.stanza.copyWith(
children: children,
),
encrypted: true,
);
}
@@ -587,48 +393,12 @@ abstract class OmemoManager extends XmppManagerBase {
@visibleForOverriding
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza);
/// Wrapper function that attempts to enter the encryption/decryption critical section.
/// In case the critical section could be entered, null is returned. If not, then a
/// Completer is returned whose future will resolve once the critical section can be
/// entered.
Future<Completer<void>?> _handlerEntry(JID fromJid) async {
return _handlerLock.synchronized(() {
if (_handlerFutures.containsKey(fromJid)) {
final c = Completer<void>();
_handlerFutures[fromJid]!.addLast(c);
return c;
}
_handlerFutures[fromJid] = Queue();
return null;
});
}
/// Wrapper function that exits the critical section.
Future<void> _handlerExit(JID fromJid) async {
await _handlerLock.synchronized(() {
if (_handlerFutures.containsKey(fromJid)) {
if (_handlerFutures[fromJid]!.isEmpty) {
_handlerFutures.remove(fromJid);
return;
}
_handlerFutures[fromJid]!.removeFirst().complete();
}
});
}
Future<StanzaHandlerData> _onIncomingStanza(Stanza stanza, StanzaHandlerData state) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state;
if (stanza.from == null) return state;
final fromJid = JID.fromString(stanza.from!).toBare();
final completer = await _handlerEntry(fromJid);
if (completer != null) {
await completer.future;
}
final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload');
final keys = List<EncryptedKey>.empty(growable: true);
@@ -649,118 +419,62 @@ abstract class OmemoManager extends XmppManagerBase {
final ourJid = getAttributes().getFullJID();
final sid = int.parse(header.attributes['sid']! as String);
// Ensure that if we receive a message from a device that we don't know about, we
// ensure that _deviceMap is up-to-date.
final devices = _deviceMap[fromJid] ?? <int>[];
if (!devices.contains(sid)) {
await getDeviceList(fromJid);
}
String? decrypted;
try {
decrypted = await _decryptMessage(
payloadElement != null ? base64.decode(payloadElement.innerText()) : null,
final om = await getOmemoManager();
final result = await om.onIncomingStanza(
OmemoIncomingStanza(
fromJid.toString(),
sid,
keys,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch,
);
} catch (ex) {
logger.warning('Error occurred during message decryption: $ex');
keys,
payloadElement?.innerText(),
),
);
await _handlerExit(fromJid);
return state.copyWith(
other: {
...state.other,
'encryption_error': ex,
},
);
}
final isAcked = await _isRatchetAcknowledged(fromJid.toString(), sid);
if (!isAcked) {
// Unacked ratchet decrypted this message
if (decrypted != null) {
// The message is not empty, i.e. contains content
logger.finest('Received non-empty OMEMO encrypted message for unacked ratchet. Acking with empty OMEMO message.');
await _ackRatchet(fromJid.toString(), sid);
await sendEmptyMessage(fromJid, calledFromCriticalSection: true);
final envelope = XMLNode.fromString(decrypted);
final children = stanza.children.where(
(child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns,
).toList()
..addAll(envelope.firstTag('content')!.children);
final other = Map<String, dynamic>.from(state.other);
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException();
}
await _handlerExit(fromJid);
return state.copyWith(
encrypted: true,
stanza: Stanza(
to: stanza.to,
from: stanza.from,
id: stanza.id,
type: stanza.type,
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
),
other: other,
);
} else {
logger.info('Received empty OMEMO message for unacked ratchet. Marking $fromJid:$sid as acked');
await _ackRatchet(fromJid.toString(), sid);
final ownId = await (await getSessionManager()).getDeviceId();
final kex = keys.any((key) => key.kex && key.rid == ownId);
if (kex) {
logger.info('Empty OMEMO message contained a kex. Answering.');
await sendEmptyMessage(fromJid, calledFromCriticalSection: true);
}
await _handlerExit(fromJid);
return state;
}
final other = Map<String, dynamic>.from(state.other);
var children = stanza.children;
if (result.error != null) {
other['encryption_error'] = result.error;
} else {
// The ratchet that decrypted the message was acked
if (decrypted != null) {
final envelope = XMLNode.fromString(decrypted);
children = stanza.children.where(
(child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns,
).toList();
}
final children = stanza.children.where(
(child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns,
).toList()
..addAll(envelope.firstTag('content')!.children);
final other = Map<String, dynamic>.from(state.other);
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException();
}
await _handlerExit(fromJid);
if (result.payload != null) {
XMLNode envelope;
try {
envelope = XMLNode.fromString(result.payload!);
} on XmlParserException catch (_) {
logger.warning('Failed to parse envelope payload: ${result.payload!}');
other['encryption_error'] = InvalidEnvelopePayloadException();
return state.copyWith(
encrypted: true,
stanza: Stanza(
to: stanza.to,
from: stanza.from,
id: stanza.id,
type: stanza.type,
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
),
other: other,
);
} else {
logger.info('Received empty OMEMO message on acked ratchet. Doing nothing');
await _handlerExit(fromJid);
return state;
}
children.addAll(
envelope.firstTag('content')!.children,
);
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException();
}
}
return state.copyWith(
encrypted: true,
stanza: Stanza(
to: stanza.to,
from: stanza.from,
id: stanza.id,
type: stanza.type,
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
),
other: other,
);
}
/// Convenience function that attempts to retrieve the raw XML payload from the
@@ -776,15 +490,12 @@ abstract class OmemoManager extends XmppManagerBase {
/// Retrieves the OMEMO device list from [jid].
Future<Result<OmemoError, List<int>>> getDeviceList(JID jid) async {
if (_deviceMap.containsKey(jid)) return Result(_deviceMap[jid]);
final itemsRaw = await _retrieveDeviceListPayload(jid);
if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError());
final ids = itemsRaw.get<XMLNode>().children
.map((child) => int.parse(child.attributes['id']! as String))
.toList();
_deviceMap[jid] = ids;
return Result(ids);
}
@@ -882,13 +593,9 @@ abstract class OmemoManager extends XmppManagerBase {
}
/// Subscribes to the device list PubSub node of [jid].
Future<void> subscribeToDeviceList(JID jid) async {
Future<void> subscribeToDeviceListImpl(String jid) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = await pm.subscribe(jid.toString(), omemoDevicesXmlns);
if (!result.isType<PubSubError>()) {
_subscriptionMap[jid] = true;
}
await pm.subscribe(jid, omemoDevicesXmlns);
}
/// Attempts to find out if [jid] supports omemo:2.

View File

@@ -0,0 +1,68 @@
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';
class MessageReactions {
const MessageReactions(this.messageId, this.emojis);
final String messageId;
final List<String> emojis;
XMLNode toXml() {
return XMLNode.xmlns(
tag: 'reactions',
xmlns: messageReactionsXmlns,
attributes: <String, String>{
'id': messageId,
},
children: emojis.map((emoji) {
return XMLNode(
tag: 'reaction',
text: emoji,
);
}).toList(),
);
}
}
class MessageReactionsManager extends XmppManagerBase {
@override
List<String> getDiscoFeatures() => [ messageReactionsXmlns ];
@override
String getName() => 'MessageReactionsManager';
@override
String getId() => messageReactionsManager;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'reactions',
tagXmlns: messageReactionsXmlns,
callback: _onReactionsReceived,
// Before the message handler
priority: -99,
),
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onReactionsReceived(Stanza message, StanzaHandlerData state) async {
final reactionsElement = message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
return state.copyWith(
messageReactions: MessageReactions(
reactionsElement.attributes['id']! as String,
reactionsElement.children
.where((c) => c.tag == 'reaction')
.map((c) => c.innerText())
.toList(),
),
);
}
}

View File

@@ -4,7 +4,6 @@ import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart';
class FileMetadataData {
const FileMetadataData({
this.mediaType,
this.width,

View File

@@ -18,7 +18,6 @@ abstract class StatelessFileSharingSource {
/// Implementation for url-data source elements.
class StatelessFileSharingUrlSource extends StatelessFileSharingSource {
StatelessFileSharingUrlSource(this.url);
factory StatelessFileSharingUrlSource.fromXml(XMLNode element) {
@@ -41,8 +40,29 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource {
}
}
class StatelessFileSharingData {
/// Finds the <sources/> element in [node] and returns the list of
/// StatelessFileSharingSources contained with it.
/// If [checkXmlns] is true, then the sources element must also have an xmlns attribute
/// of "urn:xmpp:sfs:0".
List<StatelessFileSharingSource> processStatelessFileSharingSources(XMLNode node, { bool checkXmlns = true }) {
final sources = List<StatelessFileSharingSource>.empty(growable: true);
final sourcesElement = node.firstTag(
'sources',
xmlns: checkXmlns ? sfsXmlns : null,
)!;
for (final source in sourcesElement.children) {
if (source.attributes['xmlns'] == urlDataXmlns) {
sources.add(StatelessFileSharingUrlSource.fromXml(source));
} else if (source.attributes['xmlns'] == sfsEncryptionXmlns) {
sources.add(StatelessFileSharingEncryptedSource.fromXml(source));
}
}
return sources;
}
class StatelessFileSharingData {
const StatelessFileSharingData(this.metadata, this.sources);
/// Parse [node] as a StatelessFileSharingData element.
@@ -50,20 +70,10 @@ class StatelessFileSharingData {
assert(node.attributes['xmlns'] == sfsXmlns, 'Invalid element xmlns');
assert(node.tag == 'file-sharing', 'Invalid element name');
final sources = List<StatelessFileSharingSource>.empty(growable: true);
final sourcesElement = node.firstTag('sources')!;
for (final source in sourcesElement.children) {
if (source.attributes['xmlns'] == urlDataXmlns) {
sources.add(StatelessFileSharingUrlSource.fromXml(source));
} else if (source.attributes['xmlns'] == sfsEncryptionXmlns) {
sources.add(StatelessFileSharingEncryptedSource.fromXml(source));
}
}
return StatelessFileSharingData(
FileMetadataData.fromXML(node.firstTag('file')!),
sources,
// TODO(PapaTutuWawa): This is a work around for Stickers where the source element has a XMLNS but SFS does not have one.
processStatelessFileSharingSources(node, checkXmlns: false),
);
}
@@ -120,7 +130,7 @@ class SFSManager extends XmppManagerBase {
final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!;
return state.copyWith(
sfs: StatelessFileSharingData.fromXML(sfs),
sfs: StatelessFileSharingData.fromXML(sfs, ),
);
}
}

View File

@@ -0,0 +1,310 @@
import 'dart:convert';
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/rfcs/rfc_4790.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_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
class Sticker {
const Sticker(this.metadata, this.sources, this.suggests);
factory Sticker.fromXML(XMLNode node) {
assert(node.tag == 'item', 'sticker has wrong tag');
return Sticker(
FileMetadataData.fromXML(node.firstTag('file', xmlns: fileMetadataXmlns)!),
processStatelessFileSharingSources(node, checkXmlns: false),
{},
);
}
final FileMetadataData metadata;
final List<StatelessFileSharingSource> sources;
// Language -> suggestion
final Map<String, String> suggests;
XMLNode toPubSubXML() {
final suggestsElements = suggests.keys.map((suggest) {
Map<String, String> attrs;
if (suggest.isEmpty) {
attrs = {};
} else {
attrs = {
'xml:lang': suggest,
};
}
return XMLNode(
tag: 'suggest',
attributes: attrs,
text: suggests[suggest],
);
});
return XMLNode(
tag: 'item',
children: [
metadata.toXML(),
...sources.map((source) => source.toXml()),
...suggestsElements,
],
);
}
}
class StickerPack {
const StickerPack(
this.id,
this.name,
this.summary,
this.hashAlgorithm,
this.hashValue,
this.stickers,
this.restricted,
);
factory StickerPack.fromXML(String id, XMLNode node, { bool hashAvailable = true }) {
assert(node.tag == 'pack', 'node has wrong tag');
assert(node.attributes['xmlns'] == stickersXmlns, 'node has wrong XMLNS');
var hashAlgorithm = HashFunction.sha256;
var hashValue = '';
if (hashAvailable) {
final hash = node.firstTag('hash', xmlns: hashXmlns)!;
hashAlgorithm = hashFunctionFromName(hash.attributes['algo']! as String);
hashValue = hash.innerText();
}
return StickerPack(
id,
node.firstTag('name')!.innerText(),
node.firstTag('summary')!.innerText(),
hashAlgorithm,
hashValue,
node.children
.where((e) => e.tag == 'item')
.map<Sticker>(Sticker.fromXML)
.toList(),
node.firstTag('restricted') != null,
);
}
final String id;
// TODO(PapaTutuWawa): Turn name and summary into a Map as it may contain a xml:lang
final String name;
final String summary;
final HashFunction hashAlgorithm;
final String hashValue;
final List<Sticker> stickers;
final bool restricted;
/// When using the fromXML factory to parse a description of a sticker pack with a
/// yet unknown hash, then this function can be used in order to apply the freshly
/// calculated hash to the object.
StickerPack copyWithId(HashFunction newHashFunction, String newId) {
return StickerPack(
newId,
name,
summary,
newHashFunction,
newId,
stickers,
restricted,
);
}
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'pack',
xmlns: stickersXmlns,
children: [
// Pack metadata
XMLNode(
tag: 'name',
text: name,
),
XMLNode(
tag: 'summary',
text: summary,
),
constructHashElement(
hashAlgorithm.toName(),
hashValue,
),
...restricted ?
[XMLNode(tag: 'restricted')] :
[],
// Stickers
...stickers
.map((sticker) => sticker.toPubSubXML()),
],
);
}
/// Calculates the sticker pack's hash as specified by XEP-0449.
Future<String> getHash(HashFunction hashFunction) async {
// Build the meta string
final metaTmp = [
<int>[
...utf8.encode('name'),
0x1f,
0x1f,
...utf8.encode(name),
0x1f,
0x1e,
],
<int>[
...utf8.encode('summary'),
0x1f,
0x1f,
...utf8.encode(summary),
0x1f,
0x1e,
],
]..sort(ioctetSortComparatorRaw);
final metaString = List<int>.empty(growable: true);
for (final m in metaTmp) {
metaString.addAll(m);
}
metaString.add(0x1c);
// Build item hashes
final items = List<List<int>>.empty(growable: true);
for (final sticker in stickers) {
final tmp = List<int>.empty(growable: true)
..addAll(utf8.encode(sticker.metadata.desc!))
..add(0x1e);
final hashes = List<List<int>>.empty(growable: true);
for (final hash in sticker.metadata.hashes.keys) {
hashes.add([
...utf8.encode(hash),
0x1f,
...utf8.encode(sticker.metadata.hashes[hash]!),
0x1f,
0x1e,
]);
}
hashes.sort(ioctetSortComparatorRaw);
for (final hash in hashes) {
tmp.addAll(hash);
}
tmp.add(0x1d);
items.add(tmp);
}
items.sort(ioctetSortComparatorRaw);
final stickersString = List<int>.empty(growable: true);
for (final item in items) {
stickersString.addAll(item);
}
stickersString.add(0x1c);
// Calculate the hash
final rawHash = await CryptographicHashManager.hashFromData(
[
...metaString,
...stickersString,
],
hashFunction,
);
return base64.encode(rawHash).substring(0, 24);
}
}
class StickersManager extends XmppManagerBase {
@override
String getId() => stickersManager;
@override
String getName() => 'StickersManager';
@override
Future<bool> isSupported() async => true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagXmlns: stickersXmlns,
tagName: 'sticker',
callback: _onIncomingMessage,
priority: -99,
),
];
Future<StanzaHandlerData> _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state.copyWith(
stickerPackId: sticker.attributes['pack']! as String,
);
}
/// Publishes the StickerPack [pack] to the PubSub node of [jid]. If specified, then
/// [accessModel] will be used as the PubSub node's access model.
///
/// On success, returns true. On failure, returns a PubSubError.
Future<Result<PubSubError, bool>> publishStickerPack(JID jid, StickerPack pack, { String? accessModel }) async {
assert(pack.id != '', 'The sticker pack must have an id');
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
return pm.publish(
jid.toBare().toString(),
stickersXmlns,
pack.toXML(),
id: pack.id,
options: PubSubPublishOptions(
maxItems: 'max',
accessModel: accessModel,
),
);
}
/// Removes the sticker pack with id [id] from the PubSub node of [jid].
///
/// On success, returns the true. On failure, returns a PubSubError.
Future<Result<PubSubError, bool>> retractStickerPack(JID jid, String id) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
return pm.retract(
jid,
stickersXmlns,
id,
);
}
/// Fetches the sticker pack with id [id] from [jid].
///
/// On success, returns the StickerPack. On failure, returns a PubSubError.
Future<Result<PubSubError, StickerPack>> fetchStickerPack(JID jid, String id) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final stickerPackDataRaw = await pm.getItem(
jid.toBare().toString(),
stickersXmlns,
id,
);
if (stickerPackDataRaw.isType<PubSubError>()) {
return Result(stickerPackDataRaw.get<PubSubError>());
}
final stickerPackData = stickerPackDataRaw.get<PubSubItem>();
final stickerPack = StickerPack.fromXML(
stickerPackData.id,
stickerPackData.payload,
);
return Result(stickerPack);
}
}

View File

@@ -6,12 +6,11 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
class ReplyData {
const ReplyData({
required this.to,
required this.id,
this.start,
this.end,
required this.to,
required this.id,
this.start,
this.end,
});
final String to;
final String id;

View File

@@ -1,6 +1,6 @@
name: moxxmpp
description: A pure-Dart XMPP library
version: 0.1.6
version: 0.1.6+1
homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -8,6 +8,7 @@ environment:
sdk: '>=2.17.5 <3.0.0'
dependencies:
collection: ^1.16.0
cryptography: ^2.0.5
freezed: ^2.1.0+1
freezed_annotation: ^2.1.0
@@ -19,8 +20,8 @@ dependencies:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.3.2
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.4.2
random_string: ^2.3.1
saslprep: ^1.0.2
synchronized: ^3.0.0+2
@@ -31,6 +32,6 @@ dev_dependencies:
build_runner: ^2.1.11
moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.2+8
version: ^0.1.2+9
test: ^1.16.0
very_good_analysis: ^3.0.1

View File

@@ -3,39 +3,43 @@ import 'package:test/test.dart';
void main() {
test('Parse a full JID', () {
final jid = JID.fromString('test@server/abc');
final jid = JID.fromString('test@server/abc');
expect(jid.local, 'test');
expect(jid.domain, 'server');
expect(jid.resource, 'abc');
expect(jid.toString(), 'test@server/abc');
expect(jid.local, 'test');
expect(jid.domain, 'server');
expect(jid.resource, 'abc');
expect(jid.toString(), 'test@server/abc');
});
test('Parse a bare JID', () {
final jid = JID.fromString('test@server');
final jid = JID.fromString('test@server');
expect(jid.local, 'test');
expect(jid.domain, 'server');
expect(jid.resource, '');
expect(jid.toString(), 'test@server');
expect(jid.local, 'test');
expect(jid.domain, 'server');
expect(jid.resource, '');
expect(jid.toString(), 'test@server');
});
test('Parse a JID with no local part', () {
final jid = JID.fromString('server/abc');
final jid = JID.fromString('server/abc');
expect(jid.local, '');
expect(jid.domain, 'server');
expect(jid.resource, 'abc');
expect(jid.toString(), 'server/abc');
expect(jid.local, '');
expect(jid.domain, 'server');
expect(jid.resource, 'abc');
expect(jid.toString(), 'server/abc');
});
test('Equality', () {
expect(JID.fromString('hallo@welt/abc') == JID('hallo', 'welt', 'abc'), true);
expect(JID.fromString('hallo@welt') == JID('hallo', 'welt', 'a'), false);
expect(JID.fromString('hallo@welt/abc') == JID('hallo', 'welt', 'abc'), true);
expect(JID.fromString('hallo@welt') == JID('hallo', 'welt', 'a'), false);
});
test('Whitespaces', () {
expect(JID.fromString('hallo@welt ') == JID('hallo', 'welt', ''), true);
expect(JID.fromString('hallo@welt/abc ') == JID('hallo', 'welt', 'abc'), true);
test('Dot suffix at domain part', () {
expect(JID.fromString('hallo@welt.example.') == JID('hallo', 'welt.example', ''), true);
expect(JID.fromString('hallo@welt.example./test') == JID('hallo', 'welt.example', 'test'), true);
});
test('Parse resource with a slash', () {
expect(JID.fromString('hallo@welt.example./test/welt') == JID('hallo', 'welt.example', 'test/welt'), true);
});
}

View File

@@ -61,7 +61,7 @@ void main() {
])
..registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
])

View File

@@ -0,0 +1,153 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Test receiving a roster push', () async {
final rs = TestingRosterStateManager(null, []);
rs.register((_) {});
await rs.handleRosterPush(
RosterPushResult(
XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
null,
),
);
expect(
rs.getRosterItems().indexWhere((item) => item.jid == 'testuser@server.example') != -1,
true,
);
expect(rs.loadCount, 1);
expect(rs.getRosterItems().length, 1);
// Receive another roster push
await rs.handleRosterPush(
RosterPushResult(
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
null,
),
);
expect(
rs.getRosterItems().indexWhere((item) => item.jid == 'testuser2@server2.example') != -1,
true,
);
expect(rs.loadCount, 1);
expect(rs.getRosterItems().length, 2);
// Remove one of the items
await rs.handleRosterPush(
RosterPushResult(
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'remove',
),
null,
),
);
expect(
rs.getRosterItems().indexWhere((item) => item.jid == 'testuser2@server2.example') == -1,
true,
);
expect(
rs.getRosterItems().indexWhere((item) => item.jid == 'testuser@server.example') != 1,
true,
);
expect(rs.loadCount, 1);
expect(rs.getRosterItems().length, 1);
});
test('Test a roster fetch', () async {
final rs = TestingRosterStateManager(null, []);
rs.register((_) {});
// Fetch the roster
await rs.handleRosterFetch(
RosterRequestResult(
[
XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
XmppRosterItem(
jid: 'testuser3@server3.example',
subscription: 'from',
),
],
'aaaaaaaa',
),
);
expect(rs.loadCount, 1);
expect(rs.getRosterItems().length, 3);
expect(rs.getRosterItems().indexWhere((item) => item.jid == 'testuser@server.example') != -1, true);
expect(rs.getRosterItems().indexWhere((item) => item.jid == 'testuser2@server2.example') != -1, true);
expect(rs.getRosterItems().indexWhere((item) => item.jid == 'testuser3@server3.example') != -1, true);
});
test('Test a roster fetch if we already have a roster', () async {
XmppEvent? event;
final rs = TestingRosterStateManager('aaaaa', [
XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
XmppRosterItem(
jid: 'testuser3@server3.example',
subscription: 'from',
),
]);
rs.register((_event) {
event = _event;
});
// Fetch the roster
await rs.handleRosterFetch(
RosterRequestResult(
[
XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
XmppRosterItem(
jid: 'testuser3@server3.example',
subscription: 'both',
),
XmppRosterItem(
jid: 'testuser4@server4.example',
subscription: 'both',
),
],
'bbbbb',
),
);
expect(event is RosterUpdatedEvent, true);
final updateEvent = event as RosterUpdatedEvent;
expect(updateEvent.added.length, 1);
expect(updateEvent.added.first.jid, 'testuser4@server4.example');
expect(updateEvent.modified.length, 1);
expect(updateEvent.modified.first.jid, 'testuser3@server3.example');
expect(updateEvent.removed.isEmpty, true);
});
}

View File

@@ -1,322 +0,0 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
// TODO(PapaTutuWawa): Fix tests
typedef AddRosterItemFunction = Future<RosterItem> Function(
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups,
}
);
typedef UpdateRosterItemFunction = Future<RosterItem> Function(
int id, {
String? avatarUrl,
String? avatarHash,
String? title,
String? subscription,
String? ask,
List<String>? groups,
}
);
AddRosterItemFunction mkAddRosterItem(void Function(String) callback) {
return (
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups = const [],
}
) async {
callback(jid);
return await addRosterItemFromData(
avatarUrl,
avatarHash,
jid,
title,
subscription,
ask,
groups: groups,
);
};
}
Future<RosterItem> addRosterItemFromData(
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups = const [],
}
) async => RosterItem(
0,
avatarUrl,
avatarHash,
jid,
title,
subscription,
ask,
groups,
);
UpdateRosterItemFunction mkRosterUpdate(List<RosterItem> roster) {
return (
int id, {
String? avatarUrl,
String? avatarHash,
String? title,
String? subscription,
String? ask,
List<String>? groups,
}
) async {
final item = firstWhereOrNull(roster, (RosterItem item) => item.id == id)!;
return item.copyWith(
avatarUrl: avatarUrl ?? item.avatarUrl,
avatarHash: avatarHash ?? item.avatarHash,
title: title ?? item.title,
subscription: subscription ?? item.subscription,
ask: ask ?? item.ask,
groups: groups ?? item.groups,
);
};
}
void main() {
final localRosterSingle = [
RosterItem(
0,
'',
'',
'hallo@server.example',
'hallo',
'none',
'',
[],
)
];
final localRosterDouble = [
RosterItem(
0,
'',
'',
'hallo@server.example',
'hallo',
'none',
'',
[],
),
RosterItem(
1,
'',
'',
'welt@different.server.example',
'welt',
'from',
'',
[ 'Friends' ],
)
];
group('Test roster pushes', () {
test('Test removing an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterDouble,
[
XmppRosterItem(
jid: 'hallo@server.example', subscription: 'remove',
)
],
true,
mkAddRosterItem((_) { addCalled = true; }),
mkRosterUpdate(localRosterDouble),
(jid) async {
if (jid == 'hallo@server.example') {
removeCalled = true;
}
},
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ 'hallo@server.example' ]);
expect(result.modified.length, 0);
expect(result.added.length, 0);
expect(removeCalled, true);
expect(addCalled, false);
});
test('Test adding an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterSingle,
[
XmppRosterItem(
jid: 'welt@different.server.example',
subscription: 'from',
)
],
true,
mkAddRosterItem(
(jid) {
if (jid == 'welt@different.server.example') {
addCalled = true;
}
}
),
mkRosterUpdate(localRosterSingle),
(_) async { removeCalled = true; },
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ ]);
expect(result.modified.length, 0);
expect(result.added.length, 1);
expect(result.added.first.subscription, 'from');
expect(removeCalled, false);
expect(addCalled, true);
});
test('Test modifying an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterDouble,
[
XmppRosterItem(
jid: 'welt@different.server.example',
subscription: 'both',
name: 'The World',
)
],
true,
mkAddRosterItem((_) { addCalled = false; }),
mkRosterUpdate(localRosterDouble),
(_) async { removeCalled = true; },
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ ]);
expect(result.modified.length, 1);
expect(result.added.length, 0);
expect(result.modified.first.subscription, 'both');
expect(result.modified.first.jid, 'welt@different.server.example');
expect(result.modified.first.title, 'The World');
expect(removeCalled, false);
expect(addCalled, false);
});
});
group('Test roster requests', () {
test('Test removing an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterSingle,
[],
false,
mkAddRosterItem((_) { addCalled = true; }),
mkRosterUpdate(localRosterDouble),
(jid) async {
if (jid == 'hallo@server.example') {
removeCalled = true;
}
},
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ 'hallo@server.example' ]);
expect(result.modified.length, 0);
expect(result.added.length, 0);
expect(removeCalled, true);
expect(addCalled, false);
});
test('Test adding an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterSingle,
[
XmppRosterItem(
jid: 'hallo@server.example',
name: 'hallo',
subscription: 'none',
),
XmppRosterItem(
jid: 'welt@different.server.example',
subscription: 'both',
)
],
false,
mkAddRosterItem(
(jid) {
if (jid == 'welt@different.server.example') {
addCalled = true;
}
}
),
mkRosterUpdate(localRosterSingle),
(_) async { removeCalled = true; },
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ ]);
expect(result.modified.length, 0);
expect(result.added.length, 1);
expect(result.added.first.subscription, 'both');
expect(removeCalled, false);
expect(addCalled, true);
});
test('Test modifying an item', () async {
var removeCalled = false;
var addCalled = false;
final result = await processRosterDiff(
localRosterSingle,
[
XmppRosterItem(
jid: 'hallo@server.example',
subscription: 'both',
name: 'Hallo Welt',
)
],
false,
mkAddRosterItem((_) { addCalled = false; }),
mkRosterUpdate(localRosterDouble),
(_) async { removeCalled = true; },
(_) async => null,
(_, { String? id }) async {},
);
expect(result.removed, [ ]);
expect(result.modified.length, 1);
expect(result.added.length, 0);
expect(result.modified.first.subscription, 'both');
expect(result.modified.first.jid, 'hallo@server.example');
expect(result.modified.first.title, 'Hallo Welt');
expect(removeCalled, false);
expect(addCalled, false);
});
});
}

View File

@@ -4,7 +4,7 @@ import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
Future<void> runIncomingStanzaHandlers(StreamManagementManager man, Stanza stanza) async {
for (final handler in man.getIncomingStanzaHandlers()) {
for (final handler in man.getIncomingPreStanzaHandlers()) {
if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, false, null, stanza));
}
}
@@ -243,7 +243,7 @@ void main() {
final sm = StreamManagementManager();
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
sm,
@@ -365,7 +365,7 @@ void main() {
final sm = StreamManagementManager();
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
sm,
@@ -519,7 +519,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
StreamManagementManager(),
@@ -611,7 +611,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
StreamManagementManager(),
@@ -703,7 +703,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
StreamManagementManager(),

View File

@@ -7,7 +7,7 @@ import 'helpers/xmpp.dart';
/// Returns true if the roster manager triggeres an event for a given stanza
Future<bool> testRosterManager(String bareJid, String resource, String stanzaString) async {
var eventTriggered = false;
final roster = RosterManager();
final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {
@@ -127,7 +127,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
StreamManagementManager(),
@@ -181,7 +181,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
]);
@@ -235,7 +235,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
]);
@@ -290,7 +290,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager(),
PingManager(),
]);
@@ -308,7 +308,7 @@ void main() {
group('Test roster pushes', () {
test('Test for a CVE-2015-8688 style vulnerability', () async {
var eventTriggered = false;
final roster = RosterManager();
final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {

View File

@@ -1,3 +1,7 @@
## 0.1.2+9
- Update a dependency to the latest release.
## 0.1.2+8
- Update a dependency to the latest release.

View File

@@ -14,3 +14,9 @@ is, for example, [moxdns](https://codeberg.org/moxxy/moxdns).
## License
See `./LICENSE`.
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -1,6 +1,6 @@
name: moxxmpp_socket_tcp
description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368
version: 0.1.2+8
version: 0.1.2+9
homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -12,7 +12,7 @@ dependencies:
meta: ^1.6.0
moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.6
version: ^0.1.6+1
dev_dependencies:
lints: ^2.0.0