16 Commits

Author SHA1 Message Date
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
33 changed files with 864 additions and 574 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.6 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+8 version: 0.1.2+9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

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

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';
@@ -75,6 +77,9 @@ class MessageEvent extends XmppEvent {
this.funCancellation, this.funCancellation,
this.messageRetraction, this.messageRetraction,
this.messageCorrectionId, this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
}); });
final StanzaError? error; final StanzaError? error;
final String body; final String body;
@@ -97,6 +102,9 @@ class MessageEvent extends XmppEvent {
final bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? messageCorrectionId; 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';
@@ -61,6 +62,10 @@ class StanzaHandlerData with _$StanzaHandlerData {
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
// If non-null, then the message is a correction for the specified stanza Id // If non-null, then the message is a correction for the specified stanza Id
String? lastMessageCorrectionSid, String? lastMessageCorrectionSid,
// Reactions data
MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to
String? stickerPackId,
} }
) = _StanzaHandlerData; ) = _StanzaHandlerData;
} }

View File

@@ -59,7 +59,11 @@ mixin _$StanzaHandlerData {
// retracted // retracted
MessageRetractionData? get messageRetraction => MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id 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) @JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith => $StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@@ -94,7 +98,9 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid}); String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -131,6 +137,8 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed, Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
done: done == freezed done: done == freezed
@@ -225,6 +233,14 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid ? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?, 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, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid}); String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -298,6 +316,8 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed, Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_$_StanzaHandlerData( return _then(_$_StanzaHandlerData(
done == freezed done == freezed
@@ -392,6 +412,14 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid ? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?, 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, 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.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
@@ -501,10 +531,16 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
// If non-null, then the message is a correction for the specified stanza Id // If non-null, then the message is a correction for the specified stanza Id
@override @override
final String? lastMessageCorrectionSid; 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, 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 @override
@@ -544,7 +580,11 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction) && .equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals( const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid)); other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions) &&
const DeepCollectionEquality()
.equals(other.stickerPackId, stickerPackId));
} }
@override @override
@@ -572,7 +612,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
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(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]); ]);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -603,7 +645,9 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final DelayedDelivery? delayedDelivery, final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other, final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction, 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 @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.
@@ -658,6 +702,10 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
MessageRetractionData? get messageRetraction; MessageRetractionData? get messageRetraction;
@override // If non-null, then the message is a correction for the specified stanza Id @override // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid; 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

@@ -26,3 +26,5 @@ 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 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_0184.dart';
import 'package:moxxmpp/src/xeps/xep_0308.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,
@@ -38,6 +45,10 @@ class MessageDetails {
this.shouldEncrypt = false, this.shouldEncrypt = false,
this.messageRetraction, this.messageRetraction,
this.lastMessageCorrectionId, this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
}); });
final String to; final String to;
final String? body; final String? body;
@@ -56,6 +67,10 @@ class MessageDetails {
final bool shouldEncrypt; final bool shouldEncrypt;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId; final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
@@ -81,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),
@@ -102,6 +122,11 @@ class MessageManager extends XmppManagerBase {
encrypted: state.encrypted, encrypted: state.encrypted,
messageRetraction: state.messageRetraction, messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid, 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),
),); ),);
@@ -159,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) {
@@ -171,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) {
@@ -190,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)));
} }
@@ -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); getAttributes().sendStanza(stanza, awaitable: false);
} }

View File

