13 Commits

31 changed files with 927 additions and 562 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 ## License
See `./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 version: 0.1.4+1
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.5 version: 0.1.6+1
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.2+7 version: 0.1.2+9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -1,3 +1,11 @@
## 0.1.6+1
- **FIX**: Fix LMC not working.
## 0.1.6
- **FEAT**: Implement XEP-0308.
## 0.1.5 ## 0.1.5
- **FEAT**: Message events now contain the stanza error, if available. - **FEAT**: Message events now contain the stanza error, if available.

View File

@@ -5,3 +5,9 @@ A pure-Dart XMPP library written for Moxxy.
## License ## License
See `./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

@@ -59,6 +59,7 @@ export 'package:moxxmpp/src/xeps/xep_0203.dart';
export 'package:moxxmpp/src/xeps/xep_0280.dart'; export 'package:moxxmpp/src/xeps/xep_0280.dart';
export 'package:moxxmpp/src/xeps/xep_0297.dart'; export 'package:moxxmpp/src/xeps/xep_0297.dart';
export 'package:moxxmpp/src/xeps/xep_0300.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_0333.dart';
export 'package:moxxmpp/src/xeps/xep_0334.dart'; export 'package:moxxmpp/src/xeps/xep_0334.dart';
export 'package:moxxmpp/src/xeps/xep_0352.dart'; export 'package:moxxmpp/src/xeps/xep_0352.dart';
@@ -74,7 +75,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_0385.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart'; export 'package:moxxmpp/src/xeps/xep_0414.dart';
export 'package:moxxmpp/src/xeps/xep_0424.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_0446.dart';
export 'package:moxxmpp/src/xeps/xep_0447.dart'; export 'package:moxxmpp/src/xeps/xep_0447.dart';
export 'package:moxxmpp/src/xeps/xep_0448.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'; export 'package:moxxmpp/src/xeps/xep_0461.dart';

View File

@@ -6,9 +6,11 @@ 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_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.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_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@@ -74,6 +76,10 @@ class MessageEvent extends XmppEvent {
this.funReplacement, this.funReplacement,
this.funCancellation, this.funCancellation,
this.messageRetraction, this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
}); });
final StanzaError? error; final StanzaError? error;
final String body; final String body;
@@ -95,6 +101,10 @@ class MessageEvent extends XmppEvent {
final String? funCancellation; final String? funCancellation;
final bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? messageCorrectionId;
final MessageReactions? messageReactions;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId;
final Map<String, dynamic> other; final Map<String, dynamic> other;
} }

View File

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

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_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@@ -59,6 +60,12 @@ class StanzaHandlerData with _$StanzaHandlerData {
// If non-null, then it indicates the origin Id of the message that should be // If non-null, then it indicates the origin Id of the message that should be
// retracted // retracted
MessageRetractionData? messageRetraction, 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; ) = _StanzaHandlerData;
} }

View File

@@ -58,7 +58,12 @@ mixin _$StanzaHandlerData {
throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be
// retracted // retracted
MessageRetractionData? get messageRetraction => MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
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) @JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith => $StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@@ -92,7 +97,10 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction}); MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -128,6 +136,9 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
done: done == freezed done: done == freezed
@@ -218,6 +229,18 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.messageRetraction ? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable : messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?, as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _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?,
)); ));
} }
} }
@@ -251,7 +274,10 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction}); MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -289,6 +315,9 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_$_StanzaHandlerData( return _then(_$_StanzaHandlerData(
done == freezed done == freezed
@@ -379,6 +408,18 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.messageRetraction ? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable : messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?, as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _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?,
)); ));
} }
} }
@@ -404,7 +445,10 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.encryptionType, this.encryptionType,
this.delayedDelivery, this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{}, final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction}) this.messageRetraction,
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId})
: _other = other; : _other = other;
// Indicates to the runner that processing is now done. This means that all // Indicates to the runner that processing is now done. This means that all
@@ -484,10 +528,19 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
// retracted // retracted
@override @override
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
// 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 @override
String toString() { 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)'; 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 @override
@@ -525,7 +578,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
.equals(other.delayedDelivery, delayedDelivery) && .equals(other.delayedDelivery, delayedDelivery) &&
const DeepCollectionEquality().equals(other._other, this._other) && const DeepCollectionEquality().equals(other._other, this._other) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction)); .equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions) &&
const DeepCollectionEquality()
.equals(other.stickerPackId, stickerPackId));
} }
@override @override
@@ -552,7 +611,10 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(encryptionType), const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery), const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other), const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction) const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]); ]);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -582,7 +644,10 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final ExplicitEncryptionType? encryptionType, final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery, final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other, final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction}) = _$_StanzaHandlerData; final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions,
final String? stickerPackId}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all @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. // pre-processing is done and no other handlers should be consulted.
@@ -635,6 +700,12 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
@override // If non-null, then it indicates the origin Id of the message that should be @override // If non-null, then it indicates the origin Id of the message that should be
// retracted // retracted
MessageRetractionData? get messageRetraction; 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 @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>

