Merge branch 'master' into xep_0045

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
This commit is contained in:
Ikjot Singh Dhody 2023-06-14 10:48:28 +05:30
commit 66195f66fa
66 changed files with 2497 additions and 1760 deletions

View File

@ -44,13 +44,12 @@ void main() {
FASTSaslNegotiator(),
Bind2Negotiator(),
StartTlsNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

View File

@ -1,4 +1,4 @@
## 0.3.2
## 0.4.0
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
@ -10,7 +10,12 @@
- **BREAKING**: The entity argument of `DiscoManager.discoInfoQuery` and `DiscoManager.discoItemsQuery` are now `JID` instead of `String`.
- **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`.
- **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument.
- Sent stanzas are not 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

View File

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

View File

@ -27,7 +27,9 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/queue.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart';
@ -405,8 +407,7 @@ class XmppConnection {
/// Returns true if we can send data through the socket.
Future<bool> _canSendData() async {
return [XmppConnectionState.connected, XmppConnectionState.connecting]
.contains(await getConnectionState());
return await getConnectionState() == XmppConnectionState.connected;
}
/// 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;
await _stanzaQueue.enqueueStanza(
StanzaQueueEntry(
details,
completer,
),
final entry = StanzaQueueEntry(
details,
completer,
);
if (details.bypassQueue) {
await _sendStanzaImpl(entry);
} else {
await _stanzaQueue.enqueueStanza(entry);
}
return completer?.future;
}
@ -471,8 +476,8 @@ class XmppConnection {
initial: StanzaHandlerData(
false,
false,
null,
newStanza,
TypedMap(),
encrypted: details.encrypted,
forceEncryption: details.forceEncryption,
),
@ -523,18 +528,20 @@ class XmppConnection {
if (await _canSendData()) {
_socket.write(data.stanza.toXml());
} else {
_log.fine('Not sending dat as _canSendData() returned false.');
_log.fine('Not sending data as _canSendData() returned false.');
}
// Run post-send handlers
_log.fine('Running post stanza handlers..');
final extensions = TypedMap<StanzaHandlerExtension>()
..set(StreamManagementData(details.excludeFromStreamManagement));
await _runOutgoingPostStanzaHandlers(
newStanza,
initial: StanzaHandlerData(
false,
false,
null,
newStanza,
extensions,
),
);
_log.fine('Done');
@ -649,7 +656,7 @@ class XmppConnection {
Stanza stanza, {
StanzaHandlerData? initial,
}) async {
var state = initial ?? StanzaHandlerData(false, false, null, stanza);
var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap());
for (final handler in handlers) {
if (handler.matches(state.stanza)) {
state = await handler.callback(state.stanza, state);
@ -724,7 +731,7 @@ class XmppConnection {
// it.
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted &&
incomingPreHandlers.other['encryption_error'] == null
incomingPreHandlers.encryptionError == null
? '(Encrypted) '
: '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
@ -743,10 +750,10 @@ class XmppConnection {
initial: StanzaHandlerData(
false,
incomingPreHandlers.cancel,
incomingPreHandlers.cancelReason,
incomingPreHandlers.stanza,
incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted,
other: incomingPreHandlers.other,
cancelReason: incomingPreHandlers.cancelReason,
),
);
if (!incomingHandlers.done) {
@ -835,7 +842,7 @@ class XmppConnection {
await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) {
getPresenceManager()?.sendUnavailablePresence();
await getPresenceManager()?.sendUnavailablePresence();
}
_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/roster/roster.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
import 'package:moxxmpp/src/xeps/xep_0084.dart';
import 'package:moxxmpp/src/xeps/xep_0333.dart';
abstract class XmppEvent {}
@ -74,58 +67,42 @@ class RosterUpdatedEvent extends XmppEvent {
/// Triggered when a message is received
class MessageEvent extends XmppEvent {
MessageEvent({
required this.body,
required this.fromJid,
required this.toJid,
required this.sid,
required this.stanzaId,
required this.isCarbon,
required this.deliveryReceiptRequested,
required this.isMarkable,
required this.encrypted,
required this.other,
this.error,
MessageEvent(
this.from,
this.to,
this.id,
this.encrypted,
this.extensions, {
this.type,
this.oob,
this.sfs,
this.sims,
this.reply,
this.chatState,
this.fun,
this.funReplacement,
this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.error,
this.encryptionError,
});
final StanzaError? error;
final String body;
final JID fromJid;
final JID toJid;
final String sid;
/// The from attribute of the message.
final JID from;
/// The to attribute of the message.
final JID to;
/// The id attribute of the message.
final String id;
/// The type attribute of the message.
final String? type;
final StableStanzaId stanzaId;
final bool isCarbon;
final bool deliveryReceiptRequested;
final bool isMarkable;
final OOBData? oob;
final StatelessFileSharingData? sfs;
final StatelessMediaSharingData? sims;
final ReplyData? reply;
final ChatState? chatState;
final FileMetadataData? fun;
final String? funReplacement;
final String? funCancellation;
final StanzaError? error;
/// Flag indicating whether the message was encrypted.
final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId;
final MessageReactions? messageReactions;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId;
final Map<String, dynamic> other;
/// The error in case an encryption error occurred.
final Object? encryptionError;
/// Data added by other handlers.
final TypedMap<StanzaHandlerExtension> extensions;
/// Shorthand for extensions.get<T>().
T? get<T>() => extensions.get<T>();
}
/// Triggered when a client responds to our delivery receipt request
@ -136,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent {
}
class ChatMarkerEvent extends XmppEvent {
ChatMarkerEvent({
required this.type,
required this.from,
required this.id,
});
ChatMarkerEvent(
this.from,
this.type,
this.id,
);
/// The entity that sent the chat marker.
final JID from;
final String type;
/// The type of chat marker that was sent.
final ChatMarker type;
/// The id of the message that the marker applies to.
final String id;
}
@ -166,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent {
final String resource;
}
/// Triggered when we receive presence
class PresenceReceivedEvent extends XmppEvent {
PresenceReceivedEvent(this.jid, this.presence);
final JID jid;
final Stanza presence;
}
/// Triggered when we are starting an connection attempt
class ConnectingEvent extends XmppEvent {}
@ -190,15 +166,35 @@ class SubscriptionRequestReceivedEvent extends XmppEvent {
final JID from;
}
/// Triggered when we receive a new or updated avatar
class AvatarUpdatedEvent extends XmppEvent {
AvatarUpdatedEvent({
required this.jid,
required this.base64,
required this.hash,
});
final String jid;
/// Triggered when we receive a new or updated avatar via XEP-0084
class UserAvatarUpdatedEvent extends XmppEvent {
UserAvatarUpdatedEvent(
this.jid,
this.metadata,
);
/// 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;
/// The SHA-1 hash of the avatar.
final String hash;
}

View File

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

View File

@ -1,747 +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
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;
StableStanzaId? get stableId => 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>;
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
StableStanzaId? stableId,
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>
implements $StanzaHandlerDataCopyWith<$Res> {
_$StanzaHandlerDataCopyWithImpl(this._value, this._then);
final StanzaHandlerData _value;
// ignore: unused_field
final $Res Function(StanzaHandlerData) _then;
@override
$Res call({
Object? done = freezed,
Object? cancel = freezed,
Object? cancelReason = freezed,
Object? stanza = freezed,
Object? retransmitted = freezed,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? stableId = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = freezed,
Object? deliveryReceiptRequested = freezed,
Object? isMarkable = freezed,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_value.copyWith(
done: done == freezed
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
cancel: cancel == freezed
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
cancelReason: cancelReason == freezed
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
stanza: stanza == freezed
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: retransmitted == freezed
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: sims == freezed
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: sfs == freezed
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: oob == freezed
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
stableId: stableId == freezed
? _value.stableId
: stableId // ignore: cast_nullable_to_non_nullable
as StableStanzaId?,
reply: reply == freezed
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: chatState == freezed
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: isCarbon == freezed
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: deliveryReceiptRequested == freezed
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: isMarkable == freezed
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: fun == freezed
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: funReplacement == freezed
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: funCancellation == freezed
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: encrypted == freezed
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: delayedDelivery == freezed
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: other == freezed
? _value.other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: messageRetraction == freezed
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
abstract class _$$_StanzaHandlerDataCopyWith<$Res>
implements $StanzaHandlerDataCopyWith<$Res> {
factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value,
$Res Function(_$_StanzaHandlerData) then) =
__$$_StanzaHandlerDataCopyWithImpl<$Res>;
@override
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
StableStanzaId? stableId,
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>
implements _$$_StanzaHandlerDataCopyWith<$Res> {
__$$_StanzaHandlerDataCopyWithImpl(
_$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then)
: super(_value, (v) => _then(v as _$_StanzaHandlerData));
@override
_$_StanzaHandlerData get _value => super._value as _$_StanzaHandlerData;
@override
$Res call({
Object? done = freezed,
Object? cancel = freezed,
Object? cancelReason = freezed,
Object? stanza = freezed,
Object? retransmitted = freezed,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? stableId = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = freezed,
Object? deliveryReceiptRequested = freezed,
Object? isMarkable = freezed,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_$_StanzaHandlerData(
done == freezed
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
cancel == freezed
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
cancelReason == freezed
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
stanza == freezed
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: retransmitted == freezed
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: sims == freezed
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: sfs == freezed
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: oob == freezed
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
stableId: stableId == freezed
? _value.stableId
: stableId // ignore: cast_nullable_to_non_nullable
as StableStanzaId?,
reply: reply == freezed
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: chatState == freezed
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: isCarbon == freezed
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: deliveryReceiptRequested == freezed
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: isMarkable == freezed
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: fun == freezed
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: funReplacement == freezed
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: funCancellation == freezed
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: encrypted == freezed
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: delayedDelivery == freezed
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: other == freezed
? _value._other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: messageRetraction == freezed
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$_StanzaHandlerData implements _StanzaHandlerData {
_$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza,
{this.retransmitted = false,
this.sims,
this.sfs,
this.oob,
this.stableId,
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})
: _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;
@override
final StableStanzaId? stableId;
@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 {
// 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, stableId: $stableId, 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 &&
const DeepCollectionEquality().equals(other.done, done) &&
const DeepCollectionEquality().equals(other.cancel, cancel) &&
const DeepCollectionEquality()
.equals(other.cancelReason, cancelReason) &&
const DeepCollectionEquality().equals(other.stanza, stanza) &&
const DeepCollectionEquality()
.equals(other.retransmitted, retransmitted) &&
const DeepCollectionEquality().equals(other.sims, sims) &&
const DeepCollectionEquality().equals(other.sfs, sfs) &&
const DeepCollectionEquality().equals(other.oob, oob) &&
const DeepCollectionEquality().equals(other.stableId, stableId) &&
const DeepCollectionEquality().equals(other.reply, reply) &&
const DeepCollectionEquality().equals(other.chatState, chatState) &&
const DeepCollectionEquality().equals(other.isCarbon, isCarbon) &&
const DeepCollectionEquality().equals(
other.deliveryReceiptRequested, deliveryReceiptRequested) &&
const DeepCollectionEquality()
.equals(other.isMarkable, isMarkable) &&
const DeepCollectionEquality().equals(other.fun, fun) &&
const DeepCollectionEquality()
.equals(other.funReplacement, funReplacement) &&
const DeepCollectionEquality()
.equals(other.funCancellation, funCancellation) &&
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
const DeepCollectionEquality()
.equals(other.forceEncryption, forceEncryption) &&
const DeepCollectionEquality()
.equals(other.encryptionType, encryptionType) &&
const DeepCollectionEquality()
.equals(other.delayedDelivery, delayedDelivery) &&
const DeepCollectionEquality().equals(other._other, this._other) &&
const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions) &&
const DeepCollectionEquality()
.equals(other.stickerPackId, stickerPackId));
}
@override
int get hashCode => Object.hashAll([
runtimeType,
const DeepCollectionEquality().hash(done),
const DeepCollectionEquality().hash(cancel),
const DeepCollectionEquality().hash(cancelReason),
const DeepCollectionEquality().hash(stanza),
const DeepCollectionEquality().hash(retransmitted),
const DeepCollectionEquality().hash(sims),
const DeepCollectionEquality().hash(sfs),
const DeepCollectionEquality().hash(oob),
const DeepCollectionEquality().hash(stableId),
const DeepCollectionEquality().hash(reply),
const DeepCollectionEquality().hash(chatState),
const DeepCollectionEquality().hash(isCarbon),
const DeepCollectionEquality().hash(deliveryReceiptRequested),
const DeepCollectionEquality().hash(isMarkable),
const DeepCollectionEquality().hash(fun),
const DeepCollectionEquality().hash(funReplacement),
const DeepCollectionEquality().hash(funCancellation),
const DeepCollectionEquality().hash(encrypted),
const DeepCollectionEquality().hash(forceEncryption),
const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]);
@JsonKey(ignore: true)
@override
_$$_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 StableStanzaId? stableId,
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
StableStanzaId? get stableId;
@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

@ -9,6 +9,7 @@ const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas';
// RFC 6121
const rosterXmlns = 'jabber:iq:roster';
const rosterVersioningXmlns = 'urn:xmpp:features:rosterver';
const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
// XEP-0004
const dataFormsXmlns = 'jabber:x:data';
@ -99,7 +100,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0';
// XEP-0372
const referenceXmlns = 'urn:xmpp:reference:0';
// XEP-380
// XEP-0380
const emeXmlns = 'urn:xmpp:eme:0';
const emeOtr = 'urn:xmpp:otr:0';
const emeLegacyOpenPGP = 'jabber:x:encrypted';
@ -125,6 +126,9 @@ const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420
const sceXmlns = 'urn:xmpp:sce:1';
// XEP-0421
const occupantIdXmlns = 'urn:xmpp:occupant-id:0';
// XEP-0422
const fasteningXmlns = 'urn:xmpp:fasten:0';

View File

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

View File

@ -6,14 +6,44 @@ import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A function that will be called when presence, outside of subscription request
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
/// presence.
typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
/// A pseudo-negotiator that does not really negotiate anything. Instead, its purpose
/// is to look for a stream feature indicating that we can pre-approve subscription
/// requests, shown by [PresenceNegotiator.preApprovalSupported].
class PresenceNegotiator extends XmppFeatureNegotiatorBase {
PresenceNegotiator()
: super(11, false, subscriptionPreApprovalXmlns, presenceNegotiator);
/// Flag indicating whether presence subscription pre-approval is supported
bool _supported = false;
bool get preApprovalSupported => _supported;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
_supported = true;
return const Result(NegotiatorState.done);
}
@override
void reset() {
_supported = false;
super.reset();
}
}
/// A mandatory manager that handles initial presence sending, sending of subscription
/// request management requests and triggers events for incoming presence stanzas.
class PresenceManager extends XmppManagerBase {
@ -23,11 +53,17 @@ class PresenceManager extends XmppManagerBase {
final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true);
/// The priority of the presence handler. If a handler should run before this one,
/// which terminates processing, make sure the handler has a priority greater than
/// [presenceHandlerPriority].
static int presenceHandlerPriority = -100;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'presence',
callback: _onPresence,
priority: presenceHandlerPriority,
)
];
@ -66,7 +102,7 @@ class PresenceManager extends XmppManagerBase {
from: JID.fromString(presence.from!),
),
);
return state.copyWith(done: true);
return state..done = true;
}
default:
break;
@ -75,10 +111,7 @@ class PresenceManager extends XmppManagerBase {
if (presence.from != null) {
logger.finest("Received presence from '${presence.from}'");
getAttributes().sendEvent(
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
);
return state.copyWith(done: true);
return state..done = true;
}
return state;
@ -112,24 +145,82 @@ class PresenceManager extends XmppManagerBase {
}
/// Send an unavailable presence with no 'to' attribute.
void sendUnavailablePresence() {
getAttributes().sendStanza(
Future<void> sendUnavailablePresence() async {
// 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(
Stanza.presence(
type: 'unavailable',
),
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].
void sendSubscriptionRequest(String to) {
getAttributes().sendStanza(
/// Accept a subscription request from [to].
Future<void> acceptSubscriptionRequest(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribe',
to: to,
type: 'subscribed',
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,
),
@ -137,38 +228,12 @@ class PresenceManager extends XmppManagerBase {
}
/// Sends an unsubscription request to [to].
void sendUnsubscriptionRequest(String to) {
getAttributes().sendStanza(
Future<void> unsubscribe(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'unsubscribe',
to: to,
),
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,
to: to.toString(),
),
awaitable: false,
),

View File

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

View File

@ -9,6 +9,8 @@ class StanzaDetails {
this.awaitable = true,
this.encrypted = false,
this.forceEncryption = false,
this.bypassQueue = false,
this.excludeFromStreamManagement = false,
});
/// The stanza to send.
@ -23,6 +25,16 @@ class StanzaDetails {
final bool encrypted;
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

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

@ -60,7 +60,7 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
final Logger _log = Logger('FASTSaslNegotiator');
/// The token, if non-null, to use for authentication.
FASTToken? fastToken;
String? fastToken;
@override
bool matchesFeature(List<XMLNode> features) {
@ -100,11 +100,12 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final token = response.firstTag('token', xmlns: fastXmlns);
if (token != null) {
fastToken = FASTToken.fromXml(token);
final tokenElement = response.firstTag('token', xmlns: fastXmlns);
if (tokenElement != null) {
final token = FASTToken.fromXml(tokenElement);
fastToken = token.token;
await attributes.sendEvent(
NewFASTTokenReceivedEvent(fastToken!),
NewFASTTokenReceivedEvent(token),
);
}
@ -155,7 +156,7 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
@override
Future<String> getRawStep(String input) async {
return fastToken!.token;
return fastToken!;
}
@override

View File

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

View File

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

View File

@ -56,21 +56,17 @@ class VCardManager extends XmppManagerBase {
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
final hash = x.firstTag('photo')!.innerText();
final from = JID.fromString(presence.from!).toBare().toString();
final from = JID.fromString(presence.from!).toBare();
final lastHash = _lastHash[from];
if (lastHash != hash) {
_lastHash[from] = hash;
_lastHash[from.toString()] = hash;
final vcardResult = await requestVCard(from);
if (vcardResult.isType<VCard>()) {
final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) {
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
VCardAvatarUpdatedEvent(from, binval, hash),
);
} else {
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) {
@ -102,11 +98,11 @@ class VCardManager extends XmppManagerBase {
);
}
Future<Result<VCardError, VCard>> requestVCard(String jid) async {
Future<Result<VCardError, VCard>> requestVCard(JID jid) async {
final result = (await getAttributes().sendStanza(
StanzaDetails(
Stanza.iq(
to: jid,
to: jid.toString(),
type: 'get',
children: [
XMLNode.xmlns(

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 {
@ -179,13 +179,13 @@ class PubSubManager extends XmppManagerBase {
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 result = (await attrs.sendStanza(
StanzaDetails(
Stanza.iq(
type: 'set',
to: jid,
to: jid.toString(),
children: [
XMLNode.xmlns(
tag: 'pubsub',
@ -222,13 +222,13 @@ class PubSubManager extends XmppManagerBase {
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 result = (await attrs.sendStanza(
StanzaDetails(
Stanza.iq(
type: 'set',
to: jid,
to: jid.toString(),
children: [
XMLNode.xmlns(
tag: 'pubsub',
@ -398,14 +398,14 @@ class PubSubManager extends XmppManagerBase {
}
Future<Result<PubSubError, List<PubSubItem>>> getItems(
String jid,
JID jid,
String node,
) async {
final result = (await getAttributes().sendStanza(
StanzaDetails(
Stanza.iq(
type: 'get',
to: jid,
to: jid.toString(),
children: [
XMLNode.xmlns(
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/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A data class representing the jabber:x:oob tag.
class OOBData {
const OOBData({this.url, this.desc});
class OOBData implements StanzaHandlerExtension {
const OOBData(this.url, this.desc);
/// The communicated URL of the OOB data
final String? url;
/// The description of the url.
final String? desc;
}
XMLNode constructOOBNode(OOBData data) {
final children = List<XMLNode>.empty(growable: true);
if (data.url != null) {
children.add(XMLNode(tag: 'url', text: data.url));
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'x',
xmlns: oobDataXmlns,
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 {
@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase {
final url = x.firstTag('url');
final desc = x.firstTag('desc');
return state.copyWith(
oob: OOBData(
url: url?.innerText(),
desc: desc?.innerText(),
),
);
return state
..extensions.set(
OOBData(
url?.innerText(),
desc?.innerText(),
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<OOBData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
@ -15,10 +16,20 @@ abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {}
class UserAvatar {
const UserAvatar({required this.base64, required this.hash});
/// The result of a successful query of a users avatar.
class UserAvatarData {
const UserAvatarData(this.base64, this.hash);
/// The base64-encoded avatar data.
final String base64;
/// The SHA-1 hash of the raw avatar data.
final String hash;
/// The raw avatar data.
/// NOTE: Remove newlines because "Line feeds SHOULD NOT be added but MUST be accepted"
/// (https://xmpp.org/extensions/xep-0084.html#proto-data).
List<int> get data => base64Decode(base64.replaceAll('\n', ''));
}
class UserAvatarMetadata {
@ -27,21 +38,44 @@ class UserAvatarMetadata {
this.length,
this.width,
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;
/// The identifier of the avatar
/// The identifier of the avatar.
final String id;
/// Image proportions
final int width;
final int height;
/// Image proportions.
final int? width;
final int? height;
/// The MIME type of the avatar
final String mime;
/// The URL where the avatar can be found.
final String? url;
/// The MIME type of the avatar.
final String type;
}
/// NOTE: This class requires a PubSubManager
@ -51,13 +85,18 @@ class UserAvatarManager extends XmppManagerBase {
PubSubManager _getPubSubManager() =>
getAttributes().getManagerById(pubsubManager)! as PubSubManager;
@override
List<String> getDiscoFeatures() => [
'$userAvatarMetadataXmlns+notify',
];
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is PubSubNotificationEvent) {
if (event.item.node != userAvatarDataXmlns) return;
if (event.item.node != userAvatarMetadataXmlns) return;
if (event.item.payload.tag != 'data' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
if (event.item.payload.tag != 'metadata' ||
event.item.payload.attributes['xmlns'] != userAvatarMetadataXmlns) {
logger.warning(
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
);
@ -65,10 +104,12 @@ class UserAvatarManager extends XmppManagerBase {
}
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: event.from,
base64: event.item.payload.innerText(),
hash: event.item.id,
UserAvatarUpdatedEvent(
JID.fromString(event.from),
event.item.payload
.findTags('metadata', xmlns: userAvatarMetadataXmlns)
.map(UserAvatarMetadata.fromXML)
.toList(),
),
);
}
@ -80,7 +121,7 @@ class UserAvatarManager extends XmppManagerBase {
/// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
Future<Result<AvatarError, UserAvatarData>> getUserAvatar(JID jid) async {
final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
@ -90,9 +131,9 @@ class UserAvatarManager extends XmppManagerBase {
final item = results[0];
return Result(
UserAvatar(
base64: item.payload.innerText(),
hash: item.id,
UserAvatarData(
item.payload.innerText(),
item.id,
),
);
}
@ -146,7 +187,7 @@ class UserAvatarManager extends XmppManagerBase {
'bytes': metadata.length.toString(),
'height': metadata.height.toString(),
'width': metadata.width.toString(),
'type': metadata.mime,
'type': metadata.type,
'id': metadata.id,
},
),
@ -163,14 +204,14 @@ class UserAvatarManager extends XmppManagerBase {
}
/// 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);
return const Result(true);
}
/// 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);
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/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatState { active, composing, paused, inactive, gone }
enum ChatState implements StanzaHandlerExtension {
active,
composing,
paused,
inactive,
gone;
ChatState chatStateFromString(String raw) {
switch (raw) {
case 'active':
{
factory ChatState.fromName(String state) {
switch (state) {
case 'active':
return ChatState.active;
}
case 'composing':
{
case 'composing':
return ChatState.composing;
}
case 'paused':
{
case 'paused':
return ChatState.paused;
}
case 'inactive':
{
case 'inactive':
return ChatState.inactive;
}
case 'gone':
{
case 'gone':
return ChatState.gone;
}
default:
{
return ChatState.gone;
}
}
throw Exception('Invalid chat state $state');
}
String toName() {
switch (this) {
case ChatState.active:
return 'active';
case ChatState.composing:
return 'composing';
case ChatState.paused:
return 'paused';
case ChatState.inactive:
return 'inactive';
case ChatState.gone:
return 'gone';
}
}
XMLNode toXML() {
return XMLNode.xmlns(
tag: toName(),
xmlns: chatStateXmlns,
);
}
}
String chatStateToString(ChatState state) => state.toString().split('.').last;
class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager);
@ -64,62 +80,55 @@ class ChatStateManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
ChatState? chatState;
switch (element.tag) {
case 'active':
{
chatState = ChatState.active;
}
break;
case 'composing':
{
chatState = ChatState.composing;
}
break;
case 'paused':
{
chatState = ChatState.paused;
}
break;
case 'inactive':
{
chatState = ChatState.inactive;
}
break;
case 'gone':
{
chatState = ChatState.gone;
}
break;
default:
{
logger.warning("Received invalid chat state '${element.tag}'");
}
try {
state.extensions.set(ChatState.fromName(element.tag));
} catch (_) {
logger.finest('Ignoring invalid chat state ${element.tag}');
}
return state.copyWith(chatState: chatState);
return state;
}
/// Send a chat state notification to [to]. You can specify the type attribute
/// of the message with [messageType].
void sendChatState(
Future<void> sendChatState(
ChatState state,
String to, {
String messageType = 'chat',
}) {
final tagName = state.toString().split('.').last;
getAttributes().sendStanza(
}) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to,
type: messageType,
children: [
XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns),
state.toXML(),
],
),
awaitable: false,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<ChatState>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// 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
// 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 'state.dart';
@ -36,7 +36,8 @@ mixin _$StreamManagementState {
abstract class $StreamManagementStateCopyWith<$Res> {
factory $StreamManagementStateCopyWith(StreamManagementState value,
$Res Function(StreamManagementState) then) =
_$StreamManagementStateCopyWithImpl<$Res>;
_$StreamManagementStateCopyWithImpl<$Res, StreamManagementState>;
@useResult
$Res call(
{int c2s,
int s2c,
@ -45,39 +46,42 @@ abstract class $StreamManagementStateCopyWith<$Res> {
}
/// @nodoc
class _$StreamManagementStateCopyWithImpl<$Res>
class _$StreamManagementStateCopyWithImpl<$Res,
$Val extends StreamManagementState>
implements $StreamManagementStateCopyWith<$Res> {
_$StreamManagementStateCopyWithImpl(this._value, this._then);
final StreamManagementState _value;
// ignore: unused_field
final $Res Function(StreamManagementState) _then;
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? c2s = freezed,
Object? s2c = freezed,
Object? c2s = null,
Object? s2c = null,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_value.copyWith(
c2s: c2s == freezed
c2s: null == c2s
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
s2c: s2c == freezed
s2c: null == s2c
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: streamResumptionLocation == freezed
streamResumptionLocation: freezed == streamResumptionLocation
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: streamResumptionId == freezed
streamResumptionId: freezed == streamResumptionId
? _value.streamResumptionId
: streamResumptionId // ignore: cast_nullable_to_non_nullable
as String?,
));
) as $Val);
}
}
@ -88,6 +92,7 @@ abstract class _$$_StreamManagementStateCopyWith<$Res>
$Res Function(_$_StreamManagementState) then) =
__$$_StreamManagementStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int c2s,
int s2c,
@ -97,37 +102,34 @@ abstract class _$$_StreamManagementStateCopyWith<$Res>
/// @nodoc
class __$$_StreamManagementStateCopyWithImpl<$Res>
extends _$StreamManagementStateCopyWithImpl<$Res>
extends _$StreamManagementStateCopyWithImpl<$Res, _$_StreamManagementState>
implements _$$_StreamManagementStateCopyWith<$Res> {
__$$_StreamManagementStateCopyWithImpl(_$_StreamManagementState _value,
$Res Function(_$_StreamManagementState) _then)
: super(_value, (v) => _then(v as _$_StreamManagementState));
@override
_$_StreamManagementState get _value =>
super._value as _$_StreamManagementState;
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? c2s = freezed,
Object? s2c = freezed,
Object? c2s = null,
Object? s2c = null,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_$_StreamManagementState(
c2s == freezed
null == c2s
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
s2c == freezed
null == s2c
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: streamResumptionLocation == freezed
streamResumptionLocation: freezed == streamResumptionLocation
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: streamResumptionId == freezed
streamResumptionId: freezed == streamResumptionId
? _value.streamResumptionId
: streamResumptionId // ignore: cast_nullable_to_non_nullable
as String?,
@ -163,25 +165,23 @@ class _$_StreamManagementState implements _StreamManagementState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StreamManagementState &&
const DeepCollectionEquality().equals(other.c2s, c2s) &&
const DeepCollectionEquality().equals(other.s2c, s2c) &&
const DeepCollectionEquality().equals(
other.streamResumptionLocation, streamResumptionLocation) &&
const DeepCollectionEquality()
.equals(other.streamResumptionId, streamResumptionId));
(identical(other.c2s, c2s) || other.c2s == c2s) &&
(identical(other.s2c, s2c) || other.s2c == s2c) &&
(identical(
other.streamResumptionLocation, streamResumptionLocation) ||
other.streamResumptionLocation == streamResumptionLocation) &&
(identical(other.streamResumptionId, streamResumptionId) ||
other.streamResumptionId == streamResumptionId));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(c2s),
const DeepCollectionEquality().hash(s2c),
const DeepCollectionEquality().hash(streamResumptionLocation),
const DeepCollectionEquality().hash(streamResumptionId));
runtimeType, c2s, s2c, streamResumptionLocation, streamResumptionId);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith =>
__$$_StreamManagementStateCopyWithImpl<_$_StreamManagementState>(
this, _$identity);

View File

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

View File

@ -15,6 +15,7 @@ import 'package:moxxmpp/src/xeps/xep_0198/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:synchronized/synchronized.dart';
const xmlUintMax = 4294967296; // 2**32
@ -399,10 +400,14 @@ class StreamManagementManager extends XmppManagerBase {
Stanza stanza,
StanzaHandlerData state,
) async {
await _incrementC2S();
_unackedStanzas[_state.c2s] = stanza;
if (isStreamManagementEnabled()) {
await _incrementC2S();
if (state.extensions.get<StreamManagementData>()?.exclude ?? false) {
return state;
}
_unackedStanzas[_state.c2s] = stanza;
await _sendAckRequest();
}
@ -414,6 +419,8 @@ class StreamManagementManager extends XmppManagerBase {
_unackedStanzas.clear();
for (final stanza in stanzas) {
logger
.finest('Resending ${stanza.tag} with id ${stanza.attributes["id"]}');
await getAttributes().sendStanza(
StanzaDetails(
stanza,

View File

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

View File

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

View File

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

View File

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

View File

@ -4,27 +4,86 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeChatMarkerMarkable() {
return XMLNode.xmlns(
tag: 'markable',
xmlns: chatMarkersXmlns,
);
enum ChatMarker {
received,
displayed,
acknowledged;
factory ChatMarker.fromName(String name) {
switch (name) {
case 'received':
return ChatMarker.received;
case 'displayed':
return ChatMarker.displayed;
case 'acknowledged':
return ChatMarker.acknowledged;
}
throw Exception('Invalid chat marker $name');
}
XMLNode toXML() {
String tag;
switch (this) {
case ChatMarker.received:
tag = 'received';
break;
case ChatMarker.displayed:
tag = 'displayed';
break;
case ChatMarker.acknowledged:
tag = 'acknowledged';
break;
}
return XMLNode.xmlns(
tag: tag,
xmlns: chatMarkersXmlns,
);
}
}
XMLNode makeChatMarker(String tag, String id) {
assert(
['received', 'displayed', 'acknowledged'].contains(tag),
'Invalid chat marker',
);
return XMLNode.xmlns(
tag: tag,
xmlns: chatMarkersXmlns,
attributes: {'id': id},
);
class MarkableData implements StanzaHandlerExtension {
const MarkableData(this.isMarkable);
/// Indicates whether the message can be replied to with a chat marker.
final bool isMarkable;
XMLNode toXML() {
assert(isMarkable, '');
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 {
@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase {
Stanza message,
StanzaHandlerData state,
) async {
final marker = message.firstTagByXmlns(chatMarkersXmlns)!;
final element = message.firstTagByXmlns(chatMarkersXmlns)!;
// Handle the <markable /> explicitly
if (marker.tag == 'markable') return state.copyWith(isMarkable: true);
if (!['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,
),
);
if (element.tag == 'markable') {
return state..extensions.set(const MarkableData(true));
}
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/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum MessageProcessingHint {
noPermanentStore,
noStore,
noCopies,
store,
}
store;
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
switch (element.tag) {
case 'no-permanent-store':
return MessageProcessingHint.noPermanentStore;
case 'no-store':
return MessageProcessingHint.noStore;
case 'no-copy':
return MessageProcessingHint.noCopies;
case 'store':
return MessageProcessingHint.store;
factory MessageProcessingHint.fromName(String name) {
switch (name) {
case 'no-permanent-store':
return MessageProcessingHint.noPermanentStore;
case 'no-store':
return MessageProcessingHint.noStore;
case 'no-copy':
return MessageProcessingHint.noCopies;
case 'store':
return MessageProcessingHint.store;
}
assert(false, 'Invalid Message Processing Hint: $name');
return MessageProcessingHint.noStore;
}
assert(false, 'Invalid Message Processing Hint: ${element.tag}');
return MessageProcessingHint.noStore;
}
extension XmlExtension on MessageProcessingHint {
XMLNode toXml() {
XMLNode toXML() {
String tag;
switch (this) {
case MessageProcessingHint.noPermanentStore:
@ -48,3 +53,60 @@ extension XmlExtension on MessageProcessingHint {
);
}
}
class MessageProcessingHintData implements StanzaHandlerExtension {
const MessageProcessingHintData(this.hints);
/// The attached message processing hints.
final List<MessageProcessingHint> hints;
}
class MessageProcessingHintManager extends XmppManagerBase {
MessageProcessingHintManager() : super(messageProcessingHintManager);
@override
Future<bool> isSupported() async => true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagXmlns: messageProcessingHintsXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
),
];
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final elements = stanza.findTagsByXmlns(messageProcessingHintsXmlns);
return state
..extensions.set(
MessageProcessingHintData(
elements
.map((element) => MessageProcessingHint.fromName(element.tag))
.toList(),
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageProcessingHintData>();
return data != null ? data.hints.map((hint) => hint.toXML()).toList() : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

@ -3,28 +3,69 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// Represents data provided by XEP-0359.
/// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of
/// the message stanza.
class StableStanzaId {
const StableStanzaId({this.originId, this.stanzaId, this.stanzaIdBy});
final String? originId;
final String? stanzaId;
final String? stanzaIdBy;
/// Representation of a <stanza-id /> element.
class StanzaId {
const StanzaId(
this.id,
this.by,
);
/// The unique stanza id.
final String id;
/// The JID the id was generated by.
final JID by;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'stanza-id',
xmlns: stableIdXmlns,
attributes: {
'id': id,
'by': by.toString(),
},
);
}
}
XMLNode makeOriginIdElement(String id) {
return XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': id},
);
class StableIdData implements StanzaHandlerExtension {
const StableIdData(this.originId, this.stanzaIds);
/// <origin-id />
final String? originId;
/// Stanza ids
final List<StanzaId>? stanzaIds;
XMLNode toOriginIdElement() {
assert(
originId != null,
'Can only build the XML element if originId != null',
);
return XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': 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 {
@ -50,50 +91,52 @@ class StableIdManager extends XmppManagerBase {
Stanza message,
StanzaHandlerData state,
) async {
final from = JID.fromString(message.attributes['from']! as String);
String? originId;
String? stanzaId;
String? stanzaIdBy;
final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns);
final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns);
List<StanzaId>? stanzaIds;
final originIdElement = message.firstTag('origin-id', xmlns: stableIdXmlns);
final stanzaIdElements =
message.findTags('stanza-id', xmlns: stableIdXmlns);
// Process the origin id
if (originIdTag != null) {
logger.finest('Found origin Id tag');
originId = originIdTag.attributes['id']! as String;
if (originIdElement != null) {
originId = originIdElement.attributes['id']! as String;
}
// Process the stanza id tag
if (stanzaIdTag != null) {
logger.finest('Found stanza Id tag');
final attrs = getAttributes();
final disco = attrs.getManagerById<DiscoManager>(discoManager)!;
final result = await disco.discoInfoQuery(from);
if (result.isType<DiscoInfo>()) {
final info = result.get<DiscoInfo>();
logger.finest('Got info for ${from.toString()}');
if (info.features.contains(stableIdXmlns)) {
logger.finest('${from.toString()} supports $stableIdXmlns.');
stanzaId = stanzaIdTag.attributes['id']! as String;
stanzaIdBy = stanzaIdTag.attributes['by']! as String;
} else {
logger.finest(
'${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... ',
);
}
} else {
logger.finest(
'Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... ',
);
}
if (stanzaIdElements.isNotEmpty) {
stanzaIds = stanzaIdElements
.map(
(element) => StanzaId(
element.attributes['id']! as String,
JID.fromString(element.attributes['by']! as String),
),
)
.toList();
}
return state.copyWith(
stableId: StableStanzaId(
originId: originId,
stanzaId: stanzaId,
stanzaIdBy: stanzaIdBy,
),
);
return state
..extensions.set(
StableIdData(
originId,
stanzaIds,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StableIdData>();
return data != null ? data.toXML() : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
@ -276,7 +277,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Add a storage hint in case this is a message
// Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
MessageProcessingHint.store.toXml(),
MessageProcessingHint.store.toXML(),
],
),
awaitable: false,
@ -363,19 +364,18 @@ abstract class BaseOmemoManager extends XmppManagerBase {
logger.finest('Encryption done');
if (!result.isSuccess(2)) {
final other = Map<String, dynamic>.from(state.other);
other['encryption_error_jids'] = result.jidEncryptionErrors;
other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith(
other: other,
return state
..cancel = true
// If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()]
..cancelReason = result.jidEncryptionErrors[toJid.toString()]
is NoKeyMaterialAvailableException
? OmemoNotSupportedForContactException()
: UnknownOmemoError(),
cancel: true,
);
: UnknownOmemoError()
..encryptionError = OmemoEncryptionError(
result.jidEncryptionErrors,
result.deviceEncryptionErrors,
);
}
final encrypted = _buildEncryptedElement(
@ -389,19 +389,16 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (stanza.tag == 'message') {
children
// Add EME data
..add(buildEmeElement(ExplicitEncryptionType.omemo2))
..add(ExplicitEncryptionType.omemo2.toXML())
// Add a storage hint in case this is a message
// Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
..add(MessageProcessingHint.store.toXml());
..add(MessageProcessingHint.store.toXML());
}
return state.copyWith(
stanza: state.stanza.copyWith(
children: children,
),
encrypted: true,
);
return state
..stanza = state.stanza.copyWith(children: children)
..encrypted = true;
}
/// This function is called whenever a message is to be encrypted. If it returns true,
@ -444,17 +441,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
OmemoIncomingStanza(
fromJid.toString(),
sid,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ??
state.extensions
.get<DelayedDeliveryData>()
?.timestamp
.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys,
payloadElement?.innerText(),
),
);
final other = Map<String, dynamic>.from(state.other);
var children = stanza.children;
if (result.error != null) {
other['encryption_error'] = result.error;
state.encryptionError = result.error;
} else {
children = stanza.children
.where(
@ -471,11 +470,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
envelope = XMLNode.fromString(result.payload!);
} on XmlParserException catch (_) {
logger.warning('Failed to parse envelope payload: ${result.payload!}');
other['encryption_error'] = InvalidEnvelopePayloadException();
return state.copyWith(
encrypted: true,
other: other,
);
return state
..encrypted = true
..encryptionError = InvalidEnvelopePayloadException();
}
final envelopeChildren = envelope.firstTag('content')?.children;
@ -489,13 +486,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException();
state.encryptionError = InvalidAffixElementsException();
}
}
return state.copyWith(
encrypted: true,
stanza: Stanza(
return state
..encrypted = true
..stanza = Stanza(
to: stanza.to,
from: stanza.from,
id: stanza.id,
@ -503,9 +500,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
),
other: other,
);
);
}
/// Convenience function that attempts to retrieve the raw XML payload from the
@ -516,8 +511,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
JID jid,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result =
await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns);
final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns);
if (result.isType<PubSubError>()) return Result(UnknownOmemoError());
return Result(result.get<List<PubSubItem>>().first.payload);
}
@ -543,7 +537,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
) async {
// TODO(Unknown): Should we query the device list first?
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());
final bundles = bundlesRaw
@ -639,7 +633,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Subscribes to the device list PubSub node of [jid].
Future<void> subscribeToDeviceListImpl(String jid) async {
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.

View File

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

View File

@ -1,6 +1,6 @@
import 'package:moxxmpp/src/stringxml.dart';
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation
/// A data class describing the user agent. See https://xmpp.org/extensions/xep-0388.html#initiation.
class UserAgent {
const UserAgent({
this.id,
@ -24,11 +24,9 @@ class UserAgent {
);
return XMLNode(
tag: 'user-agent',
attributes: id != null
? {
'id': id,
}
: {},
attributes: {
if (id != null) 'id': id,
},
children: [
if (software != null)
XMLNode(

View File

@ -20,12 +20,10 @@ enum Sasl2State {
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
/// registered with other negotiators that register themselves against this one.
class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
Sasl2Negotiator({
this.userAgent,
}) : super(100, false, sasl2Xmlns, sasl2Negotiator);
Sasl2Negotiator() : super(100, false, sasl2Xmlns, sasl2Negotiator);
/// The user agent data that will be sent to the server when authenticating.
final UserAgent? userAgent;
UserAgent? userAgent;
/// List of callbacks that are registered against us. Will be called once we get
/// SASL2 features.

View File

@ -0,0 +1,61 @@
import 'dart:async';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
/// Representation of a <occupant-id /> element.
class OccupantIdData implements StanzaHandlerExtension {
const OccupantIdData(
this.id,
);
/// The unique occupant id.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'occupant-id',
xmlns: occupantIdXmlns,
attributes: {
'id': id,
},
);
}
}
class OccupantIdManager extends XmppManagerBase {
OccupantIdManager() : super(occupantIdManager);
@override
List<String> getDiscoFeatures() => [
occupantIdXmlns,
];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'occupant-id',
tagXmlns: occupantIdXmlns,
callback: _onMessage,
// Before the MessageManager
priority: MessageManager.messageHandlerPriority + 1,
),
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
return state
..extensions.set(OccupantIdData(stanza.attributes['id']! as String));
}
}

View File

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

View File

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

View File

@ -4,11 +4,13 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart';
@ -227,6 +229,19 @@ class StickerPack {
}
}
class StickersData implements StanzaHandlerExtension {
const StickersData(this.stickerPackId, this.sticker, {this.addBody = true});
/// The id of the sticker pack the referenced sticker is from.
final String stickerPackId;
/// The metadata of the sticker.
final StatelessFileSharingData sticker;
/// If true, sets the sticker's metadata desc attribute as the message body.
final bool addBody;
}
class StickersManager extends XmppManagerBase {
StickersManager() : super(stickersManager);
@ -249,9 +264,35 @@ class StickersManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state.copyWith(
stickerPackId: sticker.attributes['pack']! as String,
);
return state
..extensions.set(
StickersData(
sticker.attributes['pack']! as String,
state.extensions.get<StatelessFileSharingData>()!,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StickersData>();
return data != null
? [
XMLNode.xmlns(
tag: 'sticker',
xmlns: stickersXmlns,
attributes: {
'pack': data.stickerPackId,
},
),
data.sticker.toXML(),
// Add a body
if (data.addBody && data.sticker.metadata.desc != null)
MessageBodyData(data.sticker.metadata.desc).toXML(),
]
: [];
}
/// Publishes the StickerPack [pack] to the PubSub node of [jid]. If specified, then
@ -319,4 +360,14 @@ class StickersManager extends XmppManagerBase {
return Result(stickerPack);
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
import 'dart:convert';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Test accepting newlines', () {
const data = UserAvatarData(
'cGFwYXR1d\nHV3\n\nYXdh',
'some-id',
);
expect(utf8.decode(data.data), 'papatutuwawa');
});
}

View File

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

View File

@ -15,8 +15,8 @@ Future<void> runIncomingStanzaHandlers(
StanzaHandlerData(
false,
false,
null,
stanza,
TypedMap(),
),
);
}
@ -34,8 +34,8 @@ Future<void> runOutgoingStanzaHandlers(
StanzaHandlerData(
false,
false,
null,
stanza,
TypedMap(),
),
);
}
@ -850,13 +850,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator()..resource = 'test-resource',
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -948,13 +947,12 @@ void main() {
ResourceBindingNegotiator(),
StreamManagementNegotiator()..resource = 'test-resource',
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -1051,13 +1049,12 @@ void main() {
ResourceBindingNegotiator(),
smn,
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

View File

@ -114,13 +114,12 @@ void main() {
ResourceBindingNegotiator(),
CarbonsNegotiator(),
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

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

@ -184,13 +184,12 @@ void main() {
FASTSaslNegotiator(),
Bind2Negotiator(),
CSINegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

View File

@ -59,13 +59,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -129,13 +128,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Bind2Negotiator()..tag = 'moxxmpp',
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

View File

@ -115,13 +115,12 @@ void main() {
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -199,13 +198,12 @@ void main() {
ScramHashType.sha256,
),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -276,13 +274,12 @@ void main() {
ScramHashType.sha256,
),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -356,13 +353,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
ExampleNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
@ -439,13 +435,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
ExampleNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(

View File

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

View File

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

View File

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

View File

@ -121,13 +121,12 @@ void main() {
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result1 = await conn.connect(
@ -142,8 +141,7 @@ void main() {
final token = conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken;
expect(token != null, true);
expect(token!.token, 'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm');
expect(token, 'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm');
// Disconnect
await conn.disconnect();
@ -233,18 +231,13 @@ void main() {
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator()
..fastToken = const FASTToken(
'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm',
'2020-03-12T14:36:15Z',
),
Sasl2Negotiator(
userAgent: const UserAgent(
FASTSaslNegotiator()..fastToken = 'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm',
Sasl2Negotiator()
..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result1 = await conn.connect(
@ -259,7 +252,6 @@ void main() {
final token = conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken;
expect(token != null, true);
expect(token!.token, 'ed00e36cb42449a365a306a413f51ffd5ea8');
expect(token, 'ed00e36cb42449a365a306a413f51ffd5ea8');
});
}

View File

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