diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 3794614..7336a71 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -79,4 +79,5 @@ export 'package:moxxmpp/src/xeps/xep_0444.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0447.dart'; export 'package:moxxmpp/src/xeps/xep_0448.dart'; +export 'package:moxxmpp/src/xeps/xep_0449.dart'; export 'package:moxxmpp/src/xeps/xep_0461.dart'; diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 44b6d47..14c3dd9 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -79,6 +79,7 @@ class MessageEvent extends XmppEvent { this.messageCorrectionId, this.messageReactions, this.messageProcessingHints, + this.stickerPackId, }); final StanzaError? error; final String body; @@ -103,6 +104,7 @@ class MessageEvent extends XmppEvent { final String? messageCorrectionId; final MessageReactions? messageReactions; final List? messageProcessingHints; + final String? stickerPackId; final Map other; } diff --git a/packages/moxxmpp/lib/src/managers/data.dart b/packages/moxxmpp/lib/src/managers/data.dart index e97d7e1..e964e6a 100644 --- a/packages/moxxmpp/lib/src/managers/data.dart +++ b/packages/moxxmpp/lib/src/managers/data.dart @@ -64,6 +64,8 @@ class StanzaHandlerData with _$StanzaHandlerData { String? lastMessageCorrectionSid, // Reactions data MessageReactions? messageReactions, + // The Id of the sticker pack this sticker belongs to + String? stickerPackId, } ) = _StanzaHandlerData; } diff --git a/packages/moxxmpp/lib/src/managers/data.freezed.dart b/packages/moxxmpp/lib/src/managers/data.freezed.dart index 7a61819..8c72edf 100644 --- a/packages/moxxmpp/lib/src/managers/data.freezed.dart +++ b/packages/moxxmpp/lib/src/managers/data.freezed.dart @@ -61,7 +61,9 @@ mixin _$StanzaHandlerData { 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; + MessageReactions? get messageReactions => + throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to + String? get stickerPackId => throw _privateConstructorUsedError; @JsonKey(ignore: true) $StanzaHandlerDataCopyWith get copyWith => @@ -97,7 +99,8 @@ abstract class $StanzaHandlerDataCopyWith<$Res> { Map other, MessageRetractionData? messageRetraction, String? lastMessageCorrectionSid, - MessageReactions? messageReactions}); + MessageReactions? messageReactions, + String? stickerPackId}); } /// @nodoc @@ -135,6 +138,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> Object? messageRetraction = freezed, Object? lastMessageCorrectionSid = freezed, Object? messageReactions = freezed, + Object? stickerPackId = freezed, }) { return _then(_value.copyWith( done: done == freezed @@ -233,6 +237,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> ? _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?, )); } } @@ -268,7 +276,8 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res> Map other, MessageRetractionData? messageRetraction, String? lastMessageCorrectionSid, - MessageReactions? messageReactions}); + MessageReactions? messageReactions, + String? stickerPackId}); } /// @nodoc @@ -308,6 +317,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> Object? messageRetraction = freezed, Object? lastMessageCorrectionSid = freezed, Object? messageReactions = freezed, + Object? stickerPackId = freezed, }) { return _then(_$_StanzaHandlerData( done == freezed @@ -406,6 +416,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> ? _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?, )); } } @@ -433,7 +447,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { final Map other = const {}, this.messageRetraction, this.lastMessageCorrectionSid, - this.messageReactions}) + this.messageReactions, + this.stickerPackId}) : _other = other; // Indicates to the runner that processing is now done. This means that all @@ -519,10 +534,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { // 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, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions)'; + return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)'; } @override @@ -564,7 +582,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { const DeepCollectionEquality().equals( other.lastMessageCorrectionSid, lastMessageCorrectionSid) && const DeepCollectionEquality() - .equals(other.messageReactions, messageReactions)); + .equals(other.messageReactions, messageReactions) && + const DeepCollectionEquality() + .equals(other.stickerPackId, stickerPackId)); } @override @@ -593,7 +613,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { const DeepCollectionEquality().hash(_other), const DeepCollectionEquality().hash(messageRetraction), const DeepCollectionEquality().hash(lastMessageCorrectionSid), - const DeepCollectionEquality().hash(messageReactions) + const DeepCollectionEquality().hash(messageReactions), + const DeepCollectionEquality().hash(stickerPackId) ]); @JsonKey(ignore: true) @@ -625,7 +646,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { final Map other, final MessageRetractionData? messageRetraction, final String? lastMessageCorrectionSid, - final MessageReactions? messageReactions}) = _$_StanzaHandlerData; + 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. @@ -682,6 +704,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { 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 => diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index aeb61ed..bc0bae4 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -27,3 +27,4 @@ const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager'; const messageRetractionManager = 'org.moxxmpp.messageretractionmanager'; const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; +const stickersManager = 'org.moxxmpp.stickersmanager'; diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index 9c09cde..b435f87 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -21,6 +21,11 @@ import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart'; +/// Data used to build a message stanza. +/// +/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be +/// added. This is recommended when sharing files but may cause issues when the message +/// stanza should include a SFS element without any fallbacks. class MessageDetails { const MessageDetails({ required this.to, @@ -42,6 +47,8 @@ class MessageDetails { this.lastMessageCorrectionId, this.messageReactions, this.messageProcessingHints, + this.stickerPackId, + this.setOOBFallbackBody = true, }); final String to; final String? body; @@ -61,7 +68,9 @@ class MessageDetails { final MessageRetractionData? messageRetraction; final String? lastMessageCorrectionId; final MessageReactions? messageReactions; + final String? stickerPackId; final List? messageProcessingHints; + final bool setOOBFallbackBody; } class MessageManager extends XmppManagerBase { @@ -117,6 +126,7 @@ class MessageManager extends XmppManagerBase { messageProcessingHints: hints.isEmpty ? null : hints, + stickerPackId: state.stickerPackId, other: state.other, error: StanzaError.fromStanza(message), ),); @@ -174,7 +184,7 @@ class MessageManager extends XmppManagerBase { ); } else { var body = details.body; - if (details.sfs != null) { + if (details.sfs != null && details.setOOBFallbackBody) { // TODO(Unknown): Maybe find a better solution final firstSource = details.sfs!.sources.first; if (firstSource is StatelessFileSharingUrlSource) { @@ -207,7 +217,7 @@ class MessageManager extends XmppManagerBase { stanza.addChild(details.sfs!.toXML()); final source = details.sfs!.sources.first; - if (source is StatelessFileSharingUrlSource) { + if (source is StatelessFileSharingUrlSource && details.setOOBFallbackBody) { // SFS recommends OOB as a fallback stanza.addChild(constructOOBNode(OOBData(url: source.url))); } @@ -288,6 +298,18 @@ class MessageManager extends XmppManagerBase { stanza.addChild(hint.toXml()); } } + + if (details.stickerPackId != null) { + stanza.addChild( + XMLNode.xmlns( + tag: 'sticker', + xmlns: stickersXmlns, + attributes: { + 'pack': details.stickerPackId!, + }, + ), + ); + } getAttributes().sendStanza(stanza, awaitable: false); } diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index 5204bb0..08de65e 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -141,6 +141,9 @@ const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopad const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'; +// XEP-0449 +const stickersXmlns = 'urn:xmpp:stickers:0'; + // XEP-0461 const replyXmlns = 'urn:xmpp:reply:0'; const fallbackXmlns = 'urn:xmpp:feature-fallback:0'; diff --git a/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart b/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart index 5a83074..1235c1e 100644 --- a/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_4790.dart @@ -24,3 +24,28 @@ int ioctetSortComparator(String a, String b) { return 1; } + +int ioctetSortComparatorRaw(List a, List b) { + if (a.isEmpty && b.isEmpty) { + return 0; + } + + if (a.isEmpty && b.isNotEmpty) { + return -1; + } + + if (a.isNotEmpty && b.isEmpty) { + return 1; + } + + if (a[0] == b[0]) { + return ioctetSortComparatorRaw(a.sublist(1), b.sublist(1)); + } + + // TODO(Unknown): Is this correct? + if (a[0] < b[0]) { + return -1; + } + + return 1; +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart b/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart index 2cbf409..49f8f0b 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart @@ -16,7 +16,6 @@ import 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; class PubSubPublishOptions { - const PubSubPublishOptions({ this.accessModel, this.maxItems, @@ -60,7 +59,6 @@ class PubSubPublishOptions { } class PubSubItem { - const PubSubItem({ required this.id, required this.node, required this.payload }); final String id; final String node; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0203.dart b/packages/moxxmpp/lib/src/xeps/xep_0203.dart index 2bf4fc5..05eec8a 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0203.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0203.dart @@ -8,14 +8,12 @@ import 'package:moxxmpp/src/stanza.dart'; @immutable class DelayedDelivery { - const DelayedDelivery(this.from, this.timestamp); final DateTime timestamp; final String from; } class DelayedDeliveryManager extends XmppManagerBase { - @override String getId() => delayedDeliveryManager; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0446.dart b/packages/moxxmpp/lib/src/xeps/xep_0446.dart index c2d2b70..fafc493 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0446.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0446.dart @@ -4,7 +4,6 @@ import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; import 'package:moxxmpp/src/xeps/xep_0300.dart'; class FileMetadataData { - const FileMetadataData({ this.mediaType, this.width, diff --git a/packages/moxxmpp/lib/src/xeps/xep_0447.dart b/packages/moxxmpp/lib/src/xeps/xep_0447.dart index d5933c9..0d9996d 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0447.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0447.dart @@ -18,7 +18,6 @@ abstract class StatelessFileSharingSource { /// Implementation for url-data source elements. class StatelessFileSharingUrlSource extends StatelessFileSharingSource { - StatelessFileSharingUrlSource(this.url); factory StatelessFileSharingUrlSource.fromXml(XMLNode element) { @@ -41,8 +40,29 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource { } } -class StatelessFileSharingData { +/// Finds the element in [node] and returns the list of +/// StatelessFileSharingSources contained with it. +/// If [checkXmlns] is true, then the sources element must also have an xmlns attribute +/// of "urn:xmpp:sfs:0". +List processStatelessFileSharingSources(XMLNode node, { bool checkXmlns = true }) { + final sources = List.empty(growable: true); + + final sourcesElement = node.firstTag( + 'sources', + xmlns: checkXmlns ? sfsXmlns : null, + )!; + for (final source in sourcesElement.children) { + if (source.attributes['xmlns'] == urlDataXmlns) { + sources.add(StatelessFileSharingUrlSource.fromXml(source)); + } else if (source.attributes['xmlns'] == sfsEncryptionXmlns) { + sources.add(StatelessFileSharingEncryptedSource.fromXml(source)); + } + } + return sources; +} + +class StatelessFileSharingData { const StatelessFileSharingData(this.metadata, this.sources); /// Parse [node] as a StatelessFileSharingData element. @@ -50,20 +70,10 @@ class StatelessFileSharingData { assert(node.attributes['xmlns'] == sfsXmlns, 'Invalid element xmlns'); assert(node.tag == 'file-sharing', 'Invalid element name'); - final sources = List.empty(growable: true); - - final sourcesElement = node.firstTag('sources')!; - for (final source in sourcesElement.children) { - if (source.attributes['xmlns'] == urlDataXmlns) { - sources.add(StatelessFileSharingUrlSource.fromXml(source)); - } else if (source.attributes['xmlns'] == sfsEncryptionXmlns) { - sources.add(StatelessFileSharingEncryptedSource.fromXml(source)); - } - } - return StatelessFileSharingData( FileMetadataData.fromXML(node.firstTag('file')!), - sources, + // TODO(PapaTutuWawa): This is a work around for Stickers where the source element has a XMLNS but SFS does not have one. + processStatelessFileSharingSources(node, checkXmlns: false), ); } @@ -120,7 +130,7 @@ class SFSManager extends XmppManagerBase { final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; return state.copyWith( - sfs: StatelessFileSharingData.fromXML(sfs), + sfs: StatelessFileSharingData.fromXML(sfs, ), ); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0449.dart b/packages/moxxmpp/lib/src/xeps/xep_0449.dart new file mode 100644 index 0000000..f435d93 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0449.dart @@ -0,0 +1,308 @@ +import 'dart:convert'; +import 'package:moxxmpp/src/jid.dart'; +import 'package:moxxmpp/src/managers/base.dart'; +import 'package:moxxmpp/src/managers/data.dart'; +import 'package:moxxmpp/src/managers/handlers.dart'; +import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; +import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; +import 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; +import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; +import 'package:moxxmpp/src/xeps/xep_0300.dart'; +import 'package:moxxmpp/src/xeps/xep_0446.dart'; +import 'package:moxxmpp/src/xeps/xep_0447.dart'; + +class Sticker { + const Sticker(this.metadata, this.sources, this.suggests); + + factory Sticker.fromXML(XMLNode node) { + assert(node.tag == 'item', 'sticker has wrong tag'); + + return Sticker( + FileMetadataData.fromXML(node.firstTag('file', xmlns: fileMetadataXmlns)!), + processStatelessFileSharingSources(node, checkXmlns: false), + {}, + ); + } + + final FileMetadataData metadata; + final List sources; + // Language -> suggestion + final Map suggests; + + XMLNode toPubSubXML() { + final suggestsElements = suggests.keys.map((suggest) { + Map attrs; + if (suggest.isEmpty) { + attrs = {}; + } else { + attrs = { + 'xml:lang': suggest, + }; + } + + return XMLNode( + tag: 'suggest', + attributes: attrs, + text: suggests[suggest], + ); + }); + + return XMLNode( + tag: 'item', + children: [ + metadata.toXML(), + ...sources.map((source) => source.toXml()), + ...suggestsElements, + ], + ); + } +} + +class StickerPack { + const StickerPack( + this.id, + this.name, + this.summary, + this.hashAlgorithm, + this.hashValue, + this.stickers, + this.restricted, + ); + + factory StickerPack.fromXML(String id, XMLNode node, { bool hashAvailable = true }) { + assert(node.tag == 'pack', 'node has wrong tag'); + assert(node.attributes['xmlns'] == stickersXmlns, 'node has wrong XMLNS'); + + var hashAlgorithm = HashFunction.sha256; + var hashValue = ''; + if (hashAvailable) { + final hash = node.firstTag('hash', xmlns: hashXmlns)!; + hashAlgorithm = hashFunctionFromName(hash.attributes['algo']! as String); + hashValue = hash.innerText(); + } + + return StickerPack( + id, + node.firstTag('name')!.innerText(), + node.firstTag('summary')!.innerText(), + hashAlgorithm, + hashValue, + node.children + .where((e) => e.tag == 'item') + .map(Sticker.fromXML) + .toList(), + node.firstTag('restricted') != null, + ); + } + + final String id; + // TODO(PapaTutuWawa): Turn name and summary into a Map as it may contain a xml:lang + final String name; + final String summary; + final HashFunction hashAlgorithm; + final String hashValue; + final List stickers; + final bool restricted; + + /// When using the fromXML factory to parse a description of a sticker pack with a + /// yet unknown hash, then this function can be used in order to apply the freshly + /// calculated hash to the object. + StickerPack copyWithId(HashFunction newHashFunction, String newId) { + return StickerPack( + newId, + name, + summary, + newHashFunction, + newId, + stickers, + restricted, + ); + } + + XMLNode toXML() { + return XMLNode.xmlns( + tag: 'pack', + xmlns: stickersXmlns, + children: [ + // Pack metadata + XMLNode( + tag: 'name', + text: name, + ), + XMLNode( + tag: 'summary', + text: summary, + ), + constructHashElement( + hashAlgorithm.toName(), + hashValue, + ), + + ...restricted ? + [XMLNode(tag: 'restricted')] : + [], + + // Stickers + ...stickers + .map((sticker) => sticker.toPubSubXML()), + ], + ); + } + + /// Calculates the sticker pack's hash as specified by XEP-0449. + Future getHash(HashFunction hashFunction) async { + // Build the meta string + final metaTmp = [ + [ + ...utf8.encode('name'), + 0x1f, + 0x1f, + ...utf8.encode(name), + 0x1f, + 0x1e, + ], + [ + ...utf8.encode('summary'), + 0x1f, + 0x1f, + ...utf8.encode(summary), + 0x1f, + 0x1e, + ], + ]..sort(ioctetSortComparatorRaw); + final metaString = List.empty(growable: true); + for (final m in metaTmp) { + metaString.addAll(m); + } + metaString.add(0x1c); + + // Build item hashes + final items = List>.empty(growable: true); + for (final sticker in stickers) { + final tmp = List.empty(growable: true) + ..addAll(utf8.encode(sticker.metadata.desc!)) + ..add(0x1e); + + final hashes = List>.empty(growable: true); + for (final hash in sticker.metadata.hashes.keys) { + hashes.add([ + ...utf8.encode(hash), + 0x1f, + ...utf8.encode(sticker.metadata.hashes[hash]!), + 0x1f, + 0x1e, + ]); + } + hashes.sort(ioctetSortComparatorRaw); + + for (final hash in hashes) { + tmp.addAll(hash); + } + tmp.add(0x1d); + items.add(tmp); + } + items.sort(ioctetSortComparatorRaw); + final stickersString = List.empty(growable: true); + for (final item in items) { + stickersString.addAll(item); + } + stickersString.add(0x1c); + + // Calculate the hash + final rawHash = await CryptographicHashManager.hashFromData( + [ + ...metaString, + ...stickersString, + ], + hashFunction, + ); + return base64.encode(rawHash).substring(0, 24); + } +} + +class StickersManager extends XmppManagerBase { + @override + String getId() => stickersManager; + + @override + String getName() => 'StickersManager'; + + @override + Future isSupported() async => true; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'message', + tagXmlns: stickersXmlns, + tagName: 'sticker', + callback: _onIncomingMessage, + priority: -99, + ), + ]; + + Future _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { + final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; + return state.copyWith( + stickerPackId: sticker.attributes['pack']! as String, + ); + } + + /// Publishes the StickerPack [pack] to the PubSub node of [jid]. + /// + /// On success, returns true. On failure, returns a PubSubError. + Future> publishStickerPack(JID jid, StickerPack pack) async { + assert(pack.id != '', 'The sticker pack must have an id'); + final pm = getAttributes().getManagerById(pubsubManager)!; + + return pm.publish( + jid.toBare().toString(), + stickersXmlns, + pack.toXML(), + id: pack.id, + options: const PubSubPublishOptions( + maxItems: 'max', + ), + ); + } + + /// Removes the sticker pack with id [id] from the PubSub node of [jid]. + /// + /// On success, returns the true. On failure, returns a PubSubError. + Future> retractStickerPack(JID jid, String id) async { + final pm = getAttributes().getManagerById(pubsubManager)!; + + return pm.retract( + jid, + stickersXmlns, + id, + ); + } + + /// Fetches the sticker pack with id [id] from [jid]. + /// + /// On success, returns the StickerPack. On failure, returns a PubSubError. + Future> fetchStickerPack(JID jid, String id) async { + final pm = getAttributes().getManagerById(pubsubManager)!; + final stickerPackDataRaw = await pm.getItem( + jid.toBare().toString(), + stickersXmlns, + id, + ); + if (stickerPackDataRaw.isType()) { + return Result(stickerPackDataRaw.get()); + } + + final stickerPackData = stickerPackDataRaw.get(); + final stickerPack = StickerPack.fromXML( + stickerPackData.id, + stickerPackData.payload, + ); + + return Result(stickerPack); + } +}