View File

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

View File

@@ -11,13 +11,21 @@ import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart'; import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0184.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_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0424.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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0448.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 { class MessageDetails {
const MessageDetails({ const MessageDetails({
required this.to, required this.to,
@@ -36,6 +44,11 @@ class MessageDetails {
this.funCancellation, this.funCancellation,
this.shouldEncrypt = false, this.shouldEncrypt = false,
this.messageRetraction, this.messageRetraction,
this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
}); });
final String to; final String to;
final String? body; final String? body;
@@ -53,6 +66,11 @@ class MessageDetails {
final String? funCancellation; final String? funCancellation;
final bool shouldEncrypt; final bool shouldEncrypt;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
@@ -78,6 +96,11 @@ class MessageManager extends XmppManagerBase {
final message = state.stanza; final message = state.stanza;
final body = message.firstTag('body'); 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( getAttributes().sendEvent(MessageEvent(
body: body != null ? body.innerText() : '', body: body != null ? body.innerText() : '',
fromJid: JID.fromString(message.attributes['from']! as String), fromJid: JID.fromString(message.attributes['from']! as String),
@@ -98,6 +121,12 @@ class MessageManager extends XmppManagerBase {
funCancellation: state.funCancellation, funCancellation: state.funCancellation,
encrypted: state.encrypted, encrypted: state.encrypted,
messageRetraction: state.messageRetraction, messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions,
messageProcessingHints: hints.isEmpty ?
null :
hints,
stickerPackId: state.stickerPackId,
other: state.other, other: state.other,
error: StanzaError.fromStanza(message), error: StanzaError.fromStanza(message),
),); ),);
@@ -155,7 +184,7 @@ class MessageManager extends XmppManagerBase {
); );
} else { } else {
var body = details.body; var body = details.body;
if (details.sfs != null) { if (details.sfs != null && details.setOOBFallbackBody) {
// TODO(Unknown): Maybe find a better solution // TODO(Unknown): Maybe find a better solution
final firstSource = details.sfs!.sources.first; final firstSource = details.sfs!.sources.first;
if (firstSource is StatelessFileSharingUrlSource) { if (firstSource is StatelessFileSharingUrlSource) {
@@ -167,9 +196,11 @@ class MessageManager extends XmppManagerBase {
body = details.messageRetraction!.fallback; body = details.messageRetraction!.fallback;
} }
stanza.addChild( if (body != null) {
XMLNode(tag: 'body', text: body), stanza.addChild(
); XMLNode(tag: 'body', text: body),
);
}
} }
if (details.requestDeliveryReceipt) { if (details.requestDeliveryReceipt) {
@@ -186,7 +217,7 @@ class MessageManager extends XmppManagerBase {
stanza.addChild(details.sfs!.toXML()); stanza.addChild(details.sfs!.toXML());
final source = details.sfs!.sources.first; final source = details.sfs!.sources.first;
if (source is StatelessFileSharingUrlSource) { if (source is StatelessFileSharingUrlSource && details.setOOBFallbackBody) {
// SFS recommends OOB as a fallback // SFS recommends OOB as a fallback
stanza.addChild(constructOOBNode(OOBData(url: source.url))); stanza.addChild(constructOOBNode(OOBData(url: source.url)));
} }
@@ -249,6 +280,36 @@ class MessageManager extends XmppManagerBase {
); );
} }
} }
if (details.lastMessageCorrectionId != null) {
stanza.addChild(
makeLastMessageCorrectionEdit(
details.lastMessageCorrectionId!,
),
);
}
if (details.messageReactions != null) {
stanza.addChild(details.messageReactions!.toXml());
}
if (details.messageProcessingHints != null) {
for (final hint in details.messageProcessingHints!) {
stanza.addChild(hint.toXml());
}
}
if (details.stickerPackId != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'sticker',
xmlns: stickersXmlns,
attributes: {
'pack': details.stickerPackId!,
},
),
);
}
getAttributes().sendStanza(stanza, awaitable: false); getAttributes().sendStanza(stanza, awaitable: false);
} }

