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
This commit is contained in:
PapaTutuWawa 2023-06-07 21:44:28 +00:00
commit 327f695a40
56 changed files with 2386 additions and 1944 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**: 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` - **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
@ -11,6 +11,11 @@
- **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`. - **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`.
- **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument. - **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument.
- Sent stanzas are now kept in a queue until sent. - 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 ## 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/managers/priorities.dart';
export 'package:moxxmpp/src/message.dart'; export 'package:moxxmpp/src/message.dart';
export 'package:moxxmpp/src/namespaces.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/namespaces.dart';
export 'package:moxxmpp/src/negotiators/negotiator.dart'; export 'package:moxxmpp/src/negotiators/negotiator.dart';
export 'package:moxxmpp/src/ping.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/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart'; export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.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/extensible_file_thumbnails.dart';
export 'package:moxxmpp/src/xeps/staging/fast.dart'; export 'package:moxxmpp/src/xeps/staging/fast.dart';
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.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/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/queue.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_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_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart'; import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
@ -405,8 +407,7 @@ class XmppConnection {
/// Returns true if we can send data through the socket. /// Returns true if we can send data through the socket.
Future<bool> _canSendData() async { Future<bool> _canSendData() async {
return [XmppConnectionState.connected, XmppConnectionState.connecting] return await getConnectionState() == XmppConnectionState.connected;
.contains(await getConnectionState());
} }
/// Sends a stanza described by [details] to the server. Until sent, the stanza is /// Sends a stanza described by [details] to the server. Until sent, the stanza is
@ -424,13 +425,17 @@ class XmppConnection {
); );
final completer = details.awaitable ? Completer<XMLNode>() : null; final completer = details.awaitable ? Completer<XMLNode>() : null;
await _stanzaQueue.enqueueStanza( final entry = StanzaQueueEntry(
StanzaQueueEntry( details,
details, completer,
completer,
),
); );
if (details.bypassQueue) {
await _sendStanzaImpl(entry);
} else {
await _stanzaQueue.enqueueStanza(entry);
}
return completer?.future; return completer?.future;
} }
@ -471,8 +476,8 @@ class XmppConnection {
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
false, false,
null,
newStanza, newStanza,
TypedMap(),
encrypted: details.encrypted, encrypted: details.encrypted,
forceEncryption: details.forceEncryption, forceEncryption: details.forceEncryption,
), ),
@ -523,18 +528,20 @@ class XmppConnection {
if (await _canSendData()) { if (await _canSendData()) {
_socket.write(data.stanza.toXml()); _socket.write(data.stanza.toXml());
} else { } else {
_log.fine('Not sending dat as _canSendData() returned false.'); _log.fine('Not sending data as _canSendData() returned false.');
} }
// Run post-send handlers // Run post-send handlers
_log.fine('Running post stanza handlers..'); _log.fine('Running post stanza handlers..');
final extensions = TypedMap<StanzaHandlerExtension>()
..set(StreamManagementData(details.excludeFromStreamManagement));
await _runOutgoingPostStanzaHandlers( await _runOutgoingPostStanzaHandlers(
newStanza, newStanza,
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
false, false,
null,
newStanza, newStanza,
extensions,
), ),
); );
_log.fine('Done'); _log.fine('Done');
@ -649,7 +656,7 @@ class XmppConnection {
Stanza stanza, { Stanza stanza, {
StanzaHandlerData? initial, StanzaHandlerData? initial,
}) async { }) async {
var state = initial ?? StanzaHandlerData(false, false, null, stanza); var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap());
for (final handler in handlers) { for (final handler in handlers) {
if (handler.matches(state.stanza)) { if (handler.matches(state.stanza)) {
state = await handler.callback(state.stanza, state); state = await handler.callback(state.stanza, state);
@ -724,7 +731,7 @@ class XmppConnection {
// it. // it.
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza); final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted && final prefix = incomingPreHandlers.encrypted &&
incomingPreHandlers.other['encryption_error'] == null incomingPreHandlers.encryptionError == null
? '(Encrypted) ' ? '(Encrypted) '
: ''; : '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
@ -743,10 +750,10 @@ class XmppConnection {
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
incomingPreHandlers.cancel, incomingPreHandlers.cancel,
incomingPreHandlers.cancelReason,
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted, encrypted: incomingPreHandlers.encrypted,
other: incomingPreHandlers.other, cancelReason: incomingPreHandlers.cancelReason,
), ),
); );
if (!incomingHandlers.done) { if (!incomingHandlers.done) {
@ -835,7 +842,7 @@ class XmppConnection {
await _reconnectionPolicy.setShouldReconnect(false); await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) { if (triggeredByUser) {
getPresenceManager()?.sendUnavailablePresence(); await getPresenceManager()?.sendUnavailablePresence();
} }
_socket.prepareDisconnect(); _socket.prepareDisconnect();

View File

@ -4,18 +4,11 @@ import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart'; import 'package:moxxmpp/src/roster/roster.dart';
import 'package:moxxmpp/src/stanza.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_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0084.dart';
import 'package:moxxmpp/src/xeps/xep_0085.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_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';
abstract class XmppEvent {} abstract class XmppEvent {}
@ -74,60 +67,42 @@ class RosterUpdatedEvent extends XmppEvent {
/// Triggered when a message is received /// Triggered when a message is received
class MessageEvent extends XmppEvent { class MessageEvent extends XmppEvent {
MessageEvent({ MessageEvent(
required this.body, this.from,
required this.fromJid, this.to,
required this.toJid, this.id,
required this.sid, this.encrypted,
required this.isCarbon, this.extensions, {
required this.deliveryReceiptRequested,
required this.isMarkable,
required this.encrypted,
required this.other,
this.originId,
this.stanzaIds,
this.error,
this.type, this.type,
this.oob, this.error,
this.sfs, this.encryptionError,
this.sims,
this.reply,
this.chatState,
this.fun,
this.funReplacement,
this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
}); });
final StanzaError? error;
final String body; /// The from attribute of the message.
final JID fromJid; final JID from;
final JID toJid;
final String sid; /// 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? type;
final String? originId;
final List<StanzaId>? stanzaIds; final StanzaError? error;
final bool isCarbon;
final bool deliveryReceiptRequested; /// Flag indicating whether the message was encrypted.
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 bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId; /// The error in case an encryption error occurred.
final MessageReactions? messageReactions; final Object? encryptionError;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId; /// Data added by other handlers.
final Map<String, dynamic> other; 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 /// Triggered when a client responds to our delivery receipt request
@ -138,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent {
} }
class ChatMarkerEvent extends XmppEvent { class ChatMarkerEvent extends XmppEvent {
ChatMarkerEvent({ ChatMarkerEvent(
required this.type, this.from,
required this.from, this.type,
required this.id, this.id,
}); );
/// The entity that sent the chat marker.
final JID from; 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; final String id;
} }
@ -168,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent {
final String resource; 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 /// Triggered when we are starting an connection attempt
class ConnectingEvent extends XmppEvent {} class ConnectingEvent extends XmppEvent {}
@ -192,15 +166,35 @@ class SubscriptionRequestReceivedEvent extends XmppEvent {
final JID from; final JID from;
} }
/// Triggered when we receive a new or updated avatar /// Triggered when we receive a new or updated avatar via XEP-0084
class AvatarUpdatedEvent extends XmppEvent { class UserAvatarUpdatedEvent extends XmppEvent {
AvatarUpdatedEvent({ UserAvatarUpdatedEvent(
required this.jid, this.jid,
required this.base64, this.metadata,
required this.hash, );
});
final String jid; /// The JID of the user updating their avatar.
final JID jid;
/// The metadata of the avatar.
final List<UserAvatarMetadata> metadata;
}
/// Triggered when we receive a new or updated avatar via XEP-0054
class VCardAvatarUpdatedEvent extends XmppEvent {
VCardAvatarUpdatedEvent(
this.jid,
this.base64,
this.hash,
);
/// The JID of the entity that updated their avatar.
final JID jid;
/// The base64-encoded avatar data.
final String base64; final String base64;
/// The SHA-1 hash of the avatar.
final String hash; final String hash;
} }

View File

@ -1,79 +1,47 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/util/typed_map.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';
part 'data.freezed.dart'; abstract class StanzaHandlerExtension {}
@freezed class StanzaHandlerData {
class StanzaHandlerData with _$StanzaHandlerData { StanzaHandlerData(
factory StanzaHandlerData( this.done,
// Indicates to the runner that processing is now done. This means that all this.cancel,
// pre-processing is done and no other handlers should be consulted. this.stanza,
bool done, this.extensions, {
// Indicates to the runner that processing is to be cancelled and no further handlers this.cancelReason,
// should run. The stanza also will not be sent. this.encryptionError,
bool cancel, this.encrypted = false,
// The reason why we cancelled the processing and sending this.forceEncryption = false,
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,
// XEP-0359 <origin-id />'s id attribute, if available. /// Indicates to the runner that processing is now done. This means that all
String? originId, /// pre-processing is done and no other handlers should be consulted.
bool done;
// XEP-0359 <stanza-id /> elements, if available. /// Indicates to the runner that processing is to be cancelled and no further handlers
List<StanzaId>? stanzaIds, /// should run. The stanza also will not be sent.
ReplyData? reply, bool cancel;
ChatState? chatState,
@Default(false) bool isCarbon, /// The reason why we cancelled the processing and sending.
@Default(false) bool deliveryReceiptRequested, Object? cancelReason;
@Default(false) bool isMarkable,
// File Upload Notifications /// The reason why an encryption or decryption failed.
// A notification Object? encryptionError;
FileMetadataData? fun,
// The stanza id this replaces /// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is
String? funReplacement, /// absolutely necessary, e.g. with Message Carbons or OMEMO.
// The stanza id this cancels Stanza stanza;
String? funCancellation,
// Whether the stanza was received encrypted /// Whether the stanza was received encrypted
@Default(false) bool encrypted, 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 // If true, forces the encryption manager to encrypt to the JID, even if it
// but forceEncryption is true, then the OMEMO manager will try to encrypt // would not normally. In the case of OMEMO: If shouldEncrypt returns false
// to the JID anyway. // but forceEncryption is true, then the OMEMO manager will try to encrypt
@Default(false) bool forceEncryption, // to the JID anyway.
// The stated type of encryption used, if any was used bool forceEncryption;
ExplicitEncryptionType? encryptionType,
// Delayed Delivery /// Additional data from other managers.
DelayedDelivery? delayedDelivery, final TypedMap<StanzaHandlerExtension> extensions;
// 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,
}) = _StanzaHandlerData;
} }

View File

@ -1,793 +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;
@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});
}
/// @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,
}) {
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?,
) 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});
}
/// @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,
}) {
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?,
));
}
}
/// @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})
: _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;
@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)';
}
@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));
}
@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
]);
@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}) = _$_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
@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 messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager'; const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; 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/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.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_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_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'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
/// Data used to build a message stanza. /// A callback that is called whenever a message is sent using
/// /// [MessageManager.sendMessage]. The input the typed map that is passed to
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be /// sendMessage.
/// added. This is recommended when sharing files but may cause issues when the message typedef MessageSendingCallback = List<XMLNode> Function(
/// stanza should include a SFS element without any fallbacks. TypedMap<StanzaHandlerExtension>,
class MessageDetails { );
const MessageDetails({
required this.to, /// The raw content of the <body /> element.
this.body, class MessageBodyData implements StanzaHandlerExtension {
this.requestDeliveryReceipt = false, const MessageBodyData(this.body);
this.requestChatMarkers = true,
this.id, /// The content of the <body /> element.
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;
final String? body; final String? body;
final bool requestDeliveryReceipt;
final bool requestChatMarkers; XMLNode toXML() {
final String? id; return XMLNode(
final String? originId; tag: 'body',
final String? quoteBody; text: body,
final String? quoteId; );
final String? quoteFrom; }
final ChatState? chatState; }
final StatelessFileSharingData? sfs;
final FileMetadataData? fun; /// The id attribute of the message stanza.
final String? funReplacement; class MessageIdData implements StanzaHandlerExtension {
final String? funCancellation; const MessageIdData(this.id);
final bool shouldEncrypt;
final MessageRetractionData? messageRetraction; /// The id attribute of the stanza.
final String? lastMessageCorrectionId; final String id;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
MessageManager() : super(messageManager); 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 @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
callback: _onMessage, callback: _onMessage,
priority: -100, priority: messageHandlerPriority,
) )
]; ];
@ -94,238 +76,69 @@ class MessageManager extends XmppManagerBase {
Stanza _, Stanza _,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) 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( getAttributes().sendEvent(
MessageEvent( MessageEvent(
body: body != null ? body.innerText() : '', JID.fromString(state.stanza.attributes['from']! as String),
fromJid: JID.fromString(message.attributes['from']! as String), JID.fromString(state.stanza.attributes['to']! as String),
toJid: JID.fromString(message.attributes['to']! as String), state.stanza.attributes['id']! as String,
sid: message.attributes['id']! as String, state.encrypted,
originId: state.originId, state.extensions,
stanzaIds: state.stanzaIds, type: state.stanza.attributes['type'] as String?,
isCarbon: state.isCarbon, error: StanzaError.fromStanza(state.stanza),
deliveryReceiptRequested: state.deliveryReceiptRequested, encryptionError: state.encryptionError,
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),
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
/// Send a message to to with the content body. If deliveryRequest is true, then /// Send an unawaitable message to [to]. [extensions] is a typed map that contains
/// the message will also request a delivery receipt from the receiver. /// data for building the message.
/// If id is non-null, then it will be the id of the message stanza. Future<void> sendMessage(
/// element to this id. If originId is non-null, then it will create an "origin-id" JID to,
/// child in the message stanza and set its id to originId. TypedMap<StanzaHandlerExtension> extensions,
void sendMessage(MessageDetails details) { ) async {
assert( await getAttributes().sendStanza(
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(
StanzaDetails( StanzaDetails(
stanza, Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: 'chat',
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
awaitable: false, 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 // RFC 6121
const rosterXmlns = 'jabber:iq:roster'; const rosterXmlns = 'jabber:iq:roster';
const rosterVersioningXmlns = 'urn:xmpp:features:rosterver'; const rosterVersioningXmlns = 'urn:xmpp:features:rosterver';
const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
// XEP-0004 // XEP-0004
const dataFormsXmlns = 'jabber:x:data'; const dataFormsXmlns = 'jabber:x:data';
@ -96,7 +97,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0';
// XEP-0372 // XEP-0372
const referenceXmlns = 'urn:xmpp:reference:0'; const referenceXmlns = 'urn:xmpp:reference:0';
// XEP-380 // XEP-0380
const emeXmlns = 'urn:xmpp:eme:0'; const emeXmlns = 'urn:xmpp:eme:0';
const emeOtr = 'urn:xmpp:otr:0'; const emeOtr = 'urn:xmpp:otr:0';
const emeLegacyOpenPGP = 'jabber:x:encrypted'; const emeLegacyOpenPGP = 'jabber:x:encrypted';

View File

@ -11,3 +11,4 @@ const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
const bind2Negotiator = 'org.moxxmpp.bind2'; const bind2Negotiator = 'org.moxxmpp.bind2';
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast'; const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons'; 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/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A function that will be called when presence, outside of subscription request /// 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 /// management, will be sent. Useful for managers that want to add [XMLNode]s to said
/// presence. /// presence.
typedef PresencePreSendCallback = Future<List<XMLNode>> Function(); 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 /// A mandatory manager that handles initial presence sending, sending of subscription
/// request management requests and triggers events for incoming presence stanzas. /// request management requests and triggers events for incoming presence stanzas.
class PresenceManager extends XmppManagerBase { class PresenceManager extends XmppManagerBase {
@ -23,11 +53,17 @@ class PresenceManager extends XmppManagerBase {
final List<PresencePreSendCallback> _presenceCallbacks = final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true); 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 @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'presence', stanzaTag: 'presence',
callback: _onPresence, callback: _onPresence,
priority: presenceHandlerPriority,
) )
]; ];
@ -66,7 +102,7 @@ class PresenceManager extends XmppManagerBase {
from: JID.fromString(presence.from!), from: JID.fromString(presence.from!),
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
default: default:
break; break;
@ -75,10 +111,7 @@ class PresenceManager extends XmppManagerBase {
if (presence.from != null) { if (presence.from != null) {
logger.finest("Received presence from '${presence.from}'"); logger.finest("Received presence from '${presence.from}'");
getAttributes().sendEvent( return state..done = true;
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
);
return state.copyWith(done: true);
} }
return state; return state;
@ -112,24 +145,82 @@ class PresenceManager extends XmppManagerBase {
} }
/// Send an unavailable presence with no 'to' attribute. /// Send an unavailable presence with no 'to' attribute.
void sendUnavailablePresence() { Future<void> sendUnavailablePresence() async {
getAttributes().sendStanza( // Bypass the queue so that this get's sent immediately.
// If we do it like this, we can also block the disconnection
// until we're actually ready.
await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.presence( Stanza.presence(
type: 'unavailable', type: 'unavailable',
), ),
awaitable: false, awaitable: false,
bypassQueue: true,
excludeFromStreamManagement: true,
),
);
}
/// 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: '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,
), ),
); );
} }
/// Sends a subscription request to [to]. /// Accept a subscription request from [to].
void sendSubscriptionRequest(String to) { Future<void> acceptSubscriptionRequest(JID to) async {
getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.presence( Stanza.presence(
type: 'subscribe', type: 'subscribed',
to: to, to: to.toString(),
),
awaitable: false,
),
);
}
/// Send a subscription request rejection to [to].
Future<void> rejectSubscriptionRequest(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'unsubscribed',
to: to.toString(),
), ),
awaitable: false, awaitable: false,
), ),
@ -137,38 +228,12 @@ class PresenceManager extends XmppManagerBase {
} }
/// Sends an unsubscription request to [to]. /// Sends an unsubscription request to [to].
void sendUnsubscriptionRequest(String to) { Future<void> unsubscribe(JID to) async {
getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.presence( Stanza.presence(
type: 'unsubscribe', type: 'unsubscribe',
to: to, to: to.toString(),
),
awaitable: false,
),
);
}
/// Accept a presence subscription request for [to].
void sendSubscriptionRequestApproval(String to) {
getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribed',
to: to,
),
awaitable: false,
),
);
}
/// Reject a presence subscription request for [to].
void sendSubscriptionRequestRejection(String to) {
getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'unsubscribed',
to: to,
), ),
awaitable: false, awaitable: false,
), ),

View File

@ -145,7 +145,7 @@ class RosterManager extends XmppManagerBase {
logger.warning( logger.warning(
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}', 'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
); );
return state.copyWith(done: true); return state..done = true;
} }
final query = stanza.firstTag('query', xmlns: rosterXmlns)!; final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
@ -154,7 +154,7 @@ class RosterManager extends XmppManagerBase {
if (item == null) { if (item == null) {
logger.warning('Received empty roster push'); logger.warning('Received empty roster push');
return state.copyWith(done: true); return state..done = true;
} }
unawaited( 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 /// Shared code between requesting rosters without and with roster versioning, if
@ -223,15 +223,21 @@ class RosterManager extends XmppManagerBase {
return Result(result); return Result(result);
} }
/// Requests the roster following RFC 6121. /// Requests the roster following RFC 6121. If [useRosterVersion] is set to false, then
Future<Result<RosterRequestResult, RosterError>> requestRoster() async { /// roster versioning will not be used, even if the server supports it and we have a last
/// known roster version.
Future<Result<RosterRequestResult, RosterError>> requestRoster({
bool useRosterVersion = true,
}) async {
final attrs = getAttributes(); final attrs = getAttributes();
final query = XMLNode.xmlns( final query = XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: rosterXmlns, xmlns: rosterXmlns,
); );
final rosterVersion = await _stateManager.getRosterVersion(); final rosterVersion = await _stateManager.getRosterVersion();
if (rosterVersion != null && rosterVersioningAvailable()) { if (rosterVersion != null &&
rosterVersioningAvailable() &&
useRosterVersion) {
query.attributes['ver'] = rosterVersion; query.attributes['ver'] = rosterVersion;
} }

View File

@ -9,6 +9,8 @@ class StanzaDetails {
this.awaitable = true, this.awaitable = true,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false, this.forceEncryption = false,
this.bypassQueue = false,
this.excludeFromStreamManagement = false,
}); });
/// The stanza to send. /// The stanza to send.
@ -23,6 +25,16 @@ class StanzaDetails {
final bool encrypted; final bool encrypted;
final bool forceEncryption; final bool forceEncryption;
/// Bypasses being put into the queue. Useful for sending stanzas that must go out
/// now, where it's okay if it does not get sent.
/// This should never have to be set to true.
final bool bypassQueue;
/// This makes the Stream Management implementation, when available, ignore the stanza,
/// meaning that it gets counted but excluded from resending.
/// This should never have to be set to true.
final bool excludeFromStreamManagement;
} }
/// A simple description of the <error /> element that may be inside a stanza /// A simple description of the <error /> element that may be inside a stanza

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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0446.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 /// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; 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 { class FileUploadNotificationManager extends XmppManagerBase {
FileUploadNotificationManager() : super(fileUploadNotificationManager); FileUploadNotificationManager() : super(fileUploadNotificationManager);
@ -47,11 +103,14 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final funElement = final funElement =
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
fun: FileMetadataData.fromXML( ..extensions.set(
funElement.firstTag('file', xmlns: fileMetadataXmlns)!, FileUploadNotificationData(
), FileMetadataData.fromXML(
); funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
),
),
);
} }
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived( Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(
@ -60,9 +119,12 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final element = final element =
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
funReplacement: element.attributes['id']! as String, ..extensions.set(
); FileUploadNotificationReplacementData(
element.attributes['id']! as String,
),
);
} }
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived( Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(
@ -71,8 +133,42 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final element = final element =
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
funCancellation: element.attributes['id']! as String, ..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( await reply(
@ -195,7 +195,7 @@ class DiscoManager extends XmppManagerBase {
], ],
); );
return state.copyWith(done: true); return state..done = true;
} }
Future<StanzaHandlerData> _onDiscoItemsRequest( Future<StanzaHandlerData> _onDiscoItemsRequest(
@ -223,7 +223,7 @@ class DiscoManager extends XmppManagerBase {
], ],
); );
return state.copyWith(done: true); return state..done = true;
} }
return state; return state;

View File

@ -66,11 +66,7 @@ class VCardManager extends XmppManagerBase {
final binval = vcardResult.get<VCard>().photo?.binval; final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) { if (binval != null) {
getAttributes().sendEvent( getAttributes().sendEvent(
AvatarUpdatedEvent( VCardAvatarUpdatedEvent(JID.fromString(from), binval, hash),
jid: from,
base64: binval,
hash: hash,
),
); );
} else { } else {
logger.warning('No avatar data found'); logger.warning('No avatar data found');
@ -80,7 +76,7 @@ class VCardManager extends XmppManagerBase {
} }
} }
return state.copyWith(done: true); return state..done = true;
} }
VCardPhoto? _parseVCardPhoto(XMLNode? node) { 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 { Future<int> _getNodeItemCount(JID jid, String node) async {
@ -179,13 +179,13 @@ class PubSubManager extends XmppManagerBase {
return options; return options;
} }
Future<Result<PubSubError, bool>> subscribe(String jid, String node) async { Future<Result<PubSubError, bool>> subscribe(JID jid, String node) async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = (await attrs.sendStanza( final result = (await attrs.sendStanza(
StanzaDetails( StanzaDetails(
Stanza.iq( Stanza.iq(
type: 'set', type: 'set',
to: jid, to: jid.toString(),
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'pubsub', tag: 'pubsub',
@ -222,13 +222,13 @@ class PubSubManager extends XmppManagerBase {
return Result(subscription.attributes['subscription'] == 'subscribed'); return Result(subscription.attributes['subscription'] == 'subscribed');
} }
Future<Result<PubSubError, bool>> unsubscribe(String jid, String node) async { Future<Result<PubSubError, bool>> unsubscribe(JID jid, String node) async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = (await attrs.sendStanza( final result = (await attrs.sendStanza(
StanzaDetails( StanzaDetails(
Stanza.iq( Stanza.iq(
type: 'set', type: 'set',
to: jid, to: jid.toString(),
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'pubsub', tag: 'pubsub',
@ -398,14 +398,14 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, List<PubSubItem>>> getItems( Future<Result<PubSubError, List<PubSubItem>>> getItems(
String jid, JID jid,
String node, String node,
) async { ) async {
final result = (await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.iq( Stanza.iq(
type: 'get', type: 'get',
to: jid, to: jid.toString(),
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'pubsub', tag: 'pubsub',

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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A data class representing the jabber:x:oob tag. /// A data class representing the jabber:x:oob tag.
class OOBData { class OOBData implements StanzaHandlerExtension {
const OOBData({this.url, this.desc}); const OOBData(this.url, this.desc);
/// The communicated URL of the OOB data
final String? url; final String? url;
/// The description of the url.
final String? desc; final String? desc;
}
XMLNode constructOOBNode(OOBData data) { XMLNode toXML() {
final children = List<XMLNode>.empty(growable: true); return XMLNode.xmlns(
tag: 'x',
if (data.url != null) { xmlns: oobDataXmlns,
children.add(XMLNode(tag: 'url', text: data.url)); children: [
if (url != null) XMLNode(tag: 'url', text: url),
if (desc != null) XMLNode(tag: 'desc', text: desc),
],
);
} }
if (data.desc != null) {
children.add(XMLNode(tag: 'desc', text: data.desc));
}
return XMLNode.xmlns(
tag: 'x',
xmlns: oobDataXmlns,
children: children,
);
} }
class OOBManager extends XmppManagerBase { class OOBManager extends XmppManagerBase {
@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase {
final url = x.firstTag('url'); final url = x.firstTag('url');
final desc = x.firstTag('desc'); final desc = x.firstTag('desc');
return state.copyWith( return state
oob: OOBData( ..extensions.set(
url: url?.innerText(), OOBData(
desc: desc?.innerText(), 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

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -15,10 +16,17 @@ abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {} class UnknownAvatarError extends AvatarError {}
class UserAvatar { class UserAvatarData {
const UserAvatar({required this.base64, required this.hash}); const UserAvatarData(this.base64, this.hash);
/// The base64-encoded avatar data.
final String base64; final String base64;
/// The SHA-1 hash of the raw avatar data.
final String hash; final String hash;
/// The raw avatar data.
List<int> get data => base64Decode(base64);
} }
class UserAvatarMetadata { class UserAvatarMetadata {
@ -27,21 +35,44 @@ class UserAvatarMetadata {
this.length, this.length,
this.width, this.width,
this.height, this.height,
this.mime, this.type,
this.url,
); );
/// The amount of bytes in the file factory UserAvatarMetadata.fromXML(XMLNode node) {
assert(
node.tag == 'metadata' &&
node.attributes['xmlns'] == userAvatarMetadataXmlns,
'<metadata /> element required',
);
final width = node.attributes['width'] as String?;
final height = node.attributes['height'] as String?;
return UserAvatarMetadata(
node.attributes['id']! as String,
int.parse(node.attributes['bytes']! as String),
width != null ? int.parse(width) : null,
height != null ? int.parse(height) : null,
node.attributes['type']! as String,
node.attributes['url'] as String?,
);
}
/// The amount of bytes in the file.
final int length; final int length;
/// The identifier of the avatar /// The identifier of the avatar.
final String id; final String id;
/// Image proportions /// Image proportions.
final int width; final int? width;
final int height; final int? height;
/// The MIME type of the avatar /// The URL where the avatar can be found.
final String mime; final String? url;
/// The MIME type of the avatar.
final String type;
} }
/// NOTE: This class requires a PubSubManager /// NOTE: This class requires a PubSubManager
@ -51,13 +82,18 @@ class UserAvatarManager extends XmppManagerBase {
PubSubManager _getPubSubManager() => PubSubManager _getPubSubManager() =>
getAttributes().getManagerById(pubsubManager)! as PubSubManager; getAttributes().getManagerById(pubsubManager)! as PubSubManager;
@override
List<String> getDiscoFeatures() => [
'$userAvatarMetadataXmlns+notify',
];
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PubSubNotificationEvent) { if (event is PubSubNotificationEvent) {
if (event.item.node != userAvatarDataXmlns) return; if (event.item.node != userAvatarMetadataXmlns) return;
if (event.item.payload.tag != 'data' || if (event.item.payload.tag != 'metadata' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) { event.item.payload.attributes['xmlns'] != userAvatarMetadataXmlns) {
logger.warning( logger.warning(
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...', 'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
); );
@ -65,10 +101,12 @@ class UserAvatarManager extends XmppManagerBase {
} }
getAttributes().sendEvent( getAttributes().sendEvent(
AvatarUpdatedEvent( UserAvatarUpdatedEvent(
jid: event.from, JID.fromString(event.from),
base64: event.item.payload.innerText(), event.item.payload
hash: event.item.id, .findTags('metadata', xmlns: userAvatarMetadataXmlns)
.map(UserAvatarMetadata.fromXML)
.toList(),
), ),
); );
} }
@ -80,7 +118,7 @@ class UserAvatarManager extends XmppManagerBase {
/// Requests the avatar from [jid]. Returns the avatar data if the request was /// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise /// successful. Null otherwise
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async { Future<Result<AvatarError, UserAvatarData>> getUserAvatar(JID jid) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns); final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError()); if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
@ -90,9 +128,9 @@ class UserAvatarManager extends XmppManagerBase {
final item = results[0]; final item = results[0];
return Result( return Result(
UserAvatar( UserAvatarData(
base64: item.payload.innerText(), item.payload.innerText(),
hash: item.id, item.id,
), ),
); );
} }
@ -146,7 +184,7 @@ class UserAvatarManager extends XmppManagerBase {
'bytes': metadata.length.toString(), 'bytes': metadata.length.toString(),
'height': metadata.height.toString(), 'height': metadata.height.toString(),
'width': metadata.width.toString(), 'width': metadata.width.toString(),
'type': metadata.mime, 'type': metadata.type,
'id': metadata.id, 'id': metadata.id,
}, },
), ),
@ -163,14 +201,14 @@ class UserAvatarManager extends XmppManagerBase {
} }
/// Subscribe the data and metadata node of [jid]. /// Subscribe the data and metadata node of [jid].
Future<Result<AvatarError, bool>> subscribe(String jid) async { Future<Result<AvatarError, bool>> subscribe(JID jid) async {
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return const Result(true); return const Result(true);
} }
/// Unsubscribe the data and metadata node of [jid]. /// Unsubscribe the data and metadata node of [jid].
Future<Result<AvatarError, bool>> unsubscribe(String jid) async { Future<Result<AvatarError, bool>> unsubscribe(JID jid) async {
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return const Result(true); return const Result(true);

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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatState { active, composing, paused, inactive, gone } enum ChatState implements StanzaHandlerExtension {
active,
composing,
paused,
inactive,
gone;
ChatState chatStateFromString(String raw) { factory ChatState.fromName(String state) {
switch (raw) { switch (state) {
case 'active': case 'active':
{
return ChatState.active; return ChatState.active;
} case 'composing':
case 'composing':
{
return ChatState.composing; return ChatState.composing;
} case 'paused':
case 'paused':
{
return ChatState.paused; return ChatState.paused;
} case 'inactive':
case 'inactive':
{
return ChatState.inactive; return ChatState.inactive;
} case 'gone':
case 'gone':
{
return ChatState.gone; return ChatState.gone;
} }
default:
{ throw Exception('Invalid chat state $state');
return ChatState.gone; }
}
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 { class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager); ChatStateManager() : super(chatStateManager);
@ -64,62 +80,55 @@ class ChatStateManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!; final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
ChatState? chatState;
switch (element.tag) { try {
case 'active': state.extensions.set(ChatState.fromName(element.tag));
{ } catch (_) {
chatState = ChatState.active; logger.finest('Ignoring invalid chat state ${element.tag}');
}
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}'");
}
} }
return state.copyWith(chatState: chatState); return state;
} }
/// Send a chat state notification to [to]. You can specify the type attribute /// Send a chat state notification to [to]. You can specify the type attribute
/// of the message with [messageType]. /// of the message with [messageType].
void sendChatState( Future<void> sendChatState(
ChatState state, ChatState state,
String to, { String to, {
String messageType = 'chat', String messageType = 'chat',
}) { }) async {
final tagName = state.toString().split('.').last; await getAttributes().sendStanza(
getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.message( Stanza.message(
to: to, to: to,
type: messageType, type: messageType,
children: [ 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/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/presence.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.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/stringxml.dart';
import 'package:moxxmpp/src/util/list.dart'; import 'package:moxxmpp/src/util/list.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart';
@ -105,7 +108,20 @@ class EntityCapabilitiesManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@override @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 /// Computes, if required, the capability hash of the data provided by
/// the DiscoManager. /// the DiscoManager.
@ -159,33 +175,38 @@ class EntityCapabilitiesManager extends XmppManagerBase {
} }
@visibleForTesting @visibleForTesting
Future<void> onPresence(PresenceReceivedEvent event) async { Future<StanzaHandlerData> onPresence(
final c = event.presence.firstTag('c', xmlns: capsXmlns); Stanza stanza,
if (c == null) { StanzaHandlerData state,
return; ) 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 hashFunctionName = c.attributes['hash'] as String?;
final capabilityNode = c.attributes['node'] as String?; final capabilityNode = c.attributes['node'] as String?;
final ver = c.attributes['ver'] as String?; final ver = c.attributes['ver'] as String?;
if (hashFunctionName == null || capabilityNode == null || ver == null) { if (hashFunctionName == null || capabilityNode == null || ver == null) {
return; return state;
} }
// Check if we know of the hash // Check if we know of the hash
final isCached = final isCached =
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver)); await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
if (isCached) { if (isCached) {
return; return state;
} }
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final discoRequest = await dm.discoInfoQuery( final discoRequest = await dm.discoInfoQuery(
event.jid, from,
node: capabilityNode, node: capabilityNode,
); );
if (discoRequest.isType<DiscoError>()) { if (discoRequest.isType<DiscoError>()) {
return; return state;
} }
final discoInfo = discoRequest.get<DiscoInfo>(); final discoInfo = discoRequest.get<DiscoInfo>();
@ -194,13 +215,13 @@ class EntityCapabilitiesManager extends XmppManagerBase {
await dm.addCachedDiscoInfo( await dm.addCachedDiscoInfo(
MapEntry<DiscoCacheKey, DiscoInfo>( MapEntry<DiscoCacheKey, DiscoInfo>(
DiscoCacheKey( DiscoCacheKey(
event.jid, from,
null, null,
), ),
discoInfo, discoInfo,
), ),
); );
return; return state;
} }
// Validate the disco#info result according to XEP-0115 § 5.4 // Validate the disco#info result according to XEP-0115 § 5.4
@ -214,7 +235,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning( logger.warning(
'Malformed disco#info response: More than one equal identity', 'Malformed disco#info response: More than one equal identity',
); );
return; return state;
} }
} }
@ -225,7 +246,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning( logger.warning(
'Malformed disco#info response: More than one equal feature', 'Malformed disco#info response: More than one equal feature',
); );
return; return state;
} }
} }
@ -253,7 +274,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning( logger.warning(
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.', '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( logger.warning(
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value', '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 // Check if the field type is hidden
@ -297,14 +318,16 @@ class EntityCapabilitiesManager extends XmppManagerBase {
if (computedCapabilityHash == ver) { if (computedCapabilityHash == ver) {
await _cacheLock.synchronized(() { await _cacheLock.synchronized(() {
_jidToCapHashCache[event.jid.toString()] = ver; _jidToCapHashCache[from.toString()] = ver;
_capHashCache[ver] = newDiscoInfo; _capHashCache[ver] = newDiscoInfo;
}); });
} else { } else {
logger.warning( logger.warning(
'Capability hash mismatch from ${event.jid}: Received "$ver", expected "$computedCapabilityHash".', 'Capability hash mismatch from $from: Received "$ver", expected "$computedCapabilityHash".',
); );
} }
return state;
} }
@visibleForTesting @visibleForTesting
@ -315,9 +338,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PresenceReceivedEvent) { if (event is StreamNegotiationsDoneEvent) {
unawaited(onPresence(event));
} else if (event is StreamNegotiationsDoneEvent) {
// Clear the JID to cap. hash mapping. // Clear the JID to cap. hash mapping.
await _cacheLock.synchronized(_jidToCapHashCache.clear); 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeMessageDeliveryRequest() { class MessageDeliveryReceiptData implements StanzaHandlerExtension {
return XMLNode.xmlns( const MessageDeliveryReceiptData(this.receiptRequested);
tag: 'request',
xmlns: deliveryXmlns, /// 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 {
return XMLNode.xmlns( const MessageDeliveryReceivedData(this.id);
tag: 'received',
xmlns: deliveryXmlns, /// The stanza id of the message we received.
attributes: {'id': id}, final String id;
);
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'received',
xmlns: deliveryXmlns,
attributes: {'id': id},
);
}
} }
class MessageDeliveryReceiptManager extends XmppManagerBase { class MessageDeliveryReceiptManager extends XmppManagerBase {
@ -56,7 +76,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
return state.copyWith(deliveryReceiptRequested: true); return state..extensions.set(const MessageDeliveryReceiptData(true));
} }
Future<StanzaHandlerData> _onDeliveryReceiptReceived( Future<StanzaHandlerData> _onDeliveryReceiptReceived(
@ -64,16 +84,16 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final received = message.firstTag('received', xmlns: deliveryXmlns)!; final received = message.firstTag('received', xmlns: deliveryXmlns)!;
for (final item in message.children) { // for (final item in message.children) {
if (!['origin-id', 'stanza-id', 'delay', 'store', 'received'] // if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
.contains(item.tag)) { // .contains(item.tag)) {
logger.info( // logger.info(
"Won't handle stanza as delivery receipt because we found an '${item.tag}' element", // "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( getAttributes().sendEvent(
DeliveryReceiptReceivedEvent( DeliveryReceiptReceivedEvent(
@ -81,6 +101,27 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
id: received.attributes['id']! as String, 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( 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 { 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/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.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/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
const xmlUintMax = 4294967296; // 2**32 const xmlUintMax = 4294967296; // 2**32
@ -399,10 +400,14 @@ class StreamManagementManager extends XmppManagerBase {
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
await _incrementC2S();
_unackedStanzas[_state.c2s] = stanza;
if (isStreamManagementEnabled()) { if (isStreamManagementEnabled()) {
await _incrementC2S();
if (state.extensions.get<StreamManagementData>()?.exclude ?? false) {
return state;
}
_unackedStanzas[_state.c2s] = stanza;
await _sendAckRequest(); await _sendAckRequest();
} }
@ -414,6 +419,8 @@ class StreamManagementManager extends XmppManagerBase {
_unackedStanzas.clear(); _unackedStanzas.clear();
for (final stanza in stanzas) { for (final stanza in stanzas) {
logger
.finest('Resending ${stanza.tag} with id ${stanza.attributes["id"]}');
await getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
stanza, stanza,

View File

@ -1,4 +1,5 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
@ -7,10 +8,14 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
@immutable @immutable
class DelayedDelivery { class DelayedDeliveryData implements StanzaHandlerExtension {
const DelayedDelivery(this.from, this.timestamp); const DelayedDeliveryData(this.from, this.timestamp);
/// The timestamp the message was originally sent.
final DateTime timestamp; final DateTime timestamp;
final String from;
/// The JID that originally sent the message.
final JID from;
} }
class DelayedDeliveryManager extends XmppManagerBase { class DelayedDeliveryManager extends XmppManagerBase {
@ -23,6 +28,8 @@ class DelayedDeliveryManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
tagName: 'delay',
tagXmlns: delayedDeliveryXmlns,
callback: _onIncomingMessage, callback: _onIncomingMessage,
priority: 200, priority: 200,
), ),
@ -32,14 +39,14 @@ class DelayedDeliveryManager extends XmppManagerBase {
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns)!;
if (delay == null) return state;
return state.copyWith( return state
delayedDelivery: DelayedDelivery( ..extensions.set(
delay.attributes['from']! as String, DelayedDeliveryData(
DateTime.parse(delay.attributes['stamp']! as String), 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_0297.dart';
import 'package:moxxmpp/src/xeps/xep_0386.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. /// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase { class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super(carbonsManager); CarbonsManager() : super(carbonsManager);
@ -77,15 +84,14 @@ class CarbonsManager extends XmppManagerBase {
) async { ) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final received = message.firstTag('received', xmlns: carbonsXmlns)!; 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 forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded); final carbon = unpackForwarded(forwarded);
return state.copyWith( return state
isCarbon: true, ..extensions.set(const CarbonsData(true))
stanza: carbon, ..stanza = carbon;
);
} }
Future<StanzaHandlerData> _onMessageSent( Future<StanzaHandlerData> _onMessageSent(
@ -94,15 +100,14 @@ class CarbonsManager extends XmppManagerBase {
) async { ) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!; 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 forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded); final carbon = unpackForwarded(forwarded);
return state.copyWith( return state
isCarbon: true, ..extensions.set(const CarbonsData(true))
stanza: carbon, ..stanza = carbon;
);
} }
/// Send a request to the server, asking it to enable Message Carbons. /// Send a request to the server, asking it to enable Message Carbons.

View File

@ -66,7 +66,7 @@ enum HashFunction {
return HashFunction.blake2b512; return HashFunction.blake2b512;
} }
throw Exception(); throw Exception('Invalid hash function $name');
} }
/// Like [HashFunction.fromName], but returns null if the hash function is unknown /// 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeLastMessageCorrectionEdit(String id) { class LastMessageCorrectionData implements StanzaHandlerExtension {
return XMLNode.xmlns( const LastMessageCorrectionData(this.id);
tag: 'replace',
xmlns: lmcXmlns, /// The id the LMC applies to.
attributes: <String, String>{ final String id;
'id': id,
}, XMLNode toXML() {
); return XMLNode.xmlns(
tag: 'replace',
xmlns: lmcXmlns,
attributes: {
'id': id,
},
);
}
} }
class LastMessageCorrectionManager extends XmppManagerBase { class LastMessageCorrectionManager extends XmppManagerBase {
@ -42,8 +51,30 @@ class LastMessageCorrectionManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!; final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
return state.copyWith( return state
lastMessageCorrectionSid: edit.attributes['id']! as String, ..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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeChatMarkerMarkable() { enum ChatMarker {
return XMLNode.xmlns( received,
tag: 'markable', displayed,
xmlns: chatMarkersXmlns, 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,
);
}
} }
XMLNode makeChatMarker(String tag, String id) { class MarkableData implements StanzaHandlerExtension {
assert( const MarkableData(this.isMarkable);
['received', 'displayed', 'acknowledged'].contains(tag),
'Invalid chat marker', /// Indicates whether the message can be replied to with a chat marker.
); final bool isMarkable;
return XMLNode.xmlns(
tag: tag, XMLNode toXML() {
xmlns: chatMarkersXmlns, assert(isMarkable, '');
attributes: {'id': id},
); return XMLNode.xmlns(
tag: 'markable',
xmlns: chatMarkersXmlns,
);
}
}
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,
xmlns: chatMarkersXmlns,
attributes: {
'id': id,
},
);
}
} }
class ChatMarkerManager extends XmppManagerBase { class ChatMarkerManager extends XmppManagerBase {
@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase {
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final marker = message.firstTagByXmlns(chatMarkersXmlns)!; final element = message.firstTagByXmlns(chatMarkersXmlns)!;
// Handle the <markable /> explicitly // 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 {
getAttributes().sendEvent(
ChatMarkerEvent(
from: JID.fromString(message.from!),
type: marker.tag,
id: marker.attributes['id']! as String,
),
);
} }
return state.copyWith(done: true); try {
getAttributes().sendEvent(
ChatMarkerEvent(
JID.fromString(message.from!),
ChatMarker.fromName(element.tag),
element.attributes['id']! as String,
),
);
} catch (_) {
logger.warning("Unknown message marker '${element.tag}' found.");
}
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,31 +1,36 @@
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/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum MessageProcessingHint { enum MessageProcessingHint {
noPermanentStore, noPermanentStore,
noStore, noStore,
noCopies, noCopies,
store, store;
}
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) { factory MessageProcessingHint.fromName(String name) {
switch (element.tag) { switch (name) {
case 'no-permanent-store': case 'no-permanent-store':
return MessageProcessingHint.noPermanentStore; return MessageProcessingHint.noPermanentStore;
case 'no-store': case 'no-store':
return MessageProcessingHint.noStore; return MessageProcessingHint.noStore;
case 'no-copy': case 'no-copy':
return MessageProcessingHint.noCopies; return MessageProcessingHint.noCopies;
case 'store': case 'store':
return MessageProcessingHint.store; return MessageProcessingHint.store;
}
assert(false, 'Invalid Message Processing Hint: $name');
return MessageProcessingHint.noStore;
} }
assert(false, 'Invalid Message Processing Hint: ${element.tag}'); XMLNode toXML() {
return MessageProcessingHint.noStore;
}
extension XmlExtension on MessageProcessingHint {
XMLNode toXml() {
String tag; String tag;
switch (this) { switch (this) {
case MessageProcessingHint.noPermanentStore: 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Representation of a <stanza-id /> element. /// Representation of a <stanza-id /> element.
class StanzaId { class StanzaId {
@ -20,7 +22,7 @@ class StanzaId {
/// The JID the id was generated by. /// The JID the id was generated by.
final JID by; final JID by;
XMLNode toXml() { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'stanza-id', tag: 'stanza-id',
xmlns: stableIdXmlns, xmlns: stableIdXmlns,
@ -32,12 +34,38 @@ class StanzaId {
} }
} }
XMLNode makeOriginIdElement(String id) { class StableIdData implements StanzaHandlerExtension {
return XMLNode.xmlns( const StableIdData(this.originId, this.stanzaIds);
tag: 'origin-id',
xmlns: stableIdXmlns, /// <origin-id />
attributes: {'id': 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': 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 { class StableIdManager extends XmppManagerBase {
@ -86,9 +114,29 @@ class StableIdManager extends XmppManagerBase {
.toList(); .toList();
} }
return state.copyWith( return state
originId: originId, ..extensions.set(
stanzaIds: stanzaIds, 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,64 +6,64 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
enum ExplicitEncryptionType { enum ExplicitEncryptionType implements StanzaHandlerExtension {
otr, otr,
legacyOpenPGP, legacyOpenPGP,
openPGP, openPGP,
omemo, omemo,
omemo1, omemo1,
omemo2, omemo2,
unknown, unknown;
}
String _explicitEncryptionTypeToString(ExplicitEncryptionType type) { factory ExplicitEncryptionType.fromNamespace(String namespace) {
switch (type) { switch (namespace) {
case ExplicitEncryptionType.otr: case emeOtr:
return emeOtr; return ExplicitEncryptionType.otr;
case ExplicitEncryptionType.legacyOpenPGP: case emeLegacyOpenPGP:
return emeLegacyOpenPGP; return ExplicitEncryptionType.legacyOpenPGP;
case ExplicitEncryptionType.openPGP: case emeOpenPGP:
return emeOpenPGP; return ExplicitEncryptionType.openPGP;
case ExplicitEncryptionType.omemo: case emeOmemo:
return emeOmemo; return ExplicitEncryptionType.omemo;
case ExplicitEncryptionType.omemo1: case emeOmemo1:
return emeOmemo1; return ExplicitEncryptionType.omemo1;
case ExplicitEncryptionType.omemo2: case emeOmemo2:
return emeOmemo2; return ExplicitEncryptionType.omemo2;
case ExplicitEncryptionType.unknown: default:
return ''; return ExplicitEncryptionType.unknown;
}
} }
}
ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) { String toNamespace() {
switch (str) { switch (this) {
case emeOtr: case ExplicitEncryptionType.otr:
return ExplicitEncryptionType.otr; return emeOtr;
case emeLegacyOpenPGP: case ExplicitEncryptionType.legacyOpenPGP:
return ExplicitEncryptionType.legacyOpenPGP; return emeLegacyOpenPGP;
case emeOpenPGP: case ExplicitEncryptionType.openPGP:
return ExplicitEncryptionType.openPGP; return emeOpenPGP;
case emeOmemo: case ExplicitEncryptionType.omemo:
return ExplicitEncryptionType.omemo; return emeOmemo;
case emeOmemo1: case ExplicitEncryptionType.omemo1:
return ExplicitEncryptionType.omemo1; return emeOmemo1;
case emeOmemo2: case ExplicitEncryptionType.omemo2:
return ExplicitEncryptionType.omemo2; return emeOmemo2;
default: case ExplicitEncryptionType.unknown:
return ExplicitEncryptionType.unknown; return '';
}
} }
}
/// Create an <encryption /> element with [type] indicating which type of encryption was /// Create an <encryption /> element with an xmlns indicating what type of encryption was
/// used. /// used.
XMLNode buildEmeElement(ExplicitEncryptionType type) { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'encryption', tag: 'encryption',
xmlns: emeXmlns, xmlns: emeXmlns,
attributes: <String, String>{ attributes: <String, String>{
'namespace': _explicitEncryptionTypeToString(type), 'namespace': toNamespace(),
}, },
); );
}
} }
class EmeManager extends XmppManagerBase { class EmeManager extends XmppManagerBase {
@ -91,10 +91,11 @@ class EmeManager extends XmppManagerBase {
) async { ) async {
final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; final encryption = message.firstTag('encryption', xmlns: emeXmlns)!;
return state.copyWith( return state
encryptionType: _explicitEncryptionTypeFromString( ..extensions.set(
encryption.attributes['namespace']! as String, 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. /// A simple wrapper class for defining elements that should not be encrypted.
class DoNotEncrypt { class DoNotEncrypt {
const DoNotEncrypt(this.tag, this.xmlns); const DoNotEncrypt(this.tag, this.xmlns);
/// The tag of the element.
final String tag; final String tag;
/// The xmlns attribute of the element.
final String xmlns; 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_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart'; import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart'; import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart';
@ -276,7 +277,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Add a storage hint in case this is a message // Add a storage hint in case this is a message
// Taken from the example at // Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description. // https://xmpp.org/extensions/xep-0384.html#message-structure-description.
MessageProcessingHint.store.toXml(), MessageProcessingHint.store.toXML(),
], ],
), ),
awaitable: false, awaitable: false,
@ -363,19 +364,18 @@ abstract class BaseOmemoManager extends XmppManagerBase {
logger.finest('Encryption done'); logger.finest('Encryption done');
if (!result.isSuccess(2)) { if (!result.isSuccess(2)) {
final other = Map<String, dynamic>.from(state.other); return state
other['encryption_error_jids'] = result.jidEncryptionErrors; ..cancel = true
other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith(
other: other,
// If we have no device list for toJid, then the contact most likely does not // If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2 // support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()] ..cancelReason = result.jidEncryptionErrors[toJid.toString()]
is NoKeyMaterialAvailableException is NoKeyMaterialAvailableException
? OmemoNotSupportedForContactException() ? OmemoNotSupportedForContactException()
: UnknownOmemoError(), : UnknownOmemoError()
cancel: true, ..encryptionError = OmemoEncryptionError(
); result.jidEncryptionErrors,
result.deviceEncryptionErrors,
);
} }
final encrypted = _buildEncryptedElement( final encrypted = _buildEncryptedElement(
@ -389,19 +389,16 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (stanza.tag == 'message') { if (stanza.tag == 'message') {
children children
// Add EME data // Add EME data
..add(buildEmeElement(ExplicitEncryptionType.omemo2)) ..add(ExplicitEncryptionType.omemo2.toXML())
// Add a storage hint in case this is a message // Add a storage hint in case this is a message
// Taken from the example at // Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description. // https://xmpp.org/extensions/xep-0384.html#message-structure-description.
..add(MessageProcessingHint.store.toXml()); ..add(MessageProcessingHint.store.toXML());
} }
return state.copyWith( return state
stanza: state.stanza.copyWith( ..stanza = state.stanza.copyWith(children: children)
children: children, ..encrypted = true;
),
encrypted: true,
);
} }
/// This function is called whenever a message is to be encrypted. If it returns 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( OmemoIncomingStanza(
fromJid.toString(), fromJid.toString(),
sid, sid,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? state.extensions
.get<DelayedDeliveryData>()
?.timestamp
.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch, DateTime.now().millisecondsSinceEpoch,
keys, keys,
payloadElement?.innerText(), payloadElement?.innerText(),
), ),
); );
final other = Map<String, dynamic>.from(state.other);
var children = stanza.children; var children = stanza.children;
if (result.error != null) { if (result.error != null) {
other['encryption_error'] = result.error; state.encryptionError = result.error;
} else { } else {
children = stanza.children children = stanza.children
.where( .where(
@ -471,11 +470,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
envelope = XMLNode.fromString(result.payload!); envelope = XMLNode.fromString(result.payload!);
} on XmlParserException catch (_) { } on XmlParserException catch (_) {
logger.warning('Failed to parse envelope payload: ${result.payload!}'); logger.warning('Failed to parse envelope payload: ${result.payload!}');
other['encryption_error'] = InvalidEnvelopePayloadException(); return state
return state.copyWith( ..encrypted = true
encrypted: true, ..encryptionError = InvalidEnvelopePayloadException();
other: other,
);
} }
final envelopeChildren = envelope.firstTag('content')?.children; final envelopeChildren = envelope.firstTag('content')?.children;
@ -489,13 +486,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
if (!checkAffixElements(envelope, stanza.from!, ourJid)) { if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException(); state.encryptionError = InvalidAffixElementsException();
} }
} }
return state.copyWith( return state
encrypted: true, ..encrypted = true
stanza: Stanza( ..stanza = Stanza(
to: stanza.to, to: stanza.to,
from: stanza.from, from: stanza.from,
id: stanza.id, id: stanza.id,
@ -503,9 +500,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children, children: children,
tag: stanza.tag, tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes), attributes: Map<String, String>.from(stanza.attributes),
), );
other: other,
);
} }
/// Convenience function that attempts to retrieve the raw XML payload from the /// Convenience function that attempts to retrieve the raw XML payload from the
@ -516,8 +511,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
JID jid, JID jid,
) async { ) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns);
await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns);
if (result.isType<PubSubError>()) return Result(UnknownOmemoError()); if (result.isType<PubSubError>()) return Result(UnknownOmemoError());
return Result(result.get<List<PubSubItem>>().first.payload); return Result(result.get<List<PubSubItem>>().first.payload);
} }
@ -543,7 +537,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
) async { ) async {
// TODO(Unknown): Should we query the device list first? // TODO(Unknown): Should we query the device list first?
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); final bundlesRaw = await pm.getItems(jid, omemoBundlesXmlns);
if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError()); if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError());
final bundles = bundlesRaw final bundles = bundlesRaw
@ -639,7 +633,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Subscribes to the device list PubSub node of [jid]. /// Subscribes to the device list PubSub node of [jid].
Future<void> subscribeToDeviceListImpl(String jid) async { Future<void> subscribeToDeviceListImpl(String jid) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
await pm.subscribe(jid, omemoDevicesXmlns); await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns);
} }
/// Attempts to find out if [jid] supports omemo:2. /// Attempts to find out if [jid] supports omemo:2.

View File

@ -7,7 +7,7 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
class StatelessMediaSharingData { class StatelessMediaSharingData implements StanzaHandlerExtension {
const StatelessMediaSharingData({ const StatelessMediaSharingData({
required this.mediaType, required this.mediaType,
required this.size, required this.size,
@ -70,7 +70,9 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
); );
} }
@Deprecated('Not maintained')
class SIMSManager extends XmppManagerBase { class SIMSManager extends XmppManagerBase {
@Deprecated('Not maintained')
SIMSManager() : super(simsManager); SIMSManager() : super(simsManager);
@override @override
@ -98,7 +100,7 @@ class SIMSManager extends XmppManagerBase {
final references = message.findTags('reference', xmlns: referenceXmlns); final references = message.findTags('reference', xmlns: referenceXmlns);
for (final ref in references) { for (final ref in references) {
final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); 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; 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageRetractionData { class MessageRetractionData implements StanzaHandlerExtension {
MessageRetractionData(this.id, this.fallback); MessageRetractionData(this.id, this.fallback);
/// A potential fallback message to set the body to when retracting.
final String? fallback; final String? fallback;
/// The id of the message that is retracted.
final String id; final String id;
} }
@ -47,11 +54,55 @@ class MessageRetractionManager extends XmppManagerBase {
final isFallbackBody = final isFallbackBody =
message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null;
return state.copyWith( return state
messageRetraction: MessageRetractionData( ..extensions.set(
applyTo.attributes['id']! as String, MessageRetractionData(
isFallbackBody ? message.firstTag('body')?.innerText() : null, 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
class MessageReactions { class MessageReactionsData implements StanzaHandlerExtension {
const MessageReactions(this.messageId, this.emojis); const MessageReactionsData(this.messageId, this.emojis);
final String messageId; final String messageId;
final List<String> emojis; final List<String> emojis;
XMLNode toXml() { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'reactions', tag: 'reactions',
xmlns: messageReactionsXmlns, xmlns: messageReactionsXmlns,
@ -55,14 +57,36 @@ class MessageReactionsManager extends XmppManagerBase {
) async { ) async {
final reactionsElement = final reactionsElement =
message.firstTag('reactions', xmlns: messageReactionsXmlns)!; message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
return state.copyWith( return state
messageReactions: MessageReactions( ..extensions.set(
reactionsElement.attributes['id']! as String, MessageReactionsData(
reactionsElement.children reactionsElement.attributes['id']! as String,
.where((c) => c.tag == 'reaction') reactionsElement.children
.map((c) => c.innerText()) .where((c) => c.tag == 'reaction')
.toList(), .map((c) => c.innerText())
), .toList(),
); ),
);
}
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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart';
@ -70,8 +73,12 @@ List<StatelessFileSharingSource> processStatelessFileSharingSources(
return sources; return sources;
} }
class StatelessFileSharingData { class StatelessFileSharingData implements StanzaHandlerExtension {
const StatelessFileSharingData(this.metadata, this.sources); const StatelessFileSharingData(
this.metadata,
this.sources, {
this.includeOOBFallback = false,
});
/// Parse [node] as a StatelessFileSharingData element. /// Parse [node] as a StatelessFileSharingData element.
factory StatelessFileSharingData.fromXML(XMLNode node) { factory StatelessFileSharingData.fromXML(XMLNode node) {
@ -88,6 +95,10 @@ class StatelessFileSharingData {
final FileMetadataData metadata; final FileMetadataData metadata;
final List<StatelessFileSharingSource> sources; 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() { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'file-sharing', tag: 'file-sharing',
@ -122,23 +133,54 @@ class SFSManager extends XmppManagerBase {
tagXmlns: sfsXmlns, tagXmlns: sfsXmlns,
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -98,
) )
]; ];
@override @override
Future<bool> isSupported() async => true; 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( Future<StanzaHandlerData> _onMessage(
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!;
return state.copyWith( return state
sfs: StatelessFileSharingData.fromXML( ..extensions.set(
sfs, 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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; 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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0300.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 { class StickersManager extends XmppManagerBase {
StickersManager() : super(stickersManager); StickersManager() : super(stickersManager);
@ -249,9 +264,35 @@ class StickersManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state.copyWith( return state
stickerPackId: sticker.attributes['pack']! as String, ..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 /// Publishes the StickerPack [pack] to the PubSub node of [jid]. If specified, then
@ -319,4 +360,14 @@ class StickersManager extends XmppManagerBase {
return Result(stickerPack); 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/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Data summarizing the XEP-0461 data. /// A reply to a message.
class ReplyData { class ReplyData implements StanzaHandlerExtension {
const ReplyData({ const ReplyData(
required this.id, this.id, {
this.to, this.body,
this.jid,
this.start, this.start,
this.end, this.end,
}); });
/// The bare JID to whom the reply applies to ReplyData.fromQuoteData(
final String? to; 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; final String id;
/// The start of the fallback body (inclusive) /// The start of the fallback body (inclusive)
@ -27,18 +41,21 @@ class ReplyData {
/// The end of the fallback body (exclusive) /// The end of the fallback body (exclusive)
final int? end; 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. /// 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 /// If either [ReplyData.start] or [ReplyData.end] are null, then body is returned as
/// is. /// is.
String removeFallback(String body) { String? get withoutFallback {
if (body == null) return null;
if (start == null || end == null) return body; 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. /// Internal class describing how to build a message with a quote fallback body.
@visibleForTesting
class QuoteData { class QuoteData {
const QuoteData(this.body, this.fallbackLength); const QuoteData(this.body, this.fallbackLength);
@ -85,13 +102,54 @@ class MessageRepliesManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; 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( Future<StanzaHandlerData> _onMessage(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; final reply = stanza.firstTag('reply', xmlns: replyXmlns)!;
final id = reply.attributes['id']! as String;
final to = reply.attributes['to'] as String?; final to = reply.attributes['to'] as String?;
final jid = to != null ? JID.fromString(to) : null;
int? start; int? start;
int? end; int? end;
@ -103,13 +161,25 @@ class MessageRepliesManager extends XmppManagerBase {
end = int.parse(body.attributes['end']! as String); end = int.parse(body.attributes['end']! as String);
} }
return state.copyWith( return state
reply: ReplyData( ..extensions.set(
id: id, ReplyData(
to: to, reply.attributes['id']! as String,
start: start, jid: jid,
end: end, 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 name: moxxmpp
description: A pure-Dart XMPP library description: A pure-Dart XMPP library
version: 0.3.2 version: 0.4.0
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub

View File

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

View File

@ -16,8 +16,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
); );
@ -38,8 +38,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagXmlns: 'owo', tagXmlns: 'owo',
); );
@ -59,8 +59,8 @@ void main() {
return StanzaHandlerData( return StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
); );
}, },
stanzaTag: 'iq', stanzaTag: 'iq',
@ -77,8 +77,8 @@ void main() {
StanzaHandlerData( StanzaHandlerData(
false, false,
false, false,
null,
stanza2, stanza2,
TypedMap(),
), ),
); );
expect(run, true); expect(run, true);
@ -89,8 +89,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagName: 'tag', tagName: 'tag',
); );
@ -107,8 +107,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagName: 'tag', tagName: 'tag',
stanzaTag: 'iq', stanzaTag: 'iq',
@ -127,8 +127,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
xmlns: componentAcceptXmlns, xmlns: componentAcceptXmlns,
); );
@ -147,8 +147,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagName: '1', tagName: '1',
priority: 100, priority: 100,
@ -157,8 +157,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagName: '2', tagName: '2',
), ),
@ -166,8 +166,8 @@ void main() {
callback: (stanza, _) async => StanzaHandlerData( callback: (stanza, _) async => StanzaHandlerData(
true, true,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
tagName: '3', tagName: '3',
priority: 50, 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 ecm = EntityCapabilitiesManager('');
final dm = DiscoManager([]); final dm = DiscoManager([]);
await tm.register(dm); await tm.register([dm, ecm]);
await tm.register(ecm);
// Inject a capability hash into the cache // Inject a capability hash into the cache
final aliceJid = JID.fromString('alice@example.org/abc123'); final aliceJid = JID.fromString('alice@example.org/abc123');
@ -140,7 +139,7 @@ void main() {
// Query Alice's device // Query Alice's device
final result = await dm.discoInfoQuery(aliceJid); final result = await dm.discoInfoQuery(aliceJid);
expect(result.isType<DiscoError>(), false); expect(result.isType<DiscoError>(), false);
expect(tm.sentStanzas, 0); expect(tm.socket.getState(), 0);
}); });
}); });
} }

View File

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

View File

@ -319,27 +319,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register(StubbedDiscoManager()); await tm.register([
await tm.register(manager); StubbedDiscoManager(),
manager,
]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(
@ -352,27 +353,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register(StubbedDiscoManager()); await tm.register([
await tm.register(manager); StubbedDiscoManager(),
manager,
]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94AAAAA=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94AAAAA=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(
@ -385,29 +387,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..multipleEqualIdentities = true, StubbedDiscoManager()..multipleEqualIdentities = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(
@ -420,29 +421,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..multipleEqualFeatures = true, StubbedDiscoManager()..multipleEqualFeatures = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(
@ -455,29 +455,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..multipleExtendedFormsWithSameType = true, StubbedDiscoManager()..multipleExtendedFormsWithSameType = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(
@ -490,29 +489,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..invalidExtension1 = true, StubbedDiscoManager()..invalidExtension1 = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid); final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
@ -527,29 +525,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..invalidExtension2 = true, StubbedDiscoManager()..invalidExtension2 = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid); final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
@ -564,29 +561,28 @@ void main() {
final tm = TestingManagerHolder(); final tm = TestingManagerHolder();
final manager = EntityCapabilitiesManager(''); final manager = EntityCapabilitiesManager('');
await tm.register( await tm.register([
StubbedDiscoManager()..invalidExtension3 = true, StubbedDiscoManager()..invalidExtension3 = true,
); manager,
await tm.register(manager); ]);
await manager.onPresence( final stanza = Stanza.presence(
PresenceReceivedEvent( from: aliceJid.toString(),
aliceJid, children: [
Stanza.presence( XMLNode.xmlns(
from: aliceJid.toString(), tag: 'c',
children: [ xmlns: capsXmlns,
XMLNode.xmlns( attributes: {
tag: 'c', 'hash': 'sha-1',
xmlns: capsXmlns, 'node': 'http://example.org/client',
attributes: { 'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
'hash': 'sha-1', },
'node': 'http://example.org/client',
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
},
),
],
), ),
), ],
);
await manager.onPresence(
stanza,
StanzaHandlerData(false, false, stanza, TypedMap()),
); );
expect( expect(

View File

@ -15,8 +15,8 @@ Future<void> runIncomingStanzaHandlers(
StanzaHandlerData( StanzaHandlerData(
false, false,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
); );
} }
@ -34,8 +34,8 @@ Future<void> runOutgoingStanzaHandlers(
StanzaHandlerData( StanzaHandlerData(
false, false,
false, false,
null,
stanza, 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> <size>3032449</size>
<dimensions>4096x2160</dimensions> <dimensions>4096x2160</dimensions>
<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash> <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> <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'/> <thumbnail xmlns='urn:xmpp:thumbs:1' uri='cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org' media-type='image/png' width='128' height='96'/>
</file> </file>
@ -28,11 +28,11 @@ void main() {
); );
expect( expect(
sfs.metadata.hashes['sha3-256'], sfs.metadata.hashes[HashFunction.sha3_256],
'2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=', '2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=',
); );
expect( expect(
sfs.metadata.hashes['id-blake2b256'], sfs.metadata.hashes[HashFunction.blake2b256],
'2AfMGH8O7UNPTvUVAM9aK13mpCY=', '2AfMGH8O7UNPTvUVAM9aK13mpCY=',
); );
}); });

View File

@ -1,7 +1,13 @@
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart';
void main() { void main() {
initLogger();
test('Test parsing a large sticker pack', () { test('Test parsing a large sticker pack', () {
// Example sticker pack based on the "miho" sticker pack by Movim // Example sticker pack based on the "miho" sticker pack by Movim
final rawPack = XMLNode.fromString(''' final rawPack = XMLNode.fromString('''
@ -225,4 +231,186 @@ void main() {
expect(pack.stickers.length, 16); 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:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/xmpp.dart';
void main() { void main() {
test('Test building a singleline quote', () { test('Test building a singleline quote', () {
final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!'); final quote = QuoteData.fromBodies('Hallo Welt', 'Hello Earth!');
@ -20,28 +22,250 @@ void main() {
}); });
test('Applying a singleline quote', () { test('Applying a singleline quote', () {
const body = '> Hallo Welt\nHello right back!';
const reply = ReplyData( const reply = ReplyData(
to: '', '',
id: '',
start: 0, start: 0,
end: 13, end: 13,
body: '> Hallo Welt\nHello right back!',
); );
final bodyWithoutFallback = reply.removeFallback(body); expect(reply.withoutFallback, 'Hello right back!');
expect(bodyWithoutFallback, 'Hello right back!');
}); });
test('Applying a multiline quote', () { test('Applying a multiline quote', () {
const body = "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!";
const reply = ReplyData( const reply = ReplyData(
to: '', '',
id: '',
start: 0, start: 0,
end: 28, end: 28,
body: "> Hallo Welt\n> How are you?\nI'm fine.\nThank you!",
); );
final bodyWithoutFallback = reply.removeFallback(body); expect(reply.withoutFallback, "I'm fine.\nThank you!");
expect(bodyWithoutFallback, "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( StanzaHandlerData(
false, false,
false, false,
null,
stanza, stanza,
TypedMap(),
), ),
); );
} }
@ -335,8 +335,8 @@ void main() {
StanzaHandlerData( StanzaHandlerData(
false, false,
false, false,
null,
maliciousStanza, maliciousStanza,
TypedMap(),
), ),
); );
} }
@ -793,4 +793,20 @@ void main() {
expect(fakeSocket.getState(), 9); expect(fakeSocket.getState(), 9);
expect(await stanzaFuture != null, true); 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);
});
} }