Compare commits

...

19 Commits

Author SHA1 Message Date
327f695a40 Merge pull request 'Replace StanzaHandlerData with something more extensible' (#42) from feat/type-map-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/42
2023-06-07 21:44:28 +00:00
8266765ff8 feat(core): Add encryption info to MessageEvent 2023-06-07 23:42:05 +02:00
e234c812ff feat(core): Add a shorthand for MessageEvent.extensions.get 2023-06-07 21:11:11 +02:00
4ff2992a03 fix(all): Last minute fixes 2023-06-07 21:04:05 +02:00
09fd5845aa feat(core): Re-fix the subscription functions 2023-06-07 18:54:05 +02:00
963f3f2cd9 feat(core): Check if we can do subscription pre-approval 2023-06-07 18:41:08 +02:00
da1d28a6d6 feat(core): Remove the PresenceReceivedEvent 2023-06-07 15:12:34 +02:00
cbd90b1163 feat(core): Add a list constructor to TypedMap 2023-06-07 14:13:46 +02:00
f0538b0447 feat(xep): Write a test for XEP-0334 2023-06-06 21:46:33 +02:00
968604b0ba fix(tests): Fix SFS parsing test 2023-06-06 21:31:14 +02:00
60279a84e0 feat(all): Restrict extensions to StanzaHandlerExtension 2023-06-06 21:19:42 +02:00
cf3287ccf4 feat(core): Remove freezed artifact 2023-06-06 20:26:35 +02:00
1ab0ed856f feat(all): Bump version to 0.4.0 2023-06-06 20:22:13 +02:00
10a5046431 feat(core): Rework [MessageEvent] 2023-06-06 16:19:16 +02:00
4d76b9f57a feat(core): Remove [MessageDetails] 2023-06-06 15:57:55 +02:00
0ec3777f44 feat(xep): Managers register the sending callback 2023-06-06 15:14:19 +02:00
6f5de9c4dc feat(xep): Implement the message sending callbacks 2023-06-06 14:12:49 +02:00
79d7e3ba64 feat(all): Move all managers to the new data system 2023-06-04 21:53:47 +02:00
8270185027 feat(core): Implement a typed map 2023-06-03 00:41:23 +02:00
54 changed files with 2217 additions and 1886 deletions

View File

@ -1,4 +1,4 @@
## 0.3.2
## 0.4.0
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
@ -11,6 +11,11 @@
- **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`.
- **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument.
- Sent stanzas are now kept in a queue until sent.
- **BREAKING**: `MessageManager.sendMessage` does not use `MessageDetails` anymore. Instead, use `TypedMap`.
- `MessageManager` now allows registering callbacks for adding data whenever a message is sent.
- **BREAKING**: `MessageEvent` now makes use of `TypedMap`.
- **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead.
- **BREAKING**: `ChatState.toString()` is now `ChatState.toName()`
## 0.3.1

View File

@ -18,7 +18,6 @@ export 'package:moxxmpp/src/managers/namespaces.dart';
export 'package:moxxmpp/src/managers/priorities.dart';
export 'package:moxxmpp/src/message.dart';
export 'package:moxxmpp/src/namespaces.dart';
export 'package:moxxmpp/src/negotiators/manager.dart';
export 'package:moxxmpp/src/negotiators/namespaces.dart';
export 'package:moxxmpp/src/negotiators/negotiator.dart';
export 'package:moxxmpp/src/ping.dart';
@ -40,6 +39,7 @@ export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.dart';
export 'package:moxxmpp/src/util/typed_map.dart';
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
export 'package:moxxmpp/src/xeps/staging/fast.dart';
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';

View File

@ -27,7 +27,9 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/queue.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart';
@ -474,8 +476,8 @@ class XmppConnection {
initial: StanzaHandlerData(
false,
false,
null,
newStanza,
TypedMap(),
encrypted: details.encrypted,
forceEncryption: details.forceEncryption,
),
@ -531,14 +533,15 @@ class XmppConnection {
// Run post-send handlers
_log.fine('Running post stanza handlers..');
final extensions = TypedMap<StanzaHandlerExtension>()
..set(StreamManagementData(details.excludeFromStreamManagement));
await _runOutgoingPostStanzaHandlers(
newStanza,
initial: StanzaHandlerData(
false,
false,
null,
newStanza,
excludeFromStreamManagement: details.excludeFromStreamManagement,
extensions,
),
);
_log.fine('Done');
@ -653,7 +656,7 @@ class XmppConnection {
Stanza stanza, {
StanzaHandlerData? initial,
}) async {
var state = initial ?? StanzaHandlerData(false, false, null, stanza);
var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap());
for (final handler in handlers) {
if (handler.matches(state.stanza)) {
state = await handler.callback(state.stanza, state);
@ -728,7 +731,7 @@ class XmppConnection {
// it.
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted &&
incomingPreHandlers.other['encryption_error'] == null
incomingPreHandlers.encryptionError == null
? '(Encrypted) '
: '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
@ -747,10 +750,10 @@ class XmppConnection {
initial: StanzaHandlerData(
false,
incomingPreHandlers.cancel,
incomingPreHandlers.cancelReason,
incomingPreHandlers.stanza,
incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted,
other: incomingPreHandlers.other,
cancelReason: incomingPreHandlers.cancelReason,
),
);
if (!incomingHandlers.done) {

View File

@ -4,19 +4,11 @@ import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0084.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
import 'package:moxxmpp/src/xeps/xep_0333.dart';
abstract class XmppEvent {}
@ -75,60 +67,42 @@ class RosterUpdatedEvent extends XmppEvent {
/// Triggered when a message is received
class MessageEvent extends XmppEvent {
MessageEvent({
required this.body,
required this.fromJid,
required this.toJid,
required this.sid,
required this.isCarbon,
required this.deliveryReceiptRequested,
required this.isMarkable,
required this.encrypted,
required this.other,
this.originId,
this.stanzaIds,
this.error,
MessageEvent(
this.from,
this.to,
this.id,
this.encrypted,
this.extensions, {
this.type,
this.oob,
this.sfs,
this.sims,
this.reply,
this.chatState,
this.fun,
this.funReplacement,
this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.error,
this.encryptionError,
});
final StanzaError? error;
final String body;
final JID fromJid;
final JID toJid;
final String sid;
/// The from attribute of the message.
final JID from;
/// The to attribute of the message.
final JID to;
/// The id attribute of the message.
final String id;
/// The type attribute of the message.
final String? type;
final String? originId;
final List<StanzaId>? stanzaIds;
final bool isCarbon;
final bool deliveryReceiptRequested;
final bool isMarkable;
final OOBData? oob;
final StatelessFileSharingData? sfs;
final StatelessMediaSharingData? sims;
final ReplyData? reply;
final ChatState? chatState;
final FileMetadataData? fun;
final String? funReplacement;
final String? funCancellation;
final StanzaError? error;
/// Flag indicating whether the message was encrypted.
final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId;
final MessageReactions? messageReactions;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId;
final Map<String, dynamic> other;
/// The error in case an encryption error occurred.
final Object? encryptionError;
/// Data added by other handlers.
final TypedMap<StanzaHandlerExtension> extensions;
/// Shorthand for extensions.get<T>().
T? get<T>() => extensions.get<T>();
}
/// Triggered when a client responds to our delivery receipt request
@ -139,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent {
}
class ChatMarkerEvent extends XmppEvent {
ChatMarkerEvent({
required this.type,
required this.from,
required this.id,
});
ChatMarkerEvent(
this.from,
this.type,
this.id,
);
/// The entity that sent the chat marker.
final JID from;
final String type;
/// The type of chat marker that was sent.
final ChatMarker type;
/// The id of the message that the marker applies to.
final String id;
}
@ -169,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent {
final String resource;
}
/// Triggered when we receive presence
class PresenceReceivedEvent extends XmppEvent {
PresenceReceivedEvent(this.jid, this.presence);
final JID jid;
final Stanza presence;
}
/// Triggered when we are starting an connection attempt
class ConnectingEvent extends XmppEvent {}

View File

@ -1,82 +1,47 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
part 'data.freezed.dart';
abstract class StanzaHandlerExtension {}
@freezed
class StanzaHandlerData with _$StanzaHandlerData {
factory StanzaHandlerData(
// Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
bool done,
// Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
bool cancel,
// The reason why we cancelled the processing and sending
dynamic cancelReason,
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
Stanza stanza, {
// Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
@Default(false) bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
class StanzaHandlerData {
StanzaHandlerData(
this.done,
this.cancel,
this.stanza,
this.extensions, {
this.cancelReason,
this.encryptionError,
this.encrypted = false,
this.forceEncryption = false,
});
// XEP-0359 <origin-id />'s id attribute, if available.
String? originId,
/// Indicates to the runner that processing is now done. This means that all
/// pre-processing is done and no other handlers should be consulted.
bool done;
/// Indicates to the runner that processing is to be cancelled and no further handlers
/// should run. The stanza also will not be sent.
bool cancel;
/// The reason why we cancelled the processing and sending.
Object? cancelReason;
/// The reason why an encryption or decryption failed.
Object? encryptionError;
/// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is
/// absolutely necessary, e.g. with Message Carbons or OMEMO.
Stanza stanza;
/// Whether the stanza was received encrypted
bool encrypted;
// XEP-0359 <stanza-id /> elements, if available.
List<StanzaId>? stanzaIds,
ReplyData? reply,
ChatState? chatState,
@Default(false) bool isCarbon,
@Default(false) bool deliveryReceiptRequested,
@Default(false) bool isMarkable,
// File Upload Notifications
// A notification
FileMetadataData? fun,
// The stanza id this replaces
String? funReplacement,
// The stanza id this cancels
String? funCancellation,
// Whether the stanza was received encrypted
@Default(false) bool encrypted,
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@Default(false) bool forceEncryption,
// The stated type of encryption used, if any was used
ExplicitEncryptionType? encryptionType,
// Delayed Delivery
DelayedDelivery? delayedDelivery,
// This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
@Default(<String, dynamic>{}) Map<String, dynamic> other,
// If non-null, then it indicates the origin Id of the message that should be
// retracted
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,
// Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
@Default(false) bool excludeFromStreamManagement,
}) = _StanzaHandlerData;
bool forceEncryption;
/// Additional data from other managers.
final TypedMap<StanzaHandlerExtension> extensions;
}

View File

@ -1,823 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'data.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$StanzaHandlerData {
// Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
bool get done =>
throw _privateConstructorUsedError; // Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
bool get cancel =>
throw _privateConstructorUsedError; // The reason why we cancelled the processing and sending
dynamic get cancelReason =>
throw _privateConstructorUsedError; // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
Stanza get stanza =>
throw _privateConstructorUsedError; // Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
bool get retransmitted => throw _privateConstructorUsedError;
StatelessMediaSharingData? get sims => throw _privateConstructorUsedError;
StatelessFileSharingData? get sfs => throw _privateConstructorUsedError;
OOBData? get oob =>
throw _privateConstructorUsedError; // XEP-0359 <origin-id />'s id attribute, if available.
String? get originId =>
throw _privateConstructorUsedError; // XEP-0359 <stanza-id /> elements, if available.
List<StanzaId>? get stanzaIds => throw _privateConstructorUsedError;
ReplyData? get reply => throw _privateConstructorUsedError;
ChatState? get chatState => throw _privateConstructorUsedError;
bool get isCarbon => throw _privateConstructorUsedError;
bool get deliveryReceiptRequested => throw _privateConstructorUsedError;
bool get isMarkable =>
throw _privateConstructorUsedError; // File Upload Notifications
// A notification
FileMetadataData? get fun =>
throw _privateConstructorUsedError; // The stanza id this replaces
String? get funReplacement =>
throw _privateConstructorUsedError; // The stanza id this cancels
String? get funCancellation =>
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
bool get encrypted =>
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption =>
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType =>
throw _privateConstructorUsedError; // Delayed Delivery
DelayedDelivery? get delayedDelivery =>
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
Map<String, dynamic> get other =>
throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be
// retracted
MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid =>
throw _privateConstructorUsedError; // Reactions data
MessageReactions? get messageReactions =>
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
String? get stickerPackId =>
throw _privateConstructorUsedError; // Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
bool get excludeFromStreamManagement => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StanzaHandlerDataCopyWith<$Res> {
factory $StanzaHandlerDataCopyWith(
StanzaHandlerData value, $Res Function(StanzaHandlerData) then) =
_$StanzaHandlerDataCopyWithImpl<$Res, StanzaHandlerData>;
@useResult
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
String? originId,
List<StanzaId>? stanzaIds,
ReplyData? reply,
ChatState? chatState,
bool isCarbon,
bool deliveryReceiptRequested,
bool isMarkable,
FileMetadataData? fun,
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId,
bool excludeFromStreamManagement});
}
/// @nodoc
class _$StanzaHandlerDataCopyWithImpl<$Res, $Val extends StanzaHandlerData>
implements $StanzaHandlerDataCopyWith<$Res> {
_$StanzaHandlerDataCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? done = null,
Object? cancel = null,
Object? cancelReason = freezed,
Object? stanza = null,
Object? retransmitted = null,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? originId = freezed,
Object? stanzaIds = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = null,
Object? deliveryReceiptRequested = null,
Object? isMarkable = null,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = null,
Object? forceEncryption = null,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = null,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
Object? excludeFromStreamManagement = null,
}) {
return _then(_value.copyWith(
done: null == done
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
cancel: null == cancel
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
cancelReason: freezed == cancelReason
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
stanza: null == stanza
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: null == retransmitted
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: freezed == sims
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: freezed == sfs
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: freezed == oob
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
originId: freezed == originId
? _value.originId
: originId // ignore: cast_nullable_to_non_nullable
as String?,
stanzaIds: freezed == stanzaIds
? _value.stanzaIds
: stanzaIds // ignore: cast_nullable_to_non_nullable
as List<StanzaId>?,
reply: freezed == reply
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: freezed == chatState
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: null == isCarbon
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: null == deliveryReceiptRequested
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: null == isMarkable
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: freezed == fun
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: freezed == funReplacement
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: freezed == funCancellation
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: null == encrypted
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: null == forceEncryption
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: freezed == encryptionType
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: freezed == delayedDelivery
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: null == other
? _value.other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: freezed == messageRetraction
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: freezed == lastMessageCorrectionSid
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: freezed == messageReactions
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: freezed == stickerPackId
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
excludeFromStreamManagement: null == excludeFromStreamManagement
? _value.excludeFromStreamManagement
: excludeFromStreamManagement // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StanzaHandlerDataCopyWith<$Res>
implements $StanzaHandlerDataCopyWith<$Res> {
factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value,
$Res Function(_$_StanzaHandlerData) then) =
__$$_StanzaHandlerDataCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
String? originId,
List<StanzaId>? stanzaIds,
ReplyData? reply,
ChatState? chatState,
bool isCarbon,
bool deliveryReceiptRequested,
bool isMarkable,
FileMetadataData? fun,
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId,
bool excludeFromStreamManagement});
}
/// @nodoc
class __$$_StanzaHandlerDataCopyWithImpl<$Res>
extends _$StanzaHandlerDataCopyWithImpl<$Res, _$_StanzaHandlerData>
implements _$$_StanzaHandlerDataCopyWith<$Res> {
__$$_StanzaHandlerDataCopyWithImpl(
_$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? done = null,
Object? cancel = null,
Object? cancelReason = freezed,
Object? stanza = null,
Object? retransmitted = null,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? originId = freezed,
Object? stanzaIds = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = null,
Object? deliveryReceiptRequested = null,
Object? isMarkable = null,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = null,
Object? forceEncryption = null,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = null,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
Object? excludeFromStreamManagement = null,
}) {
return _then(_$_StanzaHandlerData(
null == done
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
null == cancel
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
freezed == cancelReason
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
null == stanza
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: null == retransmitted
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: freezed == sims
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: freezed == sfs
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: freezed == oob
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
originId: freezed == originId
? _value.originId
: originId // ignore: cast_nullable_to_non_nullable
as String?,
stanzaIds: freezed == stanzaIds
? _value._stanzaIds
: stanzaIds // ignore: cast_nullable_to_non_nullable
as List<StanzaId>?,
reply: freezed == reply
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: freezed == chatState
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: null == isCarbon
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: null == deliveryReceiptRequested
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: null == isMarkable
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: freezed == fun
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: freezed == funReplacement
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: freezed == funCancellation
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: null == encrypted
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: null == forceEncryption
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: freezed == encryptionType
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: freezed == delayedDelivery
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: null == other
? _value._other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: freezed == messageRetraction
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: freezed == lastMessageCorrectionSid
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: freezed == messageReactions
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: freezed == stickerPackId
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
excludeFromStreamManagement: null == excludeFromStreamManagement
? _value.excludeFromStreamManagement
: excludeFromStreamManagement // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$_StanzaHandlerData implements _StanzaHandlerData {
_$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza,
{this.retransmitted = false,
this.sims,
this.sfs,
this.oob,
this.originId,
final List<StanzaId>? stanzaIds,
this.reply,
this.chatState,
this.isCarbon = false,
this.deliveryReceiptRequested = false,
this.isMarkable = false,
this.fun,
this.funReplacement,
this.funCancellation,
this.encrypted = false,
this.forceEncryption = false,
this.encryptionType,
this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction,
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId,
this.excludeFromStreamManagement = false})
: _stanzaIds = stanzaIds,
_other = other;
// Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
@override
final bool done;
// Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
@override
final bool cancel;
// The reason why we cancelled the processing and sending
@override
final dynamic cancelReason;
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
@override
final Stanza stanza;
// Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
@override
@JsonKey()
final bool retransmitted;
@override
final StatelessMediaSharingData? sims;
@override
final StatelessFileSharingData? sfs;
@override
final OOBData? oob;
// XEP-0359 <origin-id />'s id attribute, if available.
@override
final String? originId;
// XEP-0359 <stanza-id /> elements, if available.
final List<StanzaId>? _stanzaIds;
// XEP-0359 <stanza-id /> elements, if available.
@override
List<StanzaId>? get stanzaIds {
final value = _stanzaIds;
if (value == null) return null;
if (_stanzaIds is EqualUnmodifiableListView) return _stanzaIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final ReplyData? reply;
@override
final ChatState? chatState;
@override
@JsonKey()
final bool isCarbon;
@override
@JsonKey()
final bool deliveryReceiptRequested;
@override
@JsonKey()
final bool isMarkable;
// File Upload Notifications
// A notification
@override
final FileMetadataData? fun;
// The stanza id this replaces
@override
final String? funReplacement;
// The stanza id this cancels
@override
final String? funCancellation;
// Whether the stanza was received encrypted
@override
@JsonKey()
final bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@override
@JsonKey()
final bool forceEncryption;
// The stated type of encryption used, if any was used
@override
final ExplicitEncryptionType? encryptionType;
// Delayed Delivery
@override
final DelayedDelivery? delayedDelivery;
// This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
final Map<String, dynamic> _other;
// This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
@override
@JsonKey()
Map<String, dynamic> get other {
if (_other is EqualUnmodifiableMapView) return _other;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_other);
}
// If non-null, then it indicates the origin Id of the message that should be
// retracted
@override
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;
// Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
@override
@JsonKey()
final bool excludeFromStreamManagement;
@override
String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, originId: $originId, stanzaIds: $stanzaIds, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId, excludeFromStreamManagement: $excludeFromStreamManagement)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StanzaHandlerData &&
(identical(other.done, done) || other.done == done) &&
(identical(other.cancel, cancel) || other.cancel == cancel) &&
const DeepCollectionEquality()
.equals(other.cancelReason, cancelReason) &&
(identical(other.stanza, stanza) || other.stanza == stanza) &&
(identical(other.retransmitted, retransmitted) ||
other.retransmitted == retransmitted) &&
(identical(other.sims, sims) || other.sims == sims) &&
(identical(other.sfs, sfs) || other.sfs == sfs) &&
(identical(other.oob, oob) || other.oob == oob) &&
(identical(other.originId, originId) ||
other.originId == originId) &&
const DeepCollectionEquality()
.equals(other._stanzaIds, _stanzaIds) &&
(identical(other.reply, reply) || other.reply == reply) &&
(identical(other.chatState, chatState) ||
other.chatState == chatState) &&
(identical(other.isCarbon, isCarbon) ||
other.isCarbon == isCarbon) &&
(identical(
other.deliveryReceiptRequested, deliveryReceiptRequested) ||
other.deliveryReceiptRequested == deliveryReceiptRequested) &&
(identical(other.isMarkable, isMarkable) ||
other.isMarkable == isMarkable) &&
(identical(other.fun, fun) || other.fun == fun) &&
(identical(other.funReplacement, funReplacement) ||
other.funReplacement == funReplacement) &&
(identical(other.funCancellation, funCancellation) ||
other.funCancellation == funCancellation) &&
(identical(other.encrypted, encrypted) ||
other.encrypted == encrypted) &&
(identical(other.forceEncryption, forceEncryption) ||
other.forceEncryption == forceEncryption) &&
(identical(other.encryptionType, encryptionType) ||
other.encryptionType == encryptionType) &&
(identical(other.delayedDelivery, delayedDelivery) ||
other.delayedDelivery == delayedDelivery) &&
const DeepCollectionEquality().equals(other._other, this._other) &&
(identical(other.messageRetraction, messageRetraction) ||
other.messageRetraction == messageRetraction) &&
(identical(
other.lastMessageCorrectionSid, lastMessageCorrectionSid) ||
other.lastMessageCorrectionSid == lastMessageCorrectionSid) &&
(identical(other.messageReactions, messageReactions) ||
other.messageReactions == messageReactions) &&
(identical(other.stickerPackId, stickerPackId) ||
other.stickerPackId == stickerPackId) &&
(identical(other.excludeFromStreamManagement,
excludeFromStreamManagement) ||
other.excludeFromStreamManagement ==
excludeFromStreamManagement));
}
@override
int get hashCode => Object.hashAll([
runtimeType,
done,
cancel,
const DeepCollectionEquality().hash(cancelReason),
stanza,
retransmitted,
sims,
sfs,
oob,
originId,
const DeepCollectionEquality().hash(_stanzaIds),
reply,
chatState,
isCarbon,
deliveryReceiptRequested,
isMarkable,
fun,
funReplacement,
funCancellation,
encrypted,
forceEncryption,
encryptionType,
delayedDelivery,
const DeepCollectionEquality().hash(_other),
messageRetraction,
lastMessageCorrectionSid,
messageReactions,
stickerPackId,
excludeFromStreamManagement
]);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
__$$_StanzaHandlerDataCopyWithImpl<_$_StanzaHandlerData>(
this, _$identity);
}
abstract class _StanzaHandlerData implements StanzaHandlerData {
factory _StanzaHandlerData(final bool done, final bool cancel,
final dynamic cancelReason, final Stanza stanza,
{final bool retransmitted,
final StatelessMediaSharingData? sims,
final StatelessFileSharingData? sfs,
final OOBData? oob,
final String? originId,
final List<StanzaId>? stanzaIds,
final ReplyData? reply,
final ChatState? chatState,
final bool isCarbon,
final bool deliveryReceiptRequested,
final bool isMarkable,
final FileMetadataData? fun,
final String? funReplacement,
final String? funCancellation,
final bool encrypted,
final bool forceEncryption,
final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions,
final String? stickerPackId,
final bool excludeFromStreamManagement}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
bool get done;
@override // Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
bool get cancel;
@override // The reason why we cancelled the processing and sending
dynamic get cancelReason;
@override // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
Stanza get stanza;
@override // Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
bool get retransmitted;
@override
StatelessMediaSharingData? get sims;
@override
StatelessFileSharingData? get sfs;
@override
OOBData? get oob;
@override // XEP-0359 <origin-id />'s id attribute, if available.
String? get originId;
@override // XEP-0359 <stanza-id /> elements, if available.
List<StanzaId>? get stanzaIds;
@override
ReplyData? get reply;
@override
ChatState? get chatState;
@override
bool get isCarbon;
@override
bool get deliveryReceiptRequested;
@override
bool get isMarkable;
@override // File Upload Notifications
// A notification
FileMetadataData? get fun;
@override // The stanza id this replaces
String? get funReplacement;
@override // The stanza id this cancels
String? get funCancellation;
@override // Whether the stanza was received encrypted
bool get encrypted;
@override // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption;
@override // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType;
@override // Delayed Delivery
DelayedDelivery? get delayedDelivery;
@override // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
Map<String, dynamic> get other;
@override // If non-null, then it indicates the origin Id of the message that should be
// retracted
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 // Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
bool get excludeFromStreamManagement;
@override
@JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -31,3 +31,4 @@ const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';
const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint';

View File

@ -1,89 +1,71 @@
import 'package:moxlib/moxlib.dart';
import 'package:collection/collection.dart';
import 'package:moxxmpp/src/events.dart';
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/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0184.dart';
import 'package:moxxmpp/src/xeps/xep_0308.dart';
import 'package:moxxmpp/src/xeps/xep_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart';
import 'package:moxxmpp/src/xeps/xep_0449.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
/// Data used to build a message stanza.
///
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be
/// added. This is recommended when sharing files but may cause issues when the message
/// stanza should include a SFS element without any fallbacks.
class MessageDetails {
const MessageDetails({
required this.to,
this.body,
this.requestDeliveryReceipt = false,
this.requestChatMarkers = true,
this.id,
this.originId,
this.quoteBody,
this.quoteId,
this.quoteFrom,
this.chatState,
this.sfs,
this.fun,
this.funReplacement,
this.funCancellation,
this.shouldEncrypt = false,
this.messageRetraction,
this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
});
final String to;
/// A callback that is called whenever a message is sent using
/// [MessageManager.sendMessage]. The input the typed map that is passed to
/// sendMessage.
typedef MessageSendingCallback = List<XMLNode> Function(
TypedMap<StanzaHandlerExtension>,
);
/// The raw content of the <body /> element.
class MessageBodyData implements StanzaHandlerExtension {
const MessageBodyData(this.body);
/// The content of the <body /> element.
final String? body;
final bool requestDeliveryReceipt;
final bool requestChatMarkers;
final String? id;
final String? originId;
final String? quoteBody;
final String? quoteId;
final String? quoteFrom;
final ChatState? chatState;
final StatelessFileSharingData? sfs;
final FileMetadataData? fun;
final String? funReplacement;
final String? funCancellation;
final bool shouldEncrypt;
final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
XMLNode toXML() {
return XMLNode(
tag: 'body',
text: body,
);
}
}
/// The id attribute of the message stanza.
class MessageIdData implements StanzaHandlerExtension {
const MessageIdData(this.id);
/// The id attribute of the stanza.
final String id;
}
class MessageManager extends XmppManagerBase {
MessageManager() : super(messageManager);
/// The priority of the message handler. If a handler should run before this one,
/// which emits the [MessageEvent] event and terminates processing, make sure it
/// has a priority greater than [messageHandlerPriority].
static int messageHandlerPriority = -100;
/// A list of callbacks that are called when a message is sent in order to add
/// appropriate child elements.
final List<MessageSendingCallback> _messageSendingCallbacks =
List<MessageSendingCallback>.empty(growable: true);
void registerMessageSendingCallback(MessageSendingCallback callback) {
_messageSendingCallbacks.add(callback);
}
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
callback: _onMessage,
priority: -100,
priority: messageHandlerPriority,
)
];
@ -94,238 +76,69 @@ class MessageManager extends XmppManagerBase {
Stanza _,
StanzaHandlerData state,
) async {
final message = state.stanza;
final body = message.firstTag('body');
final hints = List<MessageProcessingHint>.empty(growable: true);
for (final element
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
hints.add(messageProcessingHintFromXml(element));
}
getAttributes().sendEvent(
MessageEvent(
body: body != null ? body.innerText() : '',
fromJid: JID.fromString(message.attributes['from']! as String),
toJid: JID.fromString(message.attributes['to']! as String),
sid: message.attributes['id']! as String,
originId: state.originId,
stanzaIds: state.stanzaIds,
isCarbon: state.isCarbon,
deliveryReceiptRequested: state.deliveryReceiptRequested,
isMarkable: state.isMarkable,
type: message.attributes['type'] as String?,
oob: state.oob,
sfs: state.sfs,
sims: state.sims,
reply: state.reply,
chatState: state.chatState,
fun: state.fun,
funReplacement: state.funReplacement,
funCancellation: state.funCancellation,
encrypted: state.encrypted,
messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions,
messageProcessingHints: hints.isEmpty ? null : hints,
stickerPackId: state.stickerPackId,
other: state.other,
error: StanzaError.fromStanza(message),
JID.fromString(state.stanza.attributes['from']! as String),
JID.fromString(state.stanza.attributes['to']! as String),
state.stanza.attributes['id']! as String,
state.encrypted,
state.extensions,
type: state.stanza.attributes['type'] as String?,
error: StanzaError.fromStanza(state.stanza),
encryptionError: state.encryptionError,
),
);
return state.copyWith(done: true);
return state..done = true;
}
/// Send a message to to with the content body. If deliveryRequest is true, then
/// the message will also request a delivery receipt from the receiver.
/// If id is non-null, then it will be the id of the message stanza.
/// element to this id. If originId is non-null, then it will create an "origin-id"
/// child in the message stanza and set its id to originId.
void sendMessage(MessageDetails details) {
assert(
implies(
details.quoteBody != null,
details.quoteFrom != null && details.quoteId != null,
),
'When quoting a message, then quoteFrom and quoteId must also be non-null',
);
final stanza = Stanza.message(
to: details.to,
type: 'chat',
id: details.id,
children: [],
);
if (details.quoteBody != null) {
final quote = QuoteData.fromBodies(details.quoteBody!, details.body!);
stanza
..addChild(
XMLNode(tag: 'body', text: quote.body),
)
..addChild(
XMLNode.xmlns(
tag: 'reply',
xmlns: replyXmlns,
attributes: {'to': details.quoteFrom!, 'id': details.quoteId!},
),
)
..addChild(
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackXmlns,
attributes: {'for': replyXmlns},
children: [
XMLNode(
tag: 'body',
attributes: <String, String>{
'start': '0',
'end': '${quote.fallbackLength}',
},
)
],
),
);
} else {
var body = details.body;
if (details.sfs != null && details.setOOBFallbackBody) {
// TODO(Unknown): Maybe find a better solution
final firstSource = details.sfs!.sources.first;
if (firstSource is StatelessFileSharingUrlSource) {
body = firstSource.url;
} else if (firstSource is StatelessFileSharingEncryptedSource) {
body = firstSource.source.url;
}
} else if (details.messageRetraction?.fallback != null) {
body = details.messageRetraction!.fallback;
}
if (body != null) {
stanza.addChild(
XMLNode(tag: 'body', text: body),
);
}
}
if (details.requestDeliveryReceipt) {
stanza.addChild(makeMessageDeliveryRequest());
}
if (details.requestChatMarkers) {
stanza.addChild(makeChatMarkerMarkable());
}
if (details.originId != null) {
stanza.addChild(makeOriginIdElement(details.originId!));
}
if (details.sfs != null) {
stanza.addChild(details.sfs!.toXML());
final source = details.sfs!.sources.first;
if (source is StatelessFileSharingUrlSource &&
details.setOOBFallbackBody) {
// SFS recommends OOB as a fallback
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
}
}
if (details.chatState != null) {
stanza.addChild(
// TODO(Unknown): Move this into xep_0085.dart
XMLNode.xmlns(
tag: chatStateToString(details.chatState!),
xmlns: chatStateXmlns,
),
);
}
if (details.fun != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'file-upload',
xmlns: fileUploadNotificationXmlns,
children: [
details.fun!.toXML(),
],
),
);
}
if (details.funReplacement != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'replaces',
xmlns: fileUploadNotificationXmlns,
attributes: <String, String>{
'id': details.funReplacement!,
},
),
);
}
if (details.messageRetraction != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'apply-to',
xmlns: fasteningXmlns,
attributes: <String, String>{
'id': details.messageRetraction!.id,
},
children: [
XMLNode.xmlns(
tag: 'retract',
xmlns: messageRetractionXmlns,
),
],
),
);
if (details.messageRetraction!.fallback != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackIndicationXmlns,
),
);
}
}
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(
/// Send an unawaitable message to [to]. [extensions] is a typed map that contains
/// data for building the message.
Future<void> sendMessage(
JID to,
TypedMap<StanzaHandlerExtension> extensions,
) async {
await getAttributes().sendStanza(
StanzaDetails(
stanza,
Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: 'chat',
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
awaitable: false,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
if (extensions.get<ReplyData>() != null) {
return [];
}
if (extensions.get<StickersData>() != null) {
return [];
}
if (extensions.get<StatelessFileSharingData>() != null) {
return [];
}
if (extensions.get<OOBData>() != null) {
return [];
}
final data = extensions.get<MessageBodyData>();
return data != null ? [data.toXML()] : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -9,6 +9,7 @@ const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas';
// RFC 6121
const rosterXmlns = 'jabber:iq:roster';
const rosterVersioningXmlns = 'urn:xmpp:features:rosterver';
const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
// XEP-0004
const dataFormsXmlns = 'jabber:x:data';
@ -96,7 +97,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0';
// XEP-0372
const referenceXmlns = 'urn:xmpp:reference:0';
// XEP-380
// XEP-0380
const emeXmlns = 'urn:xmpp:eme:0';
const emeOtr = 'urn:xmpp:otr:0';
const emeLegacyOpenPGP = 'jabber:x:encrypted';

View File

@ -11,3 +11,4 @@ const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
const bind2Negotiator = 'org.moxxmpp.bind2';
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';
const presenceNegotiator = 'org.moxxmpp.core.presence';

View File

@ -6,14 +6,44 @@ 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/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A function that will be called when presence, outside of subscription request
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
/// presence.
typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
/// A pseudo-negotiator that does not really negotiate anything. Instead, its purpose
/// is to look for a stream feature indicating that we can pre-approve subscription
/// requests, shown by [PresenceNegotiator.preApprovalSupported].
class PresenceNegotiator extends XmppFeatureNegotiatorBase {
PresenceNegotiator()
: super(11, false, subscriptionPreApprovalXmlns, presenceNegotiator);
/// Flag indicating whether presence subscription pre-approval is supported
bool _supported = false;
bool get preApprovalSupported => _supported;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
_supported = true;
return const Result(NegotiatorState.done);
}
@override
void reset() {
_supported = false;
super.reset();
}
}
/// A mandatory manager that handles initial presence sending, sending of subscription
/// request management requests and triggers events for incoming presence stanzas.
class PresenceManager extends XmppManagerBase {
@ -23,11 +53,17 @@ class PresenceManager extends XmppManagerBase {
final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true);
/// The priority of the presence handler. If a handler should run before this one,
/// which terminates processing, make sure the handler has a priority greater than
/// [presenceHandlerPriority].
static int presenceHandlerPriority = -100;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'presence',
callback: _onPresence,
priority: presenceHandlerPriority,
)
];
@ -66,7 +102,7 @@ class PresenceManager extends XmppManagerBase {
from: JID.fromString(presence.from!),
),
);
return state.copyWith(done: true);
return state..done = true;
}
default:
break;
@ -75,10 +111,7 @@ class PresenceManager extends XmppManagerBase {
if (presence.from != null) {
logger.finest("Received presence from '${presence.from}'");
getAttributes().sendEvent(
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
);
return state.copyWith(done: true);
return state..done = true;
}
return state;
@ -128,13 +161,39 @@ class PresenceManager extends XmppManagerBase {
);
}
/// Sends a subscription request to [to].
// TODO(PapaTutuWawa): Check if we're allowed to pre-approve
Future<void> requestSubscription(JID to, {bool preApprove = false}) async {
/// Similar to [requestSubscription], but it also tells the server to automatically
/// accept a subscription request from [to], should it arrive.
/// This requires a [PresenceNegotiator] to be registered as this feature is optional.
///
/// Returns true, when the stanza was sent. Returns false, when the stanza was not sent,
/// for example because the server does not support subscription pre-approvals.
Future<bool> preApproveSubscription(JID to) async {
final negotiator = getAttributes()
.getNegotiatorById<PresenceNegotiator>(presenceNegotiator);
assert(negotiator != null, 'No PresenceNegotiator registered');
if (!negotiator!.preApprovalSupported) {
return false;
}
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: preApprove ? 'subscribed' : 'subscribe',
type: 'subscribed',
to: to.toString(),
),
awaitable: false,
),
);
return true;
}
/// Sends a subscription request to [to].
Future<void> requestSubscription(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribe',
to: to.toString(),
),
awaitable: false,
@ -144,7 +203,15 @@ class PresenceManager extends XmppManagerBase {
/// Accept a subscription request from [to].
Future<void> acceptSubscriptionRequest(JID to) async {
await requestSubscription(to, preApprove: true);
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribed',
to: to.toString(),
),
awaitable: false,
),
);
}
/// Send a subscription request rejection to [to].

View File

@ -145,7 +145,7 @@ class RosterManager extends XmppManagerBase {
logger.warning(
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
);
return state.copyWith(done: true);
return state..done = true;
}
final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
@ -154,7 +154,7 @@ class RosterManager extends XmppManagerBase {
if (item == null) {
logger.warning('Received empty roster push');
return state.copyWith(done: true);
return state..done = true;
}
unawaited(
@ -177,7 +177,7 @@ class RosterManager extends XmppManagerBase {
[],
);
return state.copyWith(done: true);
return state..done = true;
}
/// Shared code between requesting rosters without and with roster versioning, if

View File

@ -0,0 +1,23 @@
/// A map, similar to Map, but always uses the type of the value as the key.
class TypedMap<B> {
/// Create an empty typed map.
TypedMap();
/// Create a typed map from a list of values.
TypedMap.fromList(List<B> items) {
for (final item in items) {
_data[item.runtimeType] = item;
}
}
/// The internal mapping of type -> data
final Map<Object, B> _data = {};
/// Associate the type of [value] with [value] in the map.
void set<T extends B>(T value) {
_data[T] = value;
}
/// Return the object of type [T] from the map, if it has been stored.
T? get<T>() => _data[T] as T?;
}

View File

@ -2,14 +2,70 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
/// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
/// Indicates a file upload notification.
class FileUploadNotificationData implements StanzaHandlerExtension {
const FileUploadNotificationData(this.metadata);
/// The file metadata indicated in the upload notification.
final FileMetadataData metadata;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'file-upload',
xmlns: fileUploadNotificationXmlns,
children: [
metadata.toXML(),
],
);
}
}
/// Indicates that a file upload has been cancelled.
class FileUploadNotificationCancellationData implements StanzaHandlerExtension {
const FileUploadNotificationCancellationData(this.id);
/// The id of the upload notifiaction that is cancelled.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'cancelled',
xmlns: fileUploadNotificationXmlns,
attributes: {
'id': id,
},
);
}
}
/// Indicates that a file upload has been completed.
class FileUploadNotificationReplacementData implements StanzaHandlerExtension {
const FileUploadNotificationReplacementData(this.id);
/// The id of the upload notifiaction that is replaced.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'replaces',
xmlns: fileUploadNotificationXmlns,
attributes: {
'id': id,
},
);
}
}
class FileUploadNotificationManager extends XmppManagerBase {
FileUploadNotificationManager() : super(fileUploadNotificationManager);
@ -47,10 +103,13 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async {
final funElement =
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith(
fun: FileMetadataData.fromXML(
return state
..extensions.set(
FileUploadNotificationData(
FileMetadataData.fromXML(
funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
),
),
);
}
@ -60,8 +119,11 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async {
final element =
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith(
funReplacement: element.attributes['id']! as String,
return state
..extensions.set(
FileUploadNotificationReplacementData(
element.attributes['id']! as String,
),
);
}
@ -71,8 +133,42 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async {
final element =
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith(
funCancellation: element.attributes['id']! as String,
return state
..extensions.set(
FileUploadNotificationCancellationData(
element.attributes['id']! as String,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final fun = extensions.get<FileUploadNotificationData>();
if (fun != null) {
return [fun.toXML()];
}
final cancel = extensions.get<FileUploadNotificationCancellationData>();
if (cancel != null) {
return [cancel.toXML()];
}
final replace = extensions.get<FileUploadNotificationReplacementData>();
if (replace != null) {
return [replace.toXML()];
}
return [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -184,7 +184,7 @@ class DiscoManager extends XmppManagerBase {
],
);
return state.copyWith(done: true);
return state..done = true;
}
await reply(
@ -195,7 +195,7 @@ class DiscoManager extends XmppManagerBase {
],
);
return state.copyWith(done: true);
return state..done = true;
}
Future<StanzaHandlerData> _onDiscoItemsRequest(
@ -223,7 +223,7 @@ class DiscoManager extends XmppManagerBase {
],
);
return state.copyWith(done: true);
return state..done = true;
}
return state;

View File

@ -76,7 +76,7 @@ class VCardManager extends XmppManagerBase {
}
}
return state.copyWith(done: true);
return state..done = true;
}
VCardPhoto? _parseVCardPhoto(XMLNode? node) {

View File

@ -114,7 +114,7 @@ class PubSubManager extends XmppManagerBase {
),
);
return state.copyWith(done: true);
return state..done = true;
}
Future<int> _getNodeItemCount(JID jid, String node) async {

View File

@ -2,32 +2,32 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A data class representing the jabber:x:oob tag.
class OOBData {
const OOBData({this.url, this.desc});
class OOBData implements StanzaHandlerExtension {
const OOBData(this.url, this.desc);
/// The communicated URL of the OOB data
final String? url;
/// The description of the url.
final String? desc;
}
XMLNode constructOOBNode(OOBData data) {
final children = List<XMLNode>.empty(growable: true);
if (data.url != null) {
children.add(XMLNode(tag: 'url', text: data.url));
}
if (data.desc != null) {
children.add(XMLNode(tag: 'desc', text: data.desc));
}
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'x',
xmlns: oobDataXmlns,
children: children,
children: [
if (url != null) XMLNode(tag: 'url', text: url),
if (desc != null) XMLNode(tag: 'desc', text: desc),
],
);
}
}
class OOBManager extends XmppManagerBase {
@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase {
final url = x.firstTag('url');
final desc = x.firstTag('desc');
return state.copyWith(
oob: OOBData(
url: url?.innerText(),
desc: desc?.innerText(),
return state
..extensions.set(
OOBData(
url?.innerText(),
desc?.innerText(),
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<OOBData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -2,43 +2,59 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatState { active, composing, paused, inactive, gone }
enum ChatState implements StanzaHandlerExtension {
active,
composing,
paused,
inactive,
gone;
ChatState chatStateFromString(String raw) {
switch (raw) {
factory ChatState.fromName(String state) {
switch (state) {
case 'active':
{
return ChatState.active;
}
case 'composing':
{
return ChatState.composing;
}
case 'paused':
{
return ChatState.paused;
}
case 'inactive':
{
return ChatState.inactive;
}
case 'gone':
{
return ChatState.gone;
}
default:
{
return ChatState.gone;
throw Exception('Invalid chat state $state');
}
String toName() {
switch (this) {
case ChatState.active:
return 'active';
case ChatState.composing:
return 'composing';
case ChatState.paused:
return 'paused';
case ChatState.inactive:
return 'inactive';
case ChatState.gone:
return 'gone';
}
}
XMLNode toXML() {
return XMLNode.xmlns(
tag: toName(),
xmlns: chatStateXmlns,
);
}
}
String chatStateToString(ChatState state) => state.toString().split('.').last;
class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager);
@ -64,62 +80,55 @@ class ChatStateManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
ChatState? chatState;
switch (element.tag) {
case 'active':
{
chatState = ChatState.active;
}
break;
case 'composing':
{
chatState = ChatState.composing;
}
break;
case 'paused':
{
chatState = ChatState.paused;
}
break;
case 'inactive':
{
chatState = ChatState.inactive;
}
break;
case 'gone':
{
chatState = ChatState.gone;
}
break;
default:
{
logger.warning("Received invalid chat state '${element.tag}'");
}
try {
state.extensions.set(ChatState.fromName(element.tag));
} catch (_) {
logger.finest('Ignoring invalid chat state ${element.tag}');
}
return state.copyWith(chatState: chatState);
return state;
}
/// Send a chat state notification to [to]. You can specify the type attribute
/// of the message with [messageType].
void sendChatState(
Future<void> sendChatState(
ChatState state,
String to, {
String messageType = 'chat',
}) {
final tagName = state.toString().split('.').last;
getAttributes().sendStanza(
}) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to,
type: messageType,
children: [
XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns),
state.toXML(),
],
),
awaitable: false,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<ChatState>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -4,10 +4,13 @@ import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
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/presence.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/util/list.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart';
@ -105,7 +108,20 @@ class EntityCapabilitiesManager extends XmppManagerBase {
Future<bool> isSupported() async => true;
@override
List<String> getDiscoFeatures() => [capsXmlns];
List<String> getDiscoFeatures() => [
capsXmlns,
];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'presence',
tagName: 'c',
tagXmlns: capsXmlns,
callback: onPresence,
priority: PresenceManager.presenceHandlerPriority + 1,
),
];
/// Computes, if required, the capability hash of the data provided by
/// the DiscoManager.
@ -159,33 +175,38 @@ class EntityCapabilitiesManager extends XmppManagerBase {
}
@visibleForTesting
Future<void> onPresence(PresenceReceivedEvent event) async {
final c = event.presence.firstTag('c', xmlns: capsXmlns);
if (c == null) {
return;
Future<StanzaHandlerData> onPresence(
Stanza stanza,
StanzaHandlerData state,
) async {
if (stanza.from == null) {
return state;
}
final from = JID.fromString(stanza.from!);
final c = stanza.firstTag('c', xmlns: capsXmlns)!;
final hashFunctionName = c.attributes['hash'] as String?;
final capabilityNode = c.attributes['node'] as String?;
final ver = c.attributes['ver'] as String?;
if (hashFunctionName == null || capabilityNode == null || ver == null) {
return;
return state;
}
// Check if we know of the hash
final isCached =
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
if (isCached) {
return;
return state;
}
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final discoRequest = await dm.discoInfoQuery(
event.jid,
from,
node: capabilityNode,
);
if (discoRequest.isType<DiscoError>()) {
return;
return state;
}
final discoInfo = discoRequest.get<DiscoInfo>();
@ -194,13 +215,13 @@ class EntityCapabilitiesManager extends XmppManagerBase {
await dm.addCachedDiscoInfo(
MapEntry<DiscoCacheKey, DiscoInfo>(
DiscoCacheKey(
event.jid,
from,
null,
),
discoInfo,
),
);
return;
return state;
}
// Validate the disco#info result according to XEP-0115 § 5.4
@ -214,7 +235,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one equal identity',
);
return;
return state;
}
}
@ -225,7 +246,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one equal feature',
);
return;
return state;
}
}
@ -253,7 +274,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.',
);
return;
return state;
}
}
@ -268,7 +289,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value',
);
return;
return state;
}
// Check if the field type is hidden
@ -297,14 +318,16 @@ class EntityCapabilitiesManager extends XmppManagerBase {
if (computedCapabilityHash == ver) {
await _cacheLock.synchronized(() {
_jidToCapHashCache[event.jid.toString()] = ver;
_jidToCapHashCache[from.toString()] = ver;
_capHashCache[ver] = newDiscoInfo;
});
} else {
logger.warning(
'Capability hash mismatch from ${event.jid}: Received "$ver", expected "$computedCapabilityHash".',
'Capability hash mismatch from $from: Received "$ver", expected "$computedCapabilityHash".',
);
}
return state;
}
@visibleForTesting
@ -315,9 +338,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is PresenceReceivedEvent) {
unawaited(onPresence(event));
} else if (event is StreamNegotiationsDoneEvent) {
if (event is StreamNegotiationsDoneEvent) {
// Clear the JID to cap. hash mapping.
await _cacheLock.synchronized(_jidToCapHashCache.clear);
}

View File

@ -4,23 +4,43 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeMessageDeliveryRequest() {
class MessageDeliveryReceiptData implements StanzaHandlerExtension {
const MessageDeliveryReceiptData(this.receiptRequested);
/// Indicates whether a delivery receipt is requested or not.
final bool receiptRequested;
XMLNode toXML() {
assert(
receiptRequested,
'This method makes little sense with receiptRequested == false',
);
return XMLNode.xmlns(
tag: 'request',
xmlns: deliveryXmlns,
);
}
}
XMLNode makeMessageDeliveryResponse(String id) {
class MessageDeliveryReceivedData implements StanzaHandlerExtension {
const MessageDeliveryReceivedData(this.id);
/// The stanza id of the message we received.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'received',
xmlns: deliveryXmlns,
attributes: {'id': id},
);
}
}
class MessageDeliveryReceiptManager extends XmppManagerBase {
@ -56,7 +76,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
Stanza message,
StanzaHandlerData state,
) async {
return state.copyWith(deliveryReceiptRequested: true);
return state..extensions.set(const MessageDeliveryReceiptData(true));
}
Future<StanzaHandlerData> _onDeliveryReceiptReceived(
@ -64,16 +84,16 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final received = message.firstTag('received', xmlns: deliveryXmlns)!;
for (final item in message.children) {
if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
.contains(item.tag)) {
logger.info(
"Won't handle stanza as delivery receipt because we found an '${item.tag}' element",
);
// for (final item in message.children) {
// if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
// .contains(item.tag)) {
// logger.info(
// "Won't handle stanza as delivery receipt because we found an '${item.tag}' element",
// );
return state.copyWith(done: true);
}
}
// return state.copyWith(done: true);
// }
// }
getAttributes().sendEvent(
DeliveryReceiptReceivedEvent(
@ -81,6 +101,27 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
id: received.attributes['id']! as String,
),
);
return state.copyWith(done: true);
return state..done = true;
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageDeliveryReceivedData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -70,7 +70,7 @@ class BlockingManager extends XmppManagerBase {
),
);
return state.copyWith(done: true);
return state..done = true;
}
Future<StanzaHandlerData> _unblockPush(
@ -92,7 +92,7 @@ class BlockingManager extends XmppManagerBase {
);
}
return state.copyWith(done: true);
return state..done = true;
}
Future<bool> block(List<String> items) async {

View File

@ -0,0 +1,8 @@
import 'package:moxxmpp/src/managers/data.dart';
class StreamManagementData implements StanzaHandlerExtension {
const StreamManagementData(this.exclude);
/// Whether the stanza should be exluded from the StreamManagement's resend queue.
final bool exclude;
}

View File

@ -15,6 +15,7 @@ import 'package:moxxmpp/src/xeps/xep_0198/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:synchronized/synchronized.dart';
const xmlUintMax = 4294967296; // 2**32
@ -402,7 +403,9 @@ class StreamManagementManager extends XmppManagerBase {
if (isStreamManagementEnabled()) {
await _incrementC2S();
if (state.excludeFromStreamManagement) return state;
if (state.extensions.get<StreamManagementData>()?.exclude ?? false) {
return state;
}
_unackedStanzas[_state.c2s] = stanza;
await _sendAckRequest();

View File

@ -1,4 +1,5 @@
import 'package:meta/meta.dart';
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';
@ -7,10 +8,14 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
@immutable
class DelayedDelivery {
const DelayedDelivery(this.from, this.timestamp);
class DelayedDeliveryData implements StanzaHandlerExtension {
const DelayedDeliveryData(this.from, this.timestamp);
/// The timestamp the message was originally sent.
final DateTime timestamp;
final String from;
/// The JID that originally sent the message.
final JID from;
}
class DelayedDeliveryManager extends XmppManagerBase {
@ -23,6 +28,8 @@ class DelayedDeliveryManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'delay',
tagXmlns: delayedDeliveryXmlns,
callback: _onIncomingMessage,
priority: 200,
),
@ -32,12 +39,12 @@ class DelayedDeliveryManager extends XmppManagerBase {
Stanza stanza,
StanzaHandlerData state,
) async {
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns);
if (delay == null) return state;
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns)!;
return state.copyWith(
delayedDelivery: DelayedDelivery(
delay.attributes['from']! as String,
return state
..extensions.set(
DelayedDeliveryData(
JID.fromString(delay.attributes['from']! as String),
DateTime.parse(delay.attributes['stamp']! as String),
),
);

View File

@ -14,6 +14,13 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
class CarbonsData implements StanzaHandlerExtension {
const CarbonsData(this.isCarbon);
/// Indicates whether this message is a carbon.
final bool isCarbon;
}
/// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super(carbonsManager);
@ -77,15 +84,14 @@ class CarbonsManager extends XmppManagerBase {
) async {
final from = JID.fromString(message.attributes['from']! as String);
final received = message.firstTag('received', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true);
if (!isCarbonValid(from)) return state..done = true;
final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded);
return state.copyWith(
isCarbon: true,
stanza: carbon,
);
return state
..extensions.set(const CarbonsData(true))
..stanza = carbon;
}
Future<StanzaHandlerData> _onMessageSent(
@ -94,15 +100,14 @@ class CarbonsManager extends XmppManagerBase {
) async {
final from = JID.fromString(message.attributes['from']! as String);
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true);
if (!isCarbonValid(from)) return state..done = true;
final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded);
return state.copyWith(
isCarbon: true,
stanza: carbon,
);
return state
..extensions.set(const CarbonsData(true))
..stanza = carbon;
}
/// Send a request to the server, asking it to enable Message Carbons.

View File

@ -66,7 +66,7 @@ enum HashFunction {
return HashFunction.blake2b512;
}
throw Exception();
throw Exception('Invalid hash function $name');
}
/// Like [HashFunction.fromName], but returns null if the hash function is unknown

View File

@ -2,18 +2,27 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeLastMessageCorrectionEdit(String id) {
class LastMessageCorrectionData implements StanzaHandlerExtension {
const LastMessageCorrectionData(this.id);
/// The id the LMC applies to.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'replace',
xmlns: lmcXmlns,
attributes: <String, String>{
attributes: {
'id': id,
},
);
}
}
class LastMessageCorrectionManager extends XmppManagerBase {
@ -42,8 +51,30 @@ class LastMessageCorrectionManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
return state.copyWith(
lastMessageCorrectionSid: edit.attributes['id']! as String,
return state
..extensions.set(
LastMessageCorrectionData(edit.attributes['id']! as String),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<LastMessageCorrectionData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -4,27 +4,86 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatMarker {
received,
displayed,
acknowledged;
factory ChatMarker.fromName(String name) {
switch (name) {
case 'received':
return ChatMarker.received;
case 'displayed':
return ChatMarker.displayed;
case 'acknowledged':
return ChatMarker.acknowledged;
}
throw Exception('Invalid chat marker $name');
}
XMLNode toXML() {
String tag;
switch (this) {
case ChatMarker.received:
tag = 'received';
break;
case ChatMarker.displayed:
tag = 'displayed';
break;
case ChatMarker.acknowledged:
tag = 'acknowledged';
break;
}
return XMLNode.xmlns(
tag: tag,
xmlns: chatMarkersXmlns,
);
}
}
class MarkableData implements StanzaHandlerExtension {
const MarkableData(this.isMarkable);
/// Indicates whether the message can be replied to with a chat marker.
final bool isMarkable;
XMLNode toXML() {
assert(isMarkable, '');
XMLNode makeChatMarkerMarkable() {
return XMLNode.xmlns(
tag: 'markable',
xmlns: chatMarkersXmlns,
);
}
}
XMLNode makeChatMarker(String tag, String id) {
assert(
['received', 'displayed', 'acknowledged'].contains(tag),
'Invalid chat marker',
);
class ChatMarkerData implements StanzaHandlerExtension {
const ChatMarkerData(this.marker, this.id);
/// The actual chat state
final ChatMarker marker;
/// The ID the chat marker applies to
final String id;
XMLNode toXML() {
final tag = marker.toXML();
return XMLNode.xmlns(
tag: tag,
tag: tag.tag,
xmlns: chatMarkersXmlns,
attributes: {'id': id},
attributes: {
'id': id,
},
);
}
}
class ChatMarkerManager extends XmppManagerBase {
@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase {
Stanza message,
StanzaHandlerData state,
) async {
final marker = message.firstTagByXmlns(chatMarkersXmlns)!;
final element = message.firstTagByXmlns(chatMarkersXmlns)!;
// Handle the <markable /> explicitly
if (marker.tag == 'markable') return state.copyWith(isMarkable: true);
if (element.tag == 'markable') {
return state..extensions.set(const MarkableData(true));
}
if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) {
logger.warning("Unknown message marker '${marker.tag}' found.");
} else {
try {
getAttributes().sendEvent(
ChatMarkerEvent(
from: JID.fromString(message.from!),
type: marker.tag,
id: marker.attributes['id']! as String,
JID.fromString(message.from!),
ChatMarker.fromName(element.tag),
element.attributes['id']! as String,
),
);
} catch (_) {
logger.warning("Unknown message marker '${element.tag}' found.");
}
return state.copyWith(done: true);
return state..done = true;
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final children = List<XMLNode>.empty(growable: true);
final marker = extensions.get<ChatMarkerData>();
if (marker != null) {
children.add(marker.toXML());
}
final markable = extensions.get<MarkableData>();
if (markable != null) {
children.add(markable.toXML());
}
return children;
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -1,15 +1,21 @@
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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum MessageProcessingHint {
noPermanentStore,
noStore,
noCopies,
store,
}
store;
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
switch (element.tag) {
factory MessageProcessingHint.fromName(String name) {
switch (name) {
case 'no-permanent-store':
return MessageProcessingHint.noPermanentStore;
case 'no-store':
@ -20,12 +26,11 @@ MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
return MessageProcessingHint.store;
}
assert(false, 'Invalid Message Processing Hint: ${element.tag}');
assert(false, 'Invalid Message Processing Hint: $name');
return MessageProcessingHint.noStore;
}
}
extension XmlExtension on MessageProcessingHint {
XMLNode toXml() {
XMLNode toXML() {
String tag;
switch (this) {
case MessageProcessingHint.noPermanentStore:
@ -48,3 +53,60 @@ extension XmlExtension on MessageProcessingHint {
);
}
}
class MessageProcessingHintData implements StanzaHandlerExtension {
const MessageProcessingHintData(this.hints);
/// The attached message processing hints.
final List<MessageProcessingHint> hints;
}
class MessageProcessingHintManager extends XmppManagerBase {
MessageProcessingHintManager() : super(messageProcessingHintManager);
@override
Future<bool> isSupported() async => true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagXmlns: messageProcessingHintsXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
),
];
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final elements = stanza.findTagsByXmlns(messageProcessingHintsXmlns);
return state
..extensions.set(
MessageProcessingHintData(
elements
.map((element) => MessageProcessingHint.fromName(element.tag))
.toList(),
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageProcessingHintData>();
return data != null ? data.hints.map((hint) => hint.toXML()).toList() : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -3,9 +3,11 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Representation of a <stanza-id /> element.
class StanzaId {
@ -20,7 +22,7 @@ class StanzaId {
/// The JID the id was generated by.
final JID by;
XMLNode toXml() {
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'stanza-id',
xmlns: stableIdXmlns,
@ -32,12 +34,38 @@ class StanzaId {
}
}
XMLNode makeOriginIdElement(String id) {
class StableIdData implements StanzaHandlerExtension {
const StableIdData(this.originId, this.stanzaIds);
/// <origin-id />
final String? originId;
/// Stanza ids
final List<StanzaId>? stanzaIds;
XMLNode toOriginIdElement() {
assert(
originId != null,
'Can only build the XML element if originId != null',
);
return XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': id},
attributes: {'id': originId!},
);
}
List<XMLNode> toXML() {
return [
if (originId != null)
XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': originId!},
),
if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()),
];
}
}
class StableIdManager extends XmppManagerBase {
@ -86,9 +114,29 @@ class StableIdManager extends XmppManagerBase {
.toList();
}
return state.copyWith(
originId: originId,
stanzaIds: stanzaIds,
return state
..extensions.set(
StableIdData(
originId,
stanzaIds,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StableIdData>();
return data != null ? data.toXML() : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -6,37 +6,17 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
enum ExplicitEncryptionType {
enum ExplicitEncryptionType implements StanzaHandlerExtension {
otr,
legacyOpenPGP,
openPGP,
omemo,
omemo1,
omemo2,
unknown,
}
unknown;
String _explicitEncryptionTypeToString(ExplicitEncryptionType type) {
switch (type) {
case ExplicitEncryptionType.otr:
return emeOtr;
case ExplicitEncryptionType.legacyOpenPGP:
return emeLegacyOpenPGP;
case ExplicitEncryptionType.openPGP:
return emeOpenPGP;
case ExplicitEncryptionType.omemo:
return emeOmemo;
case ExplicitEncryptionType.omemo1:
return emeOmemo1;
case ExplicitEncryptionType.omemo2:
return emeOmemo2;
case ExplicitEncryptionType.unknown:
return '';
}
}
ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) {
switch (str) {
factory ExplicitEncryptionType.fromNamespace(String namespace) {
switch (namespace) {
case emeOtr:
return ExplicitEncryptionType.otr;
case emeLegacyOpenPGP:
@ -52,18 +32,38 @@ ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) {
default:
return ExplicitEncryptionType.unknown;
}
}
}
/// Create an <encryption /> element with [type] indicating which type of encryption was
/// used.
XMLNode buildEmeElement(ExplicitEncryptionType type) {
String toNamespace() {
switch (this) {
case ExplicitEncryptionType.otr:
return emeOtr;
case ExplicitEncryptionType.legacyOpenPGP:
return emeLegacyOpenPGP;
case ExplicitEncryptionType.openPGP:
return emeOpenPGP;
case ExplicitEncryptionType.omemo:
return emeOmemo;
case ExplicitEncryptionType.omemo1:
return emeOmemo1;
case ExplicitEncryptionType.omemo2:
return emeOmemo2;
case ExplicitEncryptionType.unknown:
return '';
}
}
/// Create an <encryption /> element with an xmlns indicating what type of encryption was
/// used.
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'encryption',
xmlns: emeXmlns,
attributes: <String, String>{
'namespace': _explicitEncryptionTypeToString(type),
'namespace': toNamespace(),
},
);
}
}
class EmeManager extends XmppManagerBase {
@ -91,8 +91,9 @@ class EmeManager extends XmppManagerBase {
) async {
final encryption = message.firstTag('encryption', xmlns: emeXmlns)!;
return state.copyWith(
encryptionType: _explicitEncryptionTypeFromString(
return state
..extensions.set(
ExplicitEncryptionType.fromNamespace(
encryption.attributes['namespace']! as String,
),
);

View File

@ -1,6 +1,21 @@
import 'package:omemo_dart/omemo_dart.dart';
/// A simple wrapper class for defining elements that should not be encrypted.
class DoNotEncrypt {
const DoNotEncrypt(this.tag, this.xmlns);
/// The tag of the element.
final String tag;
/// The xmlns attribute of the element.
final String xmlns;
}
/// An encryption error caused by OMEMO.
class OmemoEncryptionError {
const OmemoEncryptionError(this.jids, this.devices);
/// See omemo_dart's EncryptionResult for info on these fields.
final Map<String, OmemoException> jids;
final Map<RatchetMapKey, OmemoException> devices;
}

View File

@ -16,6 +16,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
@ -276,7 +277,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// 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.
MessageProcessingHint.store.toXml(),
MessageProcessingHint.store.toXML(),
],
),
awaitable: false,
@ -363,18 +364,17 @@ abstract class BaseOmemoManager extends XmppManagerBase {
logger.finest('Encryption done');
if (!result.isSuccess(2)) {
final other = Map<String, dynamic>.from(state.other);
other['encryption_error_jids'] = result.jidEncryptionErrors;
other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith(
other: other,
return state
..cancel = true
// If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()]
..cancelReason = result.jidEncryptionErrors[toJid.toString()]
is NoKeyMaterialAvailableException
? OmemoNotSupportedForContactException()
: UnknownOmemoError(),
cancel: true,
: UnknownOmemoError()
..encryptionError = OmemoEncryptionError(
result.jidEncryptionErrors,
result.deviceEncryptionErrors,
);
}
@ -389,19 +389,16 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (stanza.tag == 'message') {
children
// Add EME data
..add(buildEmeElement(ExplicitEncryptionType.omemo2))
..add(ExplicitEncryptionType.omemo2.toXML())
// 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());
..add(MessageProcessingHint.store.toXML());
}
return state.copyWith(
stanza: state.stanza.copyWith(
children: children,
),
encrypted: true,
);
return state
..stanza = state.stanza.copyWith(children: children)
..encrypted = true;
}
/// This function is called whenever a message is to be encrypted. If it returns true,
@ -444,17 +441,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
OmemoIncomingStanza(
fromJid.toString(),
sid,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ??
state.extensions
.get<DelayedDeliveryData>()
?.timestamp
.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys,
payloadElement?.innerText(),
),
);
final other = Map<String, dynamic>.from(state.other);
var children = stanza.children;
if (result.error != null) {
other['encryption_error'] = result.error;
state.encryptionError = result.error;
} else {
children = stanza.children
.where(
@ -471,11 +470,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
envelope = XMLNode.fromString(result.payload!);
} on XmlParserException catch (_) {
logger.warning('Failed to parse envelope payload: ${result.payload!}');
other['encryption_error'] = InvalidEnvelopePayloadException();
return state.copyWith(
encrypted: true,
other: other,
);
return state
..encrypted = true
..encryptionError = InvalidEnvelopePayloadException();
}
final envelopeChildren = envelope.firstTag('content')?.children;
@ -489,13 +486,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException();
state.encryptionError = InvalidAffixElementsException();
}
}
return state.copyWith(
encrypted: true,
stanza: Stanza(
return state
..encrypted = true
..stanza = Stanza(
to: stanza.to,
from: stanza.from,
id: stanza.id,
@ -503,8 +500,6 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
),
other: other,
);
}

View File

@ -7,7 +7,7 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
class StatelessMediaSharingData {
class StatelessMediaSharingData implements StanzaHandlerExtension {
const StatelessMediaSharingData({
required this.mediaType,
required this.size,
@ -70,7 +70,9 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
);
}
@Deprecated('Not maintained')
class SIMSManager extends XmppManagerBase {
@Deprecated('Not maintained')
SIMSManager() : super(simsManager);
@override
@ -98,7 +100,7 @@ class SIMSManager extends XmppManagerBase {
final references = message.findTags('reference', xmlns: referenceXmlns);
for (final ref in references) {
final sims = ref.firstTag('media-sharing', xmlns: simsXmlns);
if (sims != null) return state.copyWith(sims: parseSIMSElement(sims));
if (sims != null) return state..extensions.set(parseSIMSElement(sims));
}
return state;

View File

@ -2,12 +2,19 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageRetractionData {
class MessageRetractionData implements StanzaHandlerExtension {
MessageRetractionData(this.id, this.fallback);
/// A potential fallback message to set the body to when retracting.
final String? fallback;
/// The id of the message that is retracted.
final String id;
}
@ -47,11 +54,55 @@ class MessageRetractionManager extends XmppManagerBase {
final isFallbackBody =
message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null;
return state.copyWith(
messageRetraction: MessageRetractionData(
return state
..extensions.set(
MessageRetractionData(
applyTo.attributes['id']! as String,
isFallbackBody ? message.firstTag('body')?.innerText() : null,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageRetractionData>();
return data != null
? [
XMLNode.xmlns(
tag: 'apply-to',
xmlns: fasteningXmlns,
attributes: <String, String>{
'id': data.id,
},
children: [
XMLNode.xmlns(
tag: 'retract',
xmlns: messageRetractionXmlns,
),
],
),
if (data.fallback != null)
XMLNode(
tag: 'body',
text: data.fallback,
),
if (data.fallback != null)
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackIndicationXmlns,
),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -2,16 +2,18 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageReactions {
const MessageReactions(this.messageId, this.emojis);
class MessageReactionsData implements StanzaHandlerExtension {
const MessageReactionsData(this.messageId, this.emojis);
final String messageId;
final List<String> emojis;
XMLNode toXml() {
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'reactions',
xmlns: messageReactionsXmlns,
@ -55,8 +57,9 @@ class MessageReactionsManager extends XmppManagerBase {
) async {
final reactionsElement =
message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
return state.copyWith(
messageReactions: MessageReactions(
return state
..extensions.set(
MessageReactionsData(
reactionsElement.attributes['id']! as String,
reactionsElement.children
.where((c) => c.tag == 'reaction')
@ -65,4 +68,25 @@ class MessageReactionsManager extends XmppManagerBase {
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageReactionsData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -3,9 +3,12 @@ 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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart';
@ -70,8 +73,12 @@ List<StatelessFileSharingSource> processStatelessFileSharingSources(
return sources;
}
class StatelessFileSharingData {
const StatelessFileSharingData(this.metadata, this.sources);
class StatelessFileSharingData implements StanzaHandlerExtension {
const StatelessFileSharingData(
this.metadata,
this.sources, {
this.includeOOBFallback = false,
});
/// Parse [node] as a StatelessFileSharingData element.
factory StatelessFileSharingData.fromXML(XMLNode node) {
@ -88,6 +95,10 @@ class StatelessFileSharingData {
final FileMetadataData metadata;
final List<StatelessFileSharingSource> sources;
/// Flag indicating whether an OOB fallback should be set. The value is only
/// relevant in the context of the messageSendingCallback.
final bool includeOOBFallback;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'file-sharing',
@ -122,23 +133,54 @@ class SFSManager extends XmppManagerBase {
tagXmlns: sfsXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
priority: -98,
)
];
@override
Future<bool> isSupported() async => true;
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StatelessFileSharingData>();
if (data == null) {
return [];
}
// TODO(Unknown): Consider all sources?
final source = data.sources.first;
OOBData? oob;
if (source is StatelessFileSharingUrlSource && data.includeOOBFallback) {
// SFS recommends OOB as a fallback
oob = OOBData(source.url, null);
}
return [
data.toXML(),
if (oob != null) oob.toXML(),
];
}
Future<StanzaHandlerData> _onMessage(
Stanza message,
StanzaHandlerData state,
) async {
final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!;
return state.copyWith(
sfs: StatelessFileSharingData.fromXML(
sfs,
),
return state
..extensions.set(
StatelessFileSharingData.fromXML(sfs),
);
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -4,11 +4,13 @@ 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/message.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/util/typed_map.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';
@ -227,6 +229,19 @@ class StickerPack {
}
}
class StickersData implements StanzaHandlerExtension {
const StickersData(this.stickerPackId, this.sticker, {this.addBody = true});
/// The id of the sticker pack the referenced sticker is from.
final String stickerPackId;
/// The metadata of the sticker.
final StatelessFileSharingData sticker;
/// If true, sets the sticker's metadata desc attribute as the message body.
final bool addBody;
}
class StickersManager extends XmppManagerBase {
StickersManager() : super(stickersManager);
@ -249,11 +264,37 @@ class StickersManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state.copyWith(
stickerPackId: sticker.attributes['pack']! as String,
return state
..extensions.set(
StickersData(
sticker.attributes['pack']! as String,
state.extensions.get<StatelessFileSharingData>()!,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StickersData>();
return data != null
? [
XMLNode.xmlns(
tag: 'sticker',
xmlns: stickersXmlns,
attributes: {
'pack': data.stickerPackId,
},
),
data.sticker.toXML(),
// Add a body
if (data.addBody && data.sticker.metadata.desc != null)
MessageBodyData(data.sticker.metadata.desc).toXML(),
]
: [];
}
/// Publishes the StickerPack [pack] to the PubSub node of [jid]. If specified, then
/// [accessModel] will be used as the PubSub node's access model.
///
@ -319,4 +360,14 @@ class StickersManager extends XmppManagerBase {
return Result(stickerPack);
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -1,24 +1,38 @@
import 'package:meta/meta.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
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/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Data summarizing the XEP-0461 data.
class ReplyData {
const ReplyData({
required this.id,
this.to,
/// A reply to a message.
class ReplyData implements StanzaHandlerExtension {
const ReplyData(
this.id, {
this.body,
this.jid,
this.start,
this.end,
});
/// The bare JID to whom the reply applies to
final String? to;
ReplyData.fromQuoteData(
this.id,
QuoteData quote, {
this.jid,
}) : body = quote.body,
start = 0,
end = quote.fallbackLength;
/// The stanza ID of the message that is replied to
/// The JID of the entity whose message we are replying to.
final JID? jid;
/// The id of the message that is replied to. What id to use depends on what kind
/// of message you want to reply to.
final String id;
/// The start of the fallback body (inclusive)
@ -27,18 +41,21 @@ class ReplyData {
/// The end of the fallback body (exclusive)
final int? end;
/// The body of the message.
final String? body;
/// Applies the metadata to the received body [body] in order to remove the fallback.
/// If either [ReplyData.start] or [ReplyData.end] are null, then body is returned as
/// is.
String removeFallback(String body) {
String? get withoutFallback {
if (body == null) return null;
if (start == null || end == null) return body;
return body.replaceRange(start!, end, '');
return body!.replaceRange(start!, end, '');
}
}
/// Internal class describing how to build a message with a quote fallback body.
@visibleForTesting
class QuoteData {
const QuoteData(this.body, this.fallbackLength);
@ -85,13 +102,54 @@ class MessageRepliesManager extends XmppManagerBase {
@override
Future<bool> isSupported() async => true;
@visibleForTesting
List<XMLNode> messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<ReplyData>();
return data != null
? [
XMLNode.xmlns(
tag: 'reply',
xmlns: replyXmlns,
attributes: {
// The to attribute is optional
if (data.jid != null) 'to': data.jid!.toString(),
'id': data.id,
},
),
if (data.body != null)
XMLNode(
tag: 'body',
text: data.body,
),
if (data.body != null)
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackXmlns,
attributes: {'for': replyXmlns},
children: [
XMLNode(
tag: 'body',
attributes: {
'start': data.start!.toString(),
'end': data.end!.toString(),
},
),
],
),
]
: [];
}
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final reply = stanza.firstTag('reply', xmlns: replyXmlns)!;
final id = reply.attributes['id']! as String;
final to = reply.attributes['to'] as String?;
final jid = to != null ? JID.fromString(to) : null;
int? start;
int? end;
@ -103,13 +161,25 @@ class MessageRepliesManager extends XmppManagerBase {
end = int.parse(body.attributes['end']! as String);
}
return state.copyWith(
reply: ReplyData(
id: id,
to: to,
return state
..extensions.set(
ReplyData(
reply.attributes['id']! as String,
jid: jid,
start: start,
end: end,
body: stanza.firstTag('body')?.innerText(),
),
);
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(messageSendingCallback);
}
}

View File

@ -1,6 +1,6 @@
name: moxxmpp
description: A pure-Dart XMPP library
version: 0.3.2
version: 0.4.0
homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub

View File

@ -1,13 +1,14 @@
import 'dart:async';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/handlers/client.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/reconnect.dart';
import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import '../helpers/xmpp.dart';
@ -15,15 +16,15 @@ import '../helpers/xmpp.dart';
/// This class allows registering managers for easier testing.
class TestingManagerHolder {
TestingManagerHolder({
BaseSocketWrapper? socket,
}) : _socket = socket ?? StubTCPSocket([]);
StubTCPSocket? stubSocket,
}) : socket = stubSocket ?? StubTCPSocket([]);
final BaseSocketWrapper _socket;
final StubTCPSocket socket;
final Map<String, XmppManagerBase> _managers = {};
// The amount of stanzas sent
int sentStanzas = 0;
/// A list of events that were triggered.
final List<XmppEvent> sentEvents = List.empty(growable: true);
static final JID jid = JID.fromString('testuser@example.org/abc123');
static final ConnectionSettings settings = ConnectionSettings(
@ -31,22 +32,17 @@ class TestingManagerHolder {
password: 'abc123',
);
Future<XMLNode> _sendStanza(
stanza, {
bool addId = true,
bool awaitable = true,
bool encrypted = false,
bool forceEncryption = false,
}) async {
sentStanzas++;
return XMLNode.fromString('<iq />');
Future<XMLNode?> _sendStanza(StanzaDetails details) async {
socket.write(details.stanza.toXml());
return null;
}
T? _getManagerById<T extends XmppManagerBase>(String id) {
return _managers[id] as T?;
}
Future<void> register(XmppManagerBase manager) async {
Future<void> register(List<XmppManagerBase> managers) async {
for (final manager in managers) {
manager.register(
XmppManagerAttributes(
sendStanza: _sendStanza,
@ -54,19 +50,22 @@ class TestingManagerHolder {
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
_socket,
socket,
),
getConnectionSettings: () => settings,
sendNonza: (_) {},
sendEvent: (_) {},
getSocket: () => _socket,
sendEvent: sentEvents.add,
getSocket: () => socket,
getNegotiatorById: getNegotiatorNullStub,
getFullJID: () => jid,
getManagerById: _getManagerById,
),
);
await manager.postRegisterCallback();
_managers[manager.id] = manager;
}
for (final manager in managers) {
await manager.postRegisterCallback();
}
}
}

View File

@ -16,8 +16,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
);
@ -38,8 +38,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagXmlns: 'owo',
);
@ -59,8 +59,8 @@ void main() {
return StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
);
},
stanzaTag: 'iq',
@ -77,8 +77,8 @@ void main() {
StanzaHandlerData(
false,
false,
null,
stanza2,
TypedMap(),
),
);
expect(run, true);
@ -89,8 +89,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagName: 'tag',
);
@ -107,8 +107,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagName: 'tag',
stanzaTag: 'iq',
@ -127,8 +127,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
xmlns: componentAcceptXmlns,
);
@ -147,8 +147,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagName: '1',
priority: 100,
@ -157,8 +157,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagName: '2',
),
@ -166,8 +166,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData(
true,
false,
null,
stanza,
TypedMap(),
),
tagName: '3',
priority: 50,

View File

@ -0,0 +1,39 @@
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:test/test.dart';
abstract class BaseType {}
class TestType1 implements BaseType {
const TestType1(this.i);
final int i;
}
class TestType2 implements BaseType {
const TestType2(this.j);
final bool j;
}
void main() {
test('Test storing data in the type map', () {
// Set
final map = TypedMap<BaseType>()
..set(const TestType1(1))
..set(const TestType2(false));
// And access
expect(map.get<TestType1>()?.i, 1);
expect(map.get<TestType2>()?.j, false);
});
test('Test storing data in the type map using a list', () {
// Set
final map = TypedMap<BaseType>.fromList([
const TestType1(1),
const TestType2(false),
]);
// And access
expect(map.get<TestType1>()?.i, 1);
expect(map.get<TestType2>()?.j, false);
});
}

View File

@ -120,8 +120,7 @@ void main() {
final ecm = EntityCapabilitiesManager('');
final dm = DiscoManager([]);
await tm.register(dm);
await tm.register(ecm);
await tm.register([dm, ecm]);
// Inject a capability hash into the cache
final aliceJid = JID.fromString('alice@example.org/abc123');
@ -140,7 +139,7 @@ void main() {
// Query Alice's device
final result = await dm.discoInfoQuery(aliceJid);
expect(result.isType<DiscoError>(), false);
expect(tm.sentStanzas, 0);
expect(tm.socket.getState(), 0);
});
});
}

View File

@ -55,8 +55,10 @@ void main() {
() async {
final manager = PubSubManager();
final tm = TestingManagerHolder();
await tm.register(StubbedDiscoManager(false));
await tm.register(manager);
await tm.register([
StubbedDiscoManager(false),
manager,
]);
final result = await manager.preprocessPublishOptions(
JID.fromString('pubsub.server.example.org'),
@ -72,8 +74,10 @@ void main() {
() async {
final manager = PubSubManager();
final tm = TestingManagerHolder();
await tm.register(StubbedDiscoManager(true));
await tm.register(manager);
await tm.register([
StubbedDiscoManager(true),
manager,
]);
final result = await manager.preprocessPublishOptions(
JID.fromString('pubsub.server.example.org'),
@ -168,7 +172,6 @@ void main() {
PubSubManager(),
DiscoManager([]),
PresenceManager(),
MessageManager(),
RosterManager(TestingRosterStateManager(null, [])),
]);
await connection.registerFeatureNegotiators([

View File

@ -319,13 +319,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(StubbedDiscoManager());
await tm.register(manager);
await tm.register([
StubbedDiscoManager(),
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -338,8 +337,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(
@ -352,13 +353,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(StubbedDiscoManager());
await tm.register(manager);
await tm.register([
StubbedDiscoManager(),
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -371,8 +371,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(
@ -385,15 +387,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..multipleEqualIdentities = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -406,8 +405,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(
@ -420,15 +421,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..multipleEqualFeatures = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -441,8 +439,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(
@ -455,15 +455,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..multipleExtendedFormsWithSameType = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -476,8 +473,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(
@ -490,15 +489,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..invalidExtension1 = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -511,8 +507,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
@ -527,15 +525,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..invalidExtension2 = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -548,8 +543,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
@ -564,15 +561,12 @@ void main() {
final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager('');
await tm.register(
await tm.register([
StubbedDiscoManager()..invalidExtension3 = true,
);
await tm.register(manager);
manager,
]);
await manager.onPresence(
PresenceReceivedEvent(
aliceJid,
Stanza.presence(
final stanza = Stanza.presence(
from: aliceJid.toString(),
children: [
XMLNode.xmlns(
@ -585,8 +579,10 @@ void main() {
},
),
],
),
),
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
);
expect(

View File

@ -15,8 +15,8 @@ Future<void> runIncomingStanzaHandlers(
StanzaHandlerData(
false,
false,
null,
stanza,
TypedMap(),
),
);
}
@ -34,8 +34,8 @@ Future<void> runOutgoingStanzaHandlers(
StanzaHandlerData(
false,
false,
null,
stanza,
TypedMap(),
),
);
}

View File

@ -0,0 +1,147 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart';
void main() {
test('Test receiving a message processing hint', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
],
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
fakeSocket,
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
);
await conn.registerManagers([
MessageManager(),
MessageProcessingHintManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
await conn.connect(
shouldReconnect: false,
enableReconnectOnSuccess: false,
waitUntilLogin: true,
);
MessageEvent? messageEvent;
conn.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
messageEvent = event;
}
});
// Send the fake message
fakeSocket.injectRawXml(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<no-copy xmlns="urn:xmpp:hints"/>
<no-store xmlns="urn:xmpp:hints"/>
</message>
''',
);
await Future<void>.delayed(const Duration(seconds: 2));
expect(
messageEvent!.extensions
.get<MessageProcessingHintData>()!
.hints
.contains(MessageProcessingHint.noCopies),
true,
);
expect(
messageEvent!.extensions
.get<MessageProcessingHintData>()!
.hints
.contains(MessageProcessingHint.noStore),
true,
);
});
test('Test sending a message processing hint', () async {
final manager = MessageManager();
final holder = TestingManagerHolder(
stubSocket: StubTCPSocket([
StanzaExpectation(
'''
<message to="user@example.org" type="chat">
<no-copy xmlns="urn:xmpp:hints"/>
<no-store xmlns="urn:xmpp:hints"/>
</message>
''',
'',
)
]),
);
await holder.register([
manager,
MessageProcessingHintManager(),
]);
await manager.sendMessage(
JID.fromString('user@example.org'),
TypedMap()
..set(
const MessageProcessingHintData([
MessageProcessingHint.noCopies,
MessageProcessingHint.noStore,
]),
),
);
});
}

View File

@ -13,7 +13,7 @@ void main() {
<size>3032449</size>
<dimensions>4096x2160</dimensions>
<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>
<hash xmlns='urn:xmpp:hashes:2' algo='id-blake2b256'>2AfMGH8O7UNPTvUVAM9aK13mpCY=</hash>
<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>2AfMGH8O7UNPTvUVAM9aK13mpCY=</hash>
<desc>Photo from the summit.</desc>
<thumbnail xmlns='urn:xmpp:thumbs:1' uri='cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org' media-type='image/png' width='128' height='96'/>
</file>
@ -28,11 +28,11 @@ void main() {
);
expect(
sfs.metadata.hashes['sha3-256'],
sfs.metadata.hashes[HashFunction.sha3_256],
'2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=',
);
expect(
sfs.metadata.hashes['id-blake2b256'],
sfs.metadata.hashes[HashFunction.blake2b256],
'2AfMGH8O7UNPTvUVAM9aK13mpCY=',
);
});

View File

@ -1,7 +1,13 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart';
void main() {
initLogger();
test('Test parsing a large sticker pack', () {
// Example sticker pack based on the "miho" sticker pack by Movim
final rawPack = XMLNode.fromString('''
@ -225,4 +231,186 @@ void main() {
expect(pack.stickers.length, 16);
});
test('Test sending a sticker', () async {
final manager = MessageManager();
final holder = TestingManagerHolder(
stubSocket: StubTCPSocket([
StanzaExpectation(
// Example taken from https://xmpp.org/extensions/xep-0449.html#send
// - Replaced <dimensions /> with <width /> and <height />
'''
<message to="user@example.org" type="chat">
<sticker xmlns='urn:xmpp:stickers:0' pack='EpRv28DHHzFrE4zd+xaNpVb4' />
<file-sharing xmlns='urn:xmpp:sfs:0'>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/png</media-type>
<desc>😘</desc>
<size>67016</size>
<width>512</width>
<height>512</height>
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=</hash>
</file>
<sources>
<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png' />
</sources>
</file-sharing>
</message>
''',
'',
),
]),
);
await holder.register([
manager,
StickersManager(),
SFSManager(),
]);
await manager.sendMessage(
JID.fromString('user@example.org'),
TypedMap()
..set(
StickersData(
'EpRv28DHHzFrE4zd+xaNpVb4',
StatelessFileSharingData(
const FileMetadataData(
mediaType: 'image/png',
desc: '😘',
size: 67016,
width: 512,
height: 512,
hashes: {
HashFunction.sha256:
'gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=',
},
thumbnails: [],
),
[
StatelessFileSharingUrlSource(
'https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png',
),
],
),
),
),
);
await Future<void>.delayed(const Duration(seconds: 1));
expect(holder.socket.getState(), 1);
});
test('Test receiving a sticker', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
],
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
fakeSocket,
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
);
await conn.registerManagers([
MessageManager(),
SFSManager(),
StickersManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
await conn.connect(
shouldReconnect: false,
enableReconnectOnSuccess: false,
waitUntilLogin: true,
);
MessageEvent? messageEvent;
conn.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
messageEvent = event;
}
});
// Send the fake message
fakeSocket.injectRawXml(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<sticker xmlns='urn:xmpp:stickers:0' pack='EpRv28DHHzFrE4zd+xaNpVb4' />
<file-sharing xmlns='urn:xmpp:sfs:0'>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/png</media-type>
<desc>😘</desc>
<size>67016</size>
<width>512</width>
<height>512</height>
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>gw+6xdCgOcvCYSKuQNrXH33lV9NMzuDf/s0huByCDsY=</hash>
</file>
<sources>
<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/51078299-d071-46e1-b6d3-3de4a8ab67d6/sticker_marsey_kiss.png' />
</sources>
</file-sharing>
</message>
''',
);
await Future<void>.delayed(const Duration(seconds: 2));
final sticker = messageEvent!.extensions.get<StickersData>()!;
final sfs = messageEvent!.extensions.get<StatelessFileSharingData>()!;
expect(sticker.stickerPackId, 'EpRv28DHHzFrE4zd+xaNpVb4');
expect(sfs.metadata.desc, '😘');
expect(
sfs.sources.first is StatelessFileSharingUrlSource,
true,
);
});
}

View File

@ -1,6 +1,8 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/xmpp.dart';
void main() {
test('Test building a singleline quote', () {
final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!');
@ -20,28 +22,250 @@ void main() {
});
test('Applying a singleline quote', () {
const body = '> Hallo Welt\nHello right back!';
const reply = ReplyData(
to: '',
id: '',
'',
start: 0,
end: 13,
body: '> Hallo Welt\nHello right back!',
);
final bodyWithoutFallback = reply.removeFallback(body);
expect(bodyWithoutFallback, 'Hello right back!');
expect(reply.withoutFallback, 'Hello right back!');
});
test('Applying a multiline quote', () {
const body = "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!";
const reply = ReplyData(
to: '',
id: '',
'',
start: 0,
end: 28,
body: "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!",
);
final bodyWithoutFallback = reply.removeFallback(body);
expect(bodyWithoutFallback, "I'm fine.\nThank you!");
expect(reply.withoutFallback, "I'm fine.\nThank you!");
});
test('Test calling the message sending callback', () {
final result = MessageRepliesManager().messageSendingCallback(
TypedMap()
..set(
ReplyData.fromQuoteData(
'some-random-id',
QuoteData.fromBodies(
'Hello world',
'How are you doing?',
),
jid: JID.fromString('quoted-user@example.org'),
),
),
);
final reply = result.firstWhere((e) => e.tag == 'reply');
final body = result.firstWhere((e) => e.tag == 'body');
final fallback = result.firstWhere((e) => e.tag == 'fallback');
expect(reply.attributes['to'], 'quoted-user@example.org');
expect(body.innerText(), '> Hello world\nHow are you doing?');
expect(fallback.children.first.attributes['start'], '0');
expect(fallback.children.first.attributes['end'], '14');
});
test('Test parsing a reply without fallback', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
],
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
fakeSocket,
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
);
await conn.registerManagers([
MessageManager(),
MessageRepliesManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
await conn.connect(
shouldReconnect: false,
enableReconnectOnSuccess: false,
waitUntilLogin: true,
);
MessageEvent? messageEvent;
conn.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
messageEvent = event;
}
});
// Send the fake message
fakeSocket.injectRawXml(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<body>Great idea!</body>
<reply to='anna@example.com/tablet' id='message-id1' xmlns='urn:xmpp:reply:0' />
</message>
''',
);
await Future<void>.delayed(const Duration(seconds: 2));
final reply = messageEvent!.extensions.get<ReplyData>()!;
expect(reply.withoutFallback, 'Great idea!');
expect(reply.id, 'message-id1');
expect(reply.jid, JID.fromString('anna@example.com/tablet'));
expect(reply.start, null);
expect(reply.end, null);
});
test('Test parsing a reply with a fallback', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
],
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
fakeSocket,
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
);
await conn.registerManagers([
MessageManager(),
MessageRepliesManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
await conn.connect(
shouldReconnect: false,
enableReconnectOnSuccess: false,
waitUntilLogin: true,
);
MessageEvent? messageEvent;
conn.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
messageEvent = event;
}
});
// Send the fake message
fakeSocket.injectRawXml(
'''
<message id="aaaaaaaaa" from="user@example.org" to="polynomdivision@test.server/abc123" type="chat">
<body>> Anna wrote:\n> We should bake a cake\nGreat idea!</body>
<reply to='anna@example.com/laptop' id='message-id1' xmlns='urn:xmpp:reply:0' />
<fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="38" />
</fallback>
</message>
''',
);
await Future<void>.delayed(const Duration(seconds: 2));
final reply = messageEvent!.extensions.get<ReplyData>()!;
expect(reply.withoutFallback, 'Great idea!');
expect(reply.id, 'message-id1');
expect(reply.jid, JID.fromString('anna@example.com/laptop'));
expect(reply.start, 0);
expect(reply.end, 38);
});
}

View File

@ -78,8 +78,8 @@ Future<bool> testRosterManager(
StanzaHandlerData(
false,
false,
null,
stanza,
TypedMap(),
),
);
}
@ -335,8 +335,8 @@ void main() {
StanzaHandlerData(
false,
false,
null,
maliciousStanza,
TypedMap(),
),
);
}
@ -793,4 +793,20 @@ void main() {
expect(fakeSocket.getState(), 9);
expect(await stanzaFuture != null, true);
});
test('Test subscription pre-approval parsing', () async {
final rawFeatures = XMLNode.fromString(
'''
<top-level>
<test-feature-1 xmlns="invalid:urn:features:1" />
<test-feature-2 xmlns="invalid:urn:features:2" />
<test-feature-3 xmlns="invalid:urn:features:3" />
<test-feature-4 xmlns="invalid:urn:features:4" />
<sub xmlns='urn:xmpp:features:pre-approval' />
</top-level>
''',
);
expect(PresenceNegotiator().matchesFeature(rawFeatures.children), true);
});
}