View File

@@ -76,6 +76,9 @@ const hashSha3512 = 'sha3-512';
const hashBlake2b256 = 'blake2b-256'; const hashBlake2b256 = 'blake2b-256';
const hashBlake2b512 = 'blake2b-512'; const hashBlake2b512 = 'blake2b-512';
// XEP-0308
const lmcXmlns = 'urn:xmpp:message-correct:0';
// XEP-0333 // XEP-0333
const chatMarkersXmlns = 'urn:xmpp:chat-markers:0'; const chatMarkersXmlns = 'urn:xmpp:chat-markers:0';
@@ -120,9 +123,12 @@ const fasteningXmlns = 'urn:xmpp:fasten:0';
// XEP-0424 // XEP-0424
const messageRetractionXmlns = 'urn:xmpp:message-retract:0'; const messageRetractionXmlns = 'urn:xmpp:message-retract:0';
// XEp-0428 // XEP-0428
const fallbackIndicationXmlns = 'urn:xmpp:fallback:0'; const fallbackIndicationXmlns = 'urn:xmpp:fallback:0';
// XEP-0444
const messageReactionsXmlns = 'urn:xmpp:reactions:0';
// XEP-0446 // XEP-0446
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';
@@ -135,6 +141,9 @@ const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopad
const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0';
const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'; const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0';
// XEP-0449
const stickersXmlns = 'urn:xmpp:stickers:0';
// XEP-0461 // XEP-0461
const replyXmlns = 'urn:xmpp:reply:0'; const replyXmlns = 'urn:xmpp:reply:0';
const fallbackXmlns = 'urn:xmpp:feature-fallback:0'; const fallbackXmlns = 'urn:xmpp:feature-fallback:0';

View File

@@ -24,3 +24,28 @@ int ioctetSortComparator(String a, String b) {
return 1; 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

@@ -129,6 +129,12 @@ class XMLNode {
}).toList(); }).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 "". /// Returns the inner text of the node. If none is set, returns the "".
String innerText() { String innerText() {
return text ?? ''; 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]. /// 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); final cacheKey = DiscoCacheKey(entity, node);
DiscoInfo? info; DiscoInfo? info;
Completer<Result<DiscoError, DiscoInfo>>? completer; Completer<Result<DiscoError, DiscoInfo>>? completer;
@@ -316,6 +316,7 @@ class DiscoManager extends XmppManagerBase {
final stanza = await getAttributes().sendStanza( final stanza = await getAttributes().sendStanza(
buildDiscoInfoQueryStanza(entity, node), buildDiscoInfoQueryStanza(entity, node),
encrypted: !shouldEncrypt,
); );
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) { 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]. /// 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() 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'); final query = stanza.firstTag('query');
if (query == null) return Result(InvalidResponseDiscoError()); 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/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.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 { class VCardPhoto {
const VCardPhoto({ this.binval }); const VCardPhoto({ this.binval });
final String? binval; final String? binval;
} }
class VCard { class VCard {
const VCard({ this.nickname, this.url, this.photo }); const VCard({ this.nickname, this.url, this.photo });
final String? nickname; final String? nickname;
final String? url; final String? url;
@@ -23,7 +28,6 @@ class VCard {
} }
class VCardManager extends XmppManagerBase { class VCardManager extends XmppManagerBase {
VCardManager() : _lastHash = {}, super(); VCardManager() : _lastHash = {}, super();
final Map<String, String> _lastHash; final Map<String, String> _lastHash;
@@ -59,12 +63,18 @@ class VCardManager extends XmppManagerBase {
final lastHash = _lastHash[from]; final lastHash = _lastHash[from];
if (lastHash != hash) { if (lastHash != hash) {
_lastHash[from] = hash; _lastHash[from] = hash;
final vcard = await requestVCard(from); final vcardResult = await requestVCard(from);
if (vcard != null) { if (vcardResult.isType<VCard>()) {
final binval = vcard.photo?.binval; final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) { if (binval != null) {
getAttributes().sendEvent(AvatarUpdatedEvent(jid: from, base64: binval, hash: hash)); getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
);
} else { } else {
logger.warning('No avatar data found'); 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( final result = await getAttributes().sendStanza(
Stanza.iq( Stanza.iq(
to: jid, 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); 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'; import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
class PubSubPublishOptions { class PubSubPublishOptions {
const PubSubPublishOptions({ const PubSubPublishOptions({
this.accessModel, this.accessModel,
this.maxItems, this.maxItems,
@@ -60,7 +59,6 @@ class PubSubPublishOptions {
} }
class PubSubItem { class PubSubItem {
const PubSubItem({ required this.id, required this.node, required this.payload }); const PubSubItem({ required this.id, required this.node, required this.payload });
final String id; final String id;
final String node; 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/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.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_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.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_0060/xep_0060.dart';
class UserAvatar { abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {}
class UserAvatar {
const UserAvatar({ required this.base64, required this.hash }); const UserAvatar({ required this.base64, required this.hash });
final String base64; final String base64;
final String hash; final String hash;
} }
class UserAvatarMetadata { class UserAvatarMetadata {
const UserAvatarMetadata( const UserAvatarMetadata(
this.id, this.id,
this.length, this.length,
@@ -62,30 +65,30 @@ class UserAvatarManager extends XmppManagerBase {
// TODO(PapaTutuWawa): Check for PEP support // TODO(PapaTutuWawa): Check for PEP support
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
/// Requests the avatar from [jid]. Returns the avatar data if the request was /// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise /// successful. Null otherwise
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
Future<UserAvatar?> getUserAvatar(String jid) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns); 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>>(); final results = resultsRaw.get<List<PubSubItem>>();
if (results.isEmpty) return null; if (results.isEmpty) return Result(UnknownAvatarError());
final item = results[0]; final item = results[0];
return UserAvatar( return Result(
base64: item.payload.innerText(), UserAvatar(
hash: item.id, base64: item.payload.innerText(),
hash: item.id,
),
); );
} }
/// Publish the avatar data, [base64], on the pubsub node using [hash] as /// 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 /// 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. /// [base64] must be the base64-encoded version of the image data.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> publishUserAvatar(String base64, String hash, bool public) async {
Future<bool> publishUserAvatar(String base64, String hash, bool public) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare().toString(),
@@ -101,14 +104,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] /// 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, /// 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. /// then the node will be set to an 'roster' access model.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async {
Future<bool> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare().toString(),
@@ -135,39 +139,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]. /// Subscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> subscribe(String jid) async {
Future<bool> subscribe(String jid) async {
await _getPubSubManager().subscribe(jid, userAvatarDataXmlns); await _getPubSubManager().subscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true; return const Result(true);
} }
/// Unsubscribe the data and metadata node of [jid]. /// Unsubscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> unsubscribe(String jid) async {
Future<bool> unsubscribe(String jid) async {
await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns); await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true; return const Result(true);
} }
/// Returns the PubSub Id of an avatar after doing a disco#items query. /// 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 /// Note that this assumes that there is only one (1) item published on
/// the node. /// the node.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, String>> getAvatarId(String jid) async {
Future<String?> getAvatarId(String jid) async {
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager; final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns); final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns, shouldEncrypt: false);
if (response.isType<DiscoError>()) return null; if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
final items = response.get<List<DiscoItem>>(); 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

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

View File

@@ -0,0 +1,50 @@
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';
XMLNode makeLastMessageCorrectionEdit(String id) {
return XMLNode.xmlns(
tag: 'replace',
xmlns: lmcXmlns,
attributes: <String, String>{
'id': id,
},
);
}
class LastMessageCorrectionManager extends XmppManagerBase {
@override
String getName() => 'LastMessageCorrectionManager';
@override
String getId() => lastMessageCorrectionManager;
@override
List<String> getDiscoFeatures() => [ lmcXmlns ];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'replace',
tagXmlns: lmcXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
)
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async {
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, store,
} }
/// NOTE: We do not define a function for turning a Message Processing Hint element into MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
/// an enum value since the elements do not concern us as a client. 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 { extension XmlExtension on MessageProcessingHint {
XMLNode toXml() { XMLNode toXml() {
String tag; String tag;

View File

@@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -25,7 +23,6 @@ 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/helpers.dart';
import 'package:moxxmpp/src/xeps/xep_0384/types.dart'; import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
import 'package:synchronized/synchronized.dart';
const _doNotEncryptList = [ const _doNotEncryptList = [
// XEP-0033 // XEP-0033
@@ -43,17 +40,7 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns), DoNotEncrypt('stanza-id', stableIdXmlns),
]; ];
abstract class OmemoManager extends XmppManagerBase { abstract class BaseOmemoManager 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 = {};
@override @override
String getId() => omemoManager; String getId() => omemoManager;
@@ -127,60 +114,31 @@ abstract class OmemoManager extends XmppManagerBase {
} else { } else {
// Someone published to their device list node // Someone published to their device list node
logger.finest('Got devices $ids'); logger.finest('Got devices $ids');
_deviceMap[jid] = ids;
} }
// Tell the OmemoManager
(await getOmemoManager())
.onDeviceListUpdate(jid.toString(), ids);
// Generate an event // Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
} }
} }
@visibleForOverriding @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. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async { Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
final session = await getSessionManager();
return session.getDeviceId();
}
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async { Future<OmemoBundle> _getDeviceBundle() async {
final session = await getSessionManager(); final om = await getOmemoManager();
return session.getDeviceBundle(); 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 /// Determines what child elements of a stanza should be encrypted. If shouldEncrypt
/// returns true for [element], then [element] will be encrypted. If shouldEncrypt /// returns true for [element], then [element] will be encrypted. If shouldEncrypt
/// returns false, then [element] won't be encrypted. /// returns false, then [element] won't be encrypted.
@@ -205,56 +163,51 @@ abstract class OmemoManager extends XmppManagerBase {
/// an attached payload, if [children] is not null, or an empty OMEMO message if /// 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 /// [children] is null. This function takes care of creating the affix elements as
/// specified by both XEP-0420 and XEP-0384. /// specified by both XEP-0420 and XEP-0384.
/// [jids] is the list of JIDs the payload should be encrypted for. /// [toJid] 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 { String _buildEnvelope(List<XMLNode> children, String toJid) {
XMLNode? payload; final payload = XMLNode.xmlns(
if (children != null) { tag: 'envelope',
payload = XMLNode.xmlns( xmlns: sceXmlns,
tag: 'envelope', children: [
xmlns: sceXmlns, XMLNode(
children: [ tag: 'content',
XMLNode( children: children,
tag: 'content', ),
children: children,
),
XMLNode( XMLNode(
tag: 'rpad', tag: 'rpad',
text: generateRpad(), text: generateRpad(),
), ),
XMLNode( XMLNode(
tag: 'to', tag: 'to',
attributes: <String, String>{ attributes: <String, String>{
'jid': toJid, 'jid': toJid,
}, },
), ),
XMLNode( XMLNode(
tag: 'from', tag: 'from',
attributes: <String, String>{ attributes: <String, String>{
'jid': getAttributes().getFullJID().toString(), 'jid': getAttributes().getFullJID().toString(),
}, },
), ),
/* /*
XMLNode( XMLNode(
tag: 'time', tag: 'time',
// TODO(Unknown): Implement // TODO(Unknown): Implement
attributes: <String, String>{ attributes: <String, String>{
'stamp': '', 'stamp': '',
}, },
), ),
*/ */
], ],
);
}
final encryptedEnvelope = await _encryptToJids(
jids,
payload?.toXml(),
newSessions: newSessions,
); );
return payload.toXml();
}
XMLNode _buildEncryptedElement(EncryptionResult result, String recipientJid, int deviceId) {
final keyElements = <String, List<XMLNode>>{}; final keyElements = <String, List<XMLNode>>{};
for (final key in encryptedEnvelope.encryptedKeys) { for (final key in result.encryptedKeys) {
final keyElement = XMLNode( final keyElement = XMLNode(
tag: 'key', tag: 'key',
attributes: <String, String>{ attributes: <String, String>{
@@ -282,11 +235,11 @@ abstract class OmemoManager extends XmppManagerBase {
}).toList(); }).toList();
var payloadElement = <XMLNode>[]; var payloadElement = <XMLNode>[];
if (payload != null) { if (result.ciphertext != null) {
payloadElement = [ payloadElement = [
XMLNode( XMLNode(
tag: 'payload', tag: 'payload',
text: base64.encode(encryptedEnvelope.ciphertext!), text: base64.encode(result.ciphertext!),
), ),
]; ];
} }
@@ -299,7 +252,7 @@ abstract class OmemoManager extends XmppManagerBase {
XMLNode( XMLNode(
tag: 'header', tag: 'header',
attributes: <String, String>{ attributes: <String, String>{
'sid': (await _getDeviceId()).toString(), 'sid': deviceId.toString(),
}, },
children: keysElements, children: keysElements,
), ),
@@ -307,136 +260,18 @@ abstract class OmemoManager extends XmppManagerBase {
); );
} }
/// A logging wrapper around acking the ratchet with [jid] with identifier [deviceId]. /// For usage with omemo_dart's OmemoManager.
Future<void> _ackRatchet(String jid, int deviceId) async { Future<void> sendEmptyMessageImpl(EncryptionResult result, String toJid) 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,
);
await getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.message( Stanza.message(
to: toJid.toString(), to: toJid,
type: 'chat', type: 'chat',
children: [ children: [
empty, _buildEncryptedElement(
result,
toJid,
await _getDeviceId(),
),
// Add a storage hint in case this is a message // Add a storage hint in case this is a message
// Taken from the example at // Taken from the example at
@@ -447,10 +282,28 @@ abstract class OmemoManager extends XmppManagerBase {
awaitable: false, awaitable: false,
encrypted: true, encrypted: true,
); );
}
if (!calledFromCriticalSection) { /// Send a heartbeat message to [jid].
await _handlerExit(toJid); 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 { Future<StanzaHandlerData> _onOutgoingStanza(Stanza stanza, StanzaHandlerData state) async {
@@ -461,6 +314,7 @@ abstract class OmemoManager extends XmppManagerBase {
if (stanza.to == null) { if (stanza.to == null) {
// We cannot encrypt in this case. // We cannot encrypt in this case.
logger.finest('Not encrypting since stanza.to is null');
return state; return state;
} }
@@ -471,34 +325,7 @@ abstract class OmemoManager extends XmppManagerBase {
} else { } else {
logger.finest('shouldEncryptStanza returned true for message to $toJid.'); 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 toEncrypt = List<XMLNode>.empty(growable: true);
final children = List<XMLNode>.empty(growable: true); final children = List<XMLNode>.empty(growable: true);
for (final child in stanza.children) { for (final child in stanza.children) {
@@ -508,76 +335,40 @@ abstract class OmemoManager extends XmppManagerBase {
toEncrypt.add(child); toEncrypt.add(child);
} }
} }
logger.finest('Beginning encryption');
final om = await getOmemoManager();
final result = await om.onOutgoingStanza(
OmemoOutgoingStanza(
[toJid.toString()],
_buildEnvelope(toEncrypt, toJid.toString()),
),
);
logger.finest('Encryption done');
final jidsToEncryptFor = <String>[JID.fromString(stanza.to!).toBare().toString()]; final encrypted = _buildEncryptedElement(
// Prevent encrypting to self if there is only one device (ours). result,
if (await _hasSessionWith(ownJid.toString())) { toJid.toString(),
jidsToEncryptFor.add(ownJid.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());
} }
try { return state.copyWith(
logger.finest('Encrypting stanza'); stanza: state.stanza.copyWith(
final encrypted = await _encryptChildren( children: children,
toEncrypt, ),
jidsToEncryptFor, encrypted: true,
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);
return state.copyWith(
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',
); );
} }
@@ -587,48 +378,12 @@ abstract class OmemoManager extends XmppManagerBase {
@visibleForOverriding @visibleForOverriding
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza); 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 { Future<StanzaHandlerData> _onIncomingStanza(Stanza stanza, StanzaHandlerData state) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns); final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state; if (encrypted == null) return state;
if (stanza.from == null) return state; if (stanza.from == null) return state;
final fromJid = JID.fromString(stanza.from!).toBare(); final fromJid = JID.fromString(stanza.from!).toBare();
final completer = await _handlerEntry(fromJid);
if (completer != null) {
await completer.future;
}
final header = encrypted.firstTag('header')!; final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload'); final payloadElement = encrypted.firstTag('payload');
final keys = List<EncryptedKey>.empty(growable: true); final keys = List<EncryptedKey>.empty(growable: true);
@@ -649,118 +404,49 @@ abstract class OmemoManager extends XmppManagerBase {
final ourJid = getAttributes().getFullJID(); final ourJid = getAttributes().getFullJID();
final sid = int.parse(header.attributes['sid']! as String); 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 final om = await getOmemoManager();
// ensure that _deviceMap is up-to-date. final result = await om.onIncomingStanza(
final devices = _deviceMap[fromJid] ?? <int>[]; OmemoIncomingStanza(
if (!devices.contains(sid)) {
await getDeviceList(fromJid);
}
String? decrypted;
try {
decrypted = await _decryptMessage(
payloadElement != null ? base64.decode(payloadElement.innerText()) : null,
fromJid.toString(), fromJid.toString(),
sid, sid,
keys,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch, state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch,
); keys,
} catch (ex) { payloadElement?.innerText(),
logger.warning('Error occurred during message decryption: $ex'); ),
);
await _handlerExit(fromJid); final children = stanza.children.where(
return state.copyWith( (child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns,
other: { ).toList();
...state.other, final other = Map<String, dynamic>.from(state.other);
'encryption_error': ex, if (result.error != null) {
}, other['encryption_error'] = result.error;
);
} }
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); if (result.payload != null) {
await sendEmptyMessage(fromJid, calledFromCriticalSection: true); final envelope = XMLNode.fromString(result.payload!);
children.addAll(
envelope.firstTag('content')!.children,
);
final envelope = XMLNode.fromString(decrypted); if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
final children = stanza.children.where( other['encryption_error'] = InvalidAffixElementsException();
(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;
}
} else {
// The ratchet that decrypted the message was acked
if (decrypted != null) {
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 on acked ratchet. Doing nothing');
await _handlerExit(fromJid);
return state;
} }
} }
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 /// Convenience function that attempts to retrieve the raw XML payload from the
@@ -776,15 +462,12 @@ abstract class OmemoManager extends XmppManagerBase {
/// Retrieves the OMEMO device list from [jid]. /// Retrieves the OMEMO device list from [jid].
Future<Result<OmemoError, List<int>>> getDeviceList(JID jid) async { Future<Result<OmemoError, List<int>>> getDeviceList(JID jid) async {
if (_deviceMap.containsKey(jid)) return Result(_deviceMap[jid]);
final itemsRaw = await _retrieveDeviceListPayload(jid); final itemsRaw = await _retrieveDeviceListPayload(jid);
if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError()); if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError());
final ids = itemsRaw.get<XMLNode>().children final ids = itemsRaw.get<XMLNode>().children
.map((child) => int.parse(child.attributes['id']! as String)) .map((child) => int.parse(child.attributes['id']! as String))
.toList(); .toList();
_deviceMap[jid] = ids;
return Result(ids); return Result(ids);
} }
@@ -882,13 +565,9 @@ abstract class OmemoManager extends XmppManagerBase {
} }
/// Subscribes to the device list PubSub node of [jid]. /// 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 pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = await pm.subscribe(jid.toString(), omemoDevicesXmlns); await pm.subscribe(jid, omemoDevicesXmlns);
if (!result.isType<PubSubError>()) {
_subscriptionMap[jid] = true;
}
} }
/// Attempts to find out if [jid] supports omemo:2. /// 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'; import 'package:moxxmpp/src/xeps/xep_0300.dart';
class FileMetadataData { class FileMetadataData {
const FileMetadataData({ const FileMetadataData({
this.mediaType, this.mediaType,
this.width, this.width,

View File

@@ -18,7 +18,6 @@ abstract class StatelessFileSharingSource {
/// Implementation for url-data source elements. /// Implementation for url-data source elements.
class StatelessFileSharingUrlSource extends StatelessFileSharingSource { class StatelessFileSharingUrlSource extends StatelessFileSharingSource {
StatelessFileSharingUrlSource(this.url); StatelessFileSharingUrlSource(this.url);
factory StatelessFileSharingUrlSource.fromXml(XMLNode element) { 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); const StatelessFileSharingData(this.metadata, this.sources);
/// Parse [node] as a StatelessFileSharingData element. /// Parse [node] as a StatelessFileSharingData element.
@@ -50,20 +70,10 @@ class StatelessFileSharingData {
assert(node.attributes['xmlns'] == sfsXmlns, 'Invalid element xmlns'); assert(node.attributes['xmlns'] == sfsXmlns, 'Invalid element xmlns');
assert(node.tag == 'file-sharing', 'Invalid element name'); 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( return StatelessFileSharingData(
FileMetadataData.fromXML(node.firstTag('file')!), 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)!; final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!;
return state.copyWith( return state.copyWith(
sfs: StatelessFileSharingData.fromXML(sfs), sfs: StatelessFileSharingData.fromXML(sfs, ),
); );
} }
} }

View File

@@ -0,0 +1,308 @@
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].
///
/// On success, returns true. On failure, returns a PubSubError.
Future<Result<PubSubError, bool>> publishStickerPack(JID jid, StickerPack pack) 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: const PubSubPublishOptions(
maxItems: 'max',
),
);
}
/// 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

@@ -1,6 +1,6 @@
name: moxxmpp name: moxxmpp
description: A pure-Dart XMPP library description: A pure-Dart XMPP library
version: 0.1.5 version: 0.1.6+1
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -19,8 +19,8 @@ dependencies:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5 version: ^0.1.5
omemo_dart: omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.3.2 version: ^0.4.0
random_string: ^2.3.1 random_string: ^2.3.1
saslprep: ^1.0.2 saslprep: ^1.0.2
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2
@@ -31,6 +31,6 @@ dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.2+7 version: ^0.1.2+9
test: ^1.16.0 test: ^1.16.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1

View File

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

View File

@@ -14,3 +14,9 @@ is, for example, [moxdns](https://codeberg.org/moxxy/moxdns).
## License ## License
See `./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 name: moxxmpp_socket_tcp
description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368 description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368
version: 0.1.2+7 version: 0.1.2+9
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -12,7 +12,7 @@ dependencies:
meta: ^1.6.0 meta: ^1.6.0
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5 version: ^0.1.6+1
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0