feat(all): Various changes

- Fix unavailable presence being sent *after* connecting
- Migrate more APIs to the JID class
- Advertise +notify for user avatar metadata
This commit is contained in:
PapaTutuWawa 2023-06-02 22:00:44 +02:00
parent 1475cb542f
commit 9e0f38154e
12 changed files with 217 additions and 106 deletions

View File

@ -405,8 +405,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 +423,17 @@ class XmppConnection {
);
final completer = details.awaitable ? Completer<XMLNode>() : null;
await _stanzaQueue.enqueueStanza(
StanzaQueueEntry(
final entry = StanzaQueueEntry(
details,
completer,
),
);
if (details.bypassQueue) {
await _sendStanzaImpl(entry);
} else {
await _stanzaQueue.enqueueStanza(entry);
}
return completer?.future;
}
@ -523,7 +526,7 @@ 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
@ -535,6 +538,7 @@ class XmppConnection {
false,
null,
newStanza,
excludeFromStreamManagement: details.excludeFromStreamManagement,
),
);
_log.fine('Done');
@ -835,7 +839,7 @@ class XmppConnection {
await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) {
getPresenceManager()?.sendUnavailablePresence();
await getPresenceManager()?.sendUnavailablePresence();
}
_socket.prepareDisconnect();

View File

@ -7,6 +7,7 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0084.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
@ -192,15 +193,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

@ -75,5 +75,8 @@ class StanzaHandlerData with _$StanzaHandlerData {
MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to
String? stickerPackId,
// Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
@Default(false) bool excludeFromStreamManagement,
}) = _StanzaHandlerData;
}

View File

@ -71,7 +71,10 @@ mixin _$StanzaHandlerData {
throw _privateConstructorUsedError; // Reactions data
MessageReactions? get messageReactions =>
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
String? get stickerPackId => throw _privateConstructorUsedError;
String? get stickerPackId =>
throw _privateConstructorUsedError; // Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
bool get excludeFromStreamManagement => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@ -111,7 +114,8 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
String? stickerPackId,
bool excludeFromStreamManagement});
}
/// @nodoc
@ -154,6 +158,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res, $Val extends StanzaHandlerData>
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
Object? excludeFromStreamManagement = null,
}) {
return _then(_value.copyWith(
done: null == done
@ -264,6 +269,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res, $Val extends StanzaHandlerData>
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
excludeFromStreamManagement: null == excludeFromStreamManagement
? _value.excludeFromStreamManagement
: excludeFromStreamManagement // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
@ -303,7 +312,8 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
String? stickerPackId,
bool excludeFromStreamManagement});
}
/// @nodoc
@ -344,6 +354,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
Object? excludeFromStreamManagement = null,
}) {
return _then(_$_StanzaHandlerData(
null == done
@ -454,6 +465,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
excludeFromStreamManagement: null == excludeFromStreamManagement
? _value.excludeFromStreamManagement
: excludeFromStreamManagement // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@ -484,7 +499,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.messageRetraction,
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId})
this.stickerPackId,
this.excludeFromStreamManagement = false})
: _stanzaIds = stanzaIds,
_other = other;
@ -595,10 +611,15 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
// The Id of the sticker pack this sticker belongs to
@override
final String? stickerPackId;
// Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
@override
@JsonKey()
final bool excludeFromStreamManagement;
@override
String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, originId: $originId, stanzaIds: $stanzaIds, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, originId: $originId, stanzaIds: $stanzaIds, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId, excludeFromStreamManagement: $excludeFromStreamManagement)';
}
@override
@ -652,7 +673,11 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
(identical(other.messageReactions, messageReactions) ||
other.messageReactions == messageReactions) &&
(identical(other.stickerPackId, stickerPackId) ||
other.stickerPackId == stickerPackId));
other.stickerPackId == stickerPackId) &&
(identical(other.excludeFromStreamManagement,
excludeFromStreamManagement) ||
other.excludeFromStreamManagement ==
excludeFromStreamManagement));
}
@override
@ -684,7 +709,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
messageRetraction,
lastMessageCorrectionSid,
messageReactions,
stickerPackId
stickerPackId,
excludeFromStreamManagement
]);
@JsonKey(ignore: true)
@ -720,7 +746,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions,
final String? stickerPackId}) = _$_StanzaHandlerData;
final String? stickerPackId,
final bool excludeFromStreamManagement}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
@ -786,6 +813,9 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
MessageReactions? get messageReactions;
@override // The Id of the sticker pack this sticker belongs to
String? get stickerPackId;
@override // Flag indicating whether the stanza should be excluded from stream management's
// resending behaviour
bool get excludeFromStreamManagement;
@override
@JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>

View File

@ -112,24 +112,48 @@ 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,
),
);
}
/// Sends a subscription request to [to].
// TODO(PapaTutuWawa): Check if we're allowed to pre-approve
Future<void> requestSubscription(JID to, {bool preApprove = false}) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: preApprove ? 'subscribed' : '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 requestSubscription(to, preApprove: true);
}
/// Send a subscription request rejection to [to].
Future<void> rejectSubscriptionRequest(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribe',
to: to,
type: 'unsubscribed',
to: to.toString(),
),
awaitable: false,
),
@ -137,38 +161,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

@ -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

@ -66,11 +66,7 @@ class VCardManager extends XmppManagerBase {
final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) {
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
VCardAvatarUpdatedEvent(JID.fromString(from), binval, hash),
);
} else {
logger.warning('No avatar data found');

View File

@ -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

@ -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,17 @@ abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {}
class UserAvatar {
const UserAvatar({required this.base64, required this.hash});
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.
List<int> get data => base64Decode(base64);
}
class UserAvatarMetadata {
@ -27,21 +35,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 +82,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 +101,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 +118,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 +128,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 +184,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 +201,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

@ -399,10 +399,12 @@ class StreamManagementManager extends XmppManagerBase {
Stanza stanza,
StanzaHandlerData state,
) async {
await _incrementC2S();
_unackedStanzas[_state.c2s] = stanza;
if (isStreamManagementEnabled()) {
await _incrementC2S();
if (state.excludeFromStreamManagement) return state;
_unackedStanzas[_state.c2s] = stanza;
await _sendAckRequest();
}
@ -414,6 +416,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

@ -516,8 +516,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 +542,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 +638,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.