@@ -123,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';
@@ -138,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,
@@ -49,6 +52,14 @@ class UserAvatarManager extends XmppManagerBase {
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PubSubNotificationEvent) { 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( getAttributes().sendEvent(
AvatarUpdatedEvent( AvatarUpdatedEvent(
jid: event.from, jid: event.from,
@@ -62,30 +73,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 +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] /// 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 +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]. /// 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

@@ -13,11 +13,10 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart'; import 'package:moxxmpp/src/xeps/xep_0297.dart';
class CarbonsManager extends XmppManagerBase { class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super();
CarbonsManager() : _isEnabled = false, _supported = false, _gotSupported = false, super(); bool _isEnabled = false;
bool _isEnabled; bool _supported = false;
bool _supported; bool _gotSupported = false;
bool _gotSupported;
@override @override
String getId() => carbonsManager; String getId() => carbonsManager;
@@ -159,6 +158,9 @@ class CarbonsManager extends XmppManagerBase {
return true; return true;
} }
/// True if Message Carbons are enabled. False, if not.
bool get isEnabled => _isEnabled;
@visibleForTesting @visibleForTesting
void forceEnable() { void forceEnable() {
_isEnabled = true; _isEnabled = true;

View File

@@ -30,8 +30,8 @@ class LastMessageCorrectionManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
tagName: 'reply', tagName: 'replace',
tagXmlns: replyXmlns, tagXmlns: lmcXmlns,
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
@@ -42,9 +42,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns); final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
if (edit == null) return state;
return state.copyWith( return state.copyWith(
lastMessageCorrectionSid: edit.attributes['id']! as String, 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';
@@ -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_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';
import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart'; import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart'; import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
@@ -25,7 +24,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 +41,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 +115,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 +164,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 +236,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 +253,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 +261,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 +283,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 +315,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 +326,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 +336,56 @@ abstract class OmemoManager extends XmppManagerBase {
toEncrypt.add(child); 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()]; if (!result.isSuccess(2)) {
// Prevent encrypting to self if there is only one device (ours). final other = Map<String, dynamic>.from(state.other);
if (await _hasSessionWith(ownJid.toString())) { other['encryption_error_jids'] = result.jidEncryptionErrors;
jidsToEncryptFor.add(ownJid.toString()); other['encryption_error_devices'] = result.deviceEncryptionErrors;
}
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);
return state.copyWith( return state.copyWith(
other: other,
cancel: true, cancel: true,
cancelReason: EncryptionFailedException(),
other: {
...state.other,
'encryption_error': ex,
},
); );
} }
}
final encrypted = _buildEncryptedElement(
/// This function returns true if the encryption scheme should ignore unacked ratchets result,
/// and don't try to build a new ratchet even though there are unacked ones. toJid.toString(),
/// The current logic is that chat states with no body ignore the "ack" state of the await _getDeviceId(),
/// ratchets. );
/// children.add(encrypted);
/// 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 // Only add message specific metadata when actually sending a message
/// not contain a <body /> element. if (stanza.tag == 'message') {
@visibleForOverriding children
bool shouldIgnoreUnackedRatchets(List<XMLNode> children) { // Add EME data
return listContains( ..add(buildEmeElement(ExplicitEncryptionType.omemo2))
children, // Add a storage hint in case this is a message
(XMLNode child) { // Taken from the example at
return child.attributes['xmlns'] == chatStateXmlns || child.attributes['xmlns'] == chatMarkersXmlns; // https://xmpp.org/extensions/xep-0384.html#message-structure-description.
}, ..add(MessageProcessingHint.store.toXml());
) && !listContains( }
children,
(XMLNode child) => child.tag == 'body', return state.copyWith(
stanza: state.stanza.copyWith(
children: children,
),
encrypted: true,
); );
} }
@@ -587,48 +395,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 +421,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 +479,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 +582,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,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'; import 'package:moxxmpp/src/stanza.dart';
class ReplyData { class ReplyData {
const ReplyData({ const ReplyData({
required this.to, required this.to,
required this.id, required this.id,
this.start, this.start,
this.end, this.end,
}); });
final String to; final String to;
final String id; final String id;

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.6 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.1
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+8 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,7 @@
## 0.1.2+9
- Update a dependency to the latest release.
## 0.1.2+8 ## 0.1.2+8
- 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+8 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.6 version: ^0.1.6+1
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0