From d53526a691acaf8b443b9daf534b8193c799e89b Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:45:58 +0400 Subject: [PATCH 01/81] chore(xml): create a private file that holds all the required methods for XML manipulation as an extension --- xml/_extensions.dart | 318 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 xml/_extensions.dart diff --git a/xml/_extensions.dart b/xml/_extensions.dart new file mode 100644 index 0000000..5c64a02 --- /dev/null +++ b/xml/_extensions.dart @@ -0,0 +1,318 @@ +part of 'base.dart'; + +extension Attribute on BaseElement { + String getAttribute( + String attribute, [ + String absence = '', + String? namespace, + ]) { + if (attribute == 'lang' || attribute == 'language') { + return _xml.getAttribute(_xmlLanguage, namespace: namespace) ?? absence; + } else { + return _xml.getAttribute(attribute, namespace: namespace) ?? absence; + } + } + + void setAttribute(String attribute, [String? value, String? namespace]) { + if (value == null) return deleteAttribute(attribute, namespace); + if (attribute == 'lang' || attribute == 'language') { + _xml.setAttribute(_xmlLanguage, value, namespace: namespace); + } else { + _xml.setAttribute(attribute, value, namespace: namespace); + } + } + + void deleteAttribute(String attribute, [String? namespace]) => + _xml.removeAttribute(attribute, namespace: namespace); +} + +extension SubTextGetters on BaseElement { + String getSubText(String name, {String absence = '', String? language}) { + assert( + language != '*', + 'Instead call `getAllSubtext` to get all language texts', + ); + + final stanzas = _xml.findAllElements(name); + if (stanzas.isEmpty) return absence; + + String? result; + for (final stanza in stanzas) { + if (_lang(stanza) == language) { + if (stanza.innerText.isEmpty) return absence; + + result = stanza.innerText; + break; + } + if (stanza.innerText.isNotEmpty) { + result = stanza.innerText; + } + } + + return result ?? absence; + } + + Map getAllSubText( + String name, { + String absence = '', + String? language, + }) { + final casted = _fixedNamespace(name).join('/'); + + final results = {}; + final stanzas = _xml.findAllElements(casted); + if (stanzas.isNotEmpty) { + for (final stanza in stanzas) { + final stanzaLanguage = _lang(stanza) ?? defaultLanguage; + + if (language == stanzaLanguage || language == '*') { + late String text; + if (stanza.innerText.isEmpty) { + text = absence; + } else { + text = stanza.innerText; + } + + results[stanzaLanguage] = text; + } + } + } + + return results; + } +} + +extension SubTextSetters on BaseElement { + XmlNode? setSubText( + String name, { + String text = '', + String? language, + bool keep = false, + }) { + /// Set language to an empty string beforehand, 'cause it will lead to + /// unexpected behaviour like adding an empty language key to the stanza. + language = language ?? ''; + final lang = language.isNotEmpty ? language : defaultLanguage; + + if (text.isEmpty && !keep) { + removeSubElement(name, language: lang); + return null; + } + + final path = _fixedNamespace(name); + final casted = path.last; + + XmlNode? parent; + final elements = []; + + List missingPath = []; + final searchOrder = List.from(path)..removeLast(); + + while (searchOrder.isNotEmpty) { + parent = _xml.xpath('/${searchOrder.join('/')}').firstOrNull; + + final searched = searchOrder.removeLast(); + if (parent != null) break; + missingPath.add(searched); + } + + missingPath = missingPath.reversed.toList(); + + if (parent != null) { + elements.addAll(_xml.xpath('/${path.join('/')}')); + } else { + parent = _xml; + elements.clear(); + } + + for (final missing in missingPath) { + final temporary = WhixpUtils.xmlElement(missing); + parent?.children.add(temporary); + parent = temporary; + } + + for (final element in elements) { + final language = _lang(element) ?? defaultLanguage; + if ((lang == null && language == defaultLanguage) || lang == language) { + _xml.innerText = text; + return _xml; + } + } + + final temporary = WhixpUtils.xmlElement(casted); + temporary.innerText = text; + + if (lang != null && lang != defaultLanguage) { + temporary.setAttribute(_xmlLanguage, lang); + } + + parent?.children.add(temporary); + return temporary; + } + + void setAllSubText( + String name, { + String? language, + Map values = const {}, + }) { + assert(values.isNotEmpty, 'Subtext values to be set can not be empty'); + removeSubElement(name, language: language); + for (final entry in values.entries) { + if (language == null || language == '*' || entry.key == language) { + setSubText(name, text: entry.value, language: entry.key); + } + } + } +} + +extension SubTextRemovers on BaseElement { + void removeSubText(String name, {bool all = false, String? language}) => + removeSubElement(name, all: all, language: language, onlyContent: true); +} + +extension SubElementRemovers on BaseElement { + void removeSubElement( + String name, { + bool all = false, + bool onlyContent = false, + String? language, + }) { + final path = _fixedNamespace(name); + final target = path.last; + + final lang = language ?? defaultLanguage; + + Iterable enumerate(List iterable) sync* { + for (int i = 0; i < iterable.length; i++) { + yield i; + } + } + + XmlNode parent = _xml; + for (final level in enumerate(path)) { + final elementPath = path.sublist(0, path.length - level).join('/'); + final parentPath = + (level > 0) ? path.sublist(0, path.length - level - 1).join('/') : ''; + + final elements = _xml.xpath('/$elementPath'); + if (parentPath.isNotEmpty) { + parent = _xml.xpath('/$parentPath').firstOrNull ?? _xml; + } + + for (final element in elements.toList()) { + if (element is XmlElement) { + if (element.name.qualified == target || element.children.isEmpty) { + final elementLanguage = _lang(element); + if (lang == '*' || elementLanguage == lang) { + final result = parent.children.remove(element); + if (onlyContent && result) { + parent.children.add(element..innerText = ''); + } + } + } + } + } + + if (!all) return; + } + } +} + +extension XMLManipulator on BaseElement { + XmlElement get xml => _xml; + + Iterable get childElements => _xml.childElements; + + XmlElement? getElement(String name) => _xml.getElement(name); +} + +extension Registrator on BaseElement { + void register(String name, BaseElement element) => + _ElementPluginRegistrator().register(name, element); + + void unregister(String name) => _ElementPluginRegistrator().unregister(name); +} + +extension Interfaces on BaseElement { + void setInterface(String name, String value, {String? language}) { + final lang = language ?? defaultLanguage; + assert(lang != '*', 'Use `setInterfaces` method instead'); + + setSubText(name, text: value, language: lang); + } + + void setInterfaces( + String name, + Map values, { + String? language, + }) { + final lang = language ?? defaultLanguage; + + return setAllSubText(name, values: values, language: lang); + } + + List getInterfaces(String name, {String? language}) { + final lang = language ?? defaultLanguage; + + final elements = _xml.findAllElements(name); + final interfaces = []; + if (elements.isEmpty) return interfaces; + + for (final element in elements) { + if (_lang(element) == lang || lang == '*') { + interfaces.add(element); + } + } + + return interfaces; + } + + void removeInterface(String name, {String? language}) { + final lang = language ?? defaultLanguage; + + if (_plugins.contains(name)) { + final plugin = get(name, language: lang); + if (plugin == null) return; + + _registeredPlugins.remove(name); + _xml.children.remove(plugin._xml); + } else { + removeSubElement(name, language: lang); + } + } + + XmlNode? setEmptyInterface(String name, bool value, {String? language}) { + if (value) { + return setSubText(name, keep: true, language: language); + } else { + return setSubText(name, language: language); + } + } + + bool containsInterface(String name) => _xml.getElement(name) != null; +} + +extension Language on BaseElement { + String? _lang([XmlNode? xml]) { + if (xml != null) { + return xml + .copy() + .xpath('//*[@$_xmlLanguage]') + .firstOrNull + ?.getAttribute(_xmlLanguage); + } + + /// Get default language by the lookup to the root element of the root + /// element. + return _xml.getAttribute(_xmlLanguage); + } +} + +extension Tag on BaseElement { + String get tag { + if (_namespace != null) { + return '$_namespace$_name'; + } + return _name; + } +} From 7d312dee18dad59a0052455dc4de4b0b82a64d94 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:46:31 +0400 Subject: [PATCH 02/81] chore(xml): create a model that holds required properties for manipulation --- xml/_model.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 xml/_model.dart diff --git a/xml/_model.dart b/xml/_model.dart new file mode 100644 index 0000000..21d588a --- /dev/null +++ b/xml/_model.dart @@ -0,0 +1,28 @@ +part of 'base.dart'; + +class ElementModel { + const ElementModel(this.name, this.namespace, {this.xml, this.parent}); + + final String name; + final String? namespace; + final XmlElement? xml; + final BaseElement? parent; + + String _getter(String? data) => data ?? 'NOT included'; + + @override + String toString() => + '''Element Model: namespace => ${_getter(namespace)}, parent => ${parent?._name}'''; + + @override + bool operator ==(Object element) => + element is ElementModel && + element.name == name && + element.namespace == namespace && + element.xml == xml && + element.parent == parent; + + @override + int get hashCode => + name.hashCode ^ namespace.hashCode ^ xml.hashCode ^ parent.hashCode; +} From 4a83bfe37b5df2b54cb995187406fb5c9e13b0a2 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:47:18 +0400 Subject: [PATCH 03/81] chore(xml): create registrator to register required stanza types --- xml/_registry.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 xml/_registry.dart diff --git a/xml/_registry.dart b/xml/_registry.dart new file mode 100644 index 0000000..c6f4e98 --- /dev/null +++ b/xml/_registry.dart @@ -0,0 +1,13 @@ +part of 'stanza.dart'; + +final _createRegistry = { + IQ: (xml) => IQ(xml: xml), + Query: (xml) => Query(xml: xml), +}; + +final _parseRegistry = { + IQ: (model) => + IQ(namespace: model.namespace, xml: model.xml, parent: model.parent), + Query: (model) => + Query(namespace: model.namespace, xml: model.xml, parent: model.parent), +}; From 4a8ec338791edab33b8df27186b06bbe0e050ee0 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:47:38 +0400 Subject: [PATCH 04/81] chore(xml): create element registrator --- xml/_registrator.dart | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 xml/_registrator.dart diff --git a/xml/_registrator.dart b/xml/_registrator.dart new file mode 100644 index 0000000..a84b075 --- /dev/null +++ b/xml/_registrator.dart @@ -0,0 +1,31 @@ +part of 'base.dart'; + +class _ElementPluginRegistrator { + factory _ElementPluginRegistrator() => _instance; + + _ElementPluginRegistrator._(); + + static final _ElementPluginRegistrator _instance = + _ElementPluginRegistrator._(); + + final _plugins = {}; + + BaseElement get(String name) { + assert(_plugins.containsKey(name), '$name plugin is not registered'); + return _plugins[name]!; + } + + void register(String name, BaseElement element) { + if (_plugins.containsKey(name)) return; + + _plugins[name] = element; + } + + void unregister(String name) { + if (!_plugins.containsKey(name)) return; + + _plugins.remove(name); + } + + void clear() => _plugins.clear(); +} From 94735074221250eee7557cc7178eb8f21b1953b6 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:48:07 +0400 Subject: [PATCH 05/81] refactor(xml): move global variables as a static to the new file --- xml/_static.dart | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 xml/_static.dart diff --git a/xml/_static.dart b/xml/_static.dart new file mode 100644 index 0000000..f7a19ec --- /dev/null +++ b/xml/_static.dart @@ -0,0 +1,3 @@ +part of 'base.dart'; + +const String _xmlLanguage = 'xml:lang'; From 29a2b2342120cab74e1a13e2f438533a12f13afa Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:48:47 +0400 Subject: [PATCH 06/81] refactor(xml): change contents of the base class for `XML` --- xml/base.dart | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 xml/base.dart diff --git a/xml/base.dart b/xml/base.dart new file mode 100644 index 0000000..bdb0e1c --- /dev/null +++ b/xml/base.dart @@ -0,0 +1,206 @@ +import 'package:dartz/dartz.dart'; + +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart'; +import 'package:xml/xpath.dart'; + +import 'stanza.dart'; + +part '_extensions.dart'; +part '_model.dart'; +part '_registrator.dart'; +part '_static.dart'; + +List _fixNamespace( + String xPath, { + String? absenceNamespace, + bool propogateNamespace = true, +}) { + final fixed = []; + + final namespaceBlocks = xPath.split('{'); + for (final block in namespaceBlocks) { + late String? namespace; + late List elements; + if (block.contains('}')) { + final namespaceBlockSplit = block.split('}'); + namespace = namespaceBlockSplit[0]; + elements = namespaceBlockSplit[1].split('/'); + } else { + namespace = absenceNamespace; + elements = block.split('/'); + } + + for (final element in elements) { + late String tag; + if (element.isNotEmpty) { + if (propogateNamespace && element[0] != '*') { + if (namespace != null) { + tag = '<$element xmlns="$namespace"/>'; + } else { + tag = '<$element/>'; + } + } else { + tag = element; + } + fixed.add(tag); + } + } + } + + return fixed; +} + +abstract class BaseElement implements BaseElementFactory { + BaseElement( + String name, { + String? namespace, + Set plugins = const {}, + XmlElement? xml, + BaseElement? parent, + }) { + _name = name; + _namespace = namespace; + _parent = parent; + _plugins = plugins; + + /// Whenever the element is initialized, create an empty list of plugins. + _registeredPlugins = >{}; + + final parse = !_setup(xml); + + if (parse) { + final childElements = _xml.childElements.toList().reversed.toList(); + final elements = >[]; + + if (childElements.isEmpty) return; + + for (final element in childElements) { + final name = element.localName; + + if (_plugins.contains(name)) { + final plugin = _ElementPluginRegistrator().get(name); + elements.add(Tuple2(element, plugin)); + } + } + + for (final element in elements) { + _initPlugin(element.value2._name, element.value1, element.value2); + } + } + } + + late final String _name; + late final String? _namespace; + late final XmlElement _xml; + late final BaseElement? _parent; + late final Set _plugins; + late final Map> _registeredPlugins; + + bool _setup([XmlElement? xml]) { + _ElementPluginRegistrator().register(_name, this); + if (xml != null) { + _xml = xml; + return false; + } + + final parts = _name.split('/'); + + XmlElement? lastxml; + for (final splitted in parts) { + final newxml = lastxml == null + ? WhixpUtils.xmlElement(splitted, namespace: _namespace) + : WhixpUtils.xmlElement(splitted); + + if (lastxml == null) { + lastxml = newxml; + } else { + lastxml.children.add(newxml); + } + } + + _xml = lastxml ?? XmlElement(XmlName('')); + + if (_parent != null) { + _parent._xml.children.add(_xml); + } + + return true; + } + + BaseElement _initPlugin( + String pluginName, + XmlElement existingXml, + BaseElement currentElement, { + String? language, + }) { + final name = existingXml.localName; + final namespace = existingXml.getAttribute('xmlns'); + final plugin = BaseElementFactory.parse( + currentElement, + ElementModel(name, namespace, xml: existingXml, parent: this), + ); + final lang = language ?? defaultLanguage; + + _registeredPlugins[pluginName] = Tuple2(lang, plugin); + + return plugin; + } + + List _fixedNamespace(String xPath) => _fixNamespace( + xPath, + absenceNamespace: _namespace, + propogateNamespace: false, + ); + + E? get( + String name, { + String? language, + bool fallbackInitialize = false, + }) { + final lang = language ?? defaultLanguage; + + BaseElement? plugin; + if (_registeredPlugins.containsKey(name)) { + final existing = _registeredPlugins[name]; + if (existing != null && existing.value1 == lang) { + plugin = existing.value2; + } + } else if (fallbackInitialize) { + final element = _xml.getElement(name); + if (element != null) { + plugin = _initPlugin(name, element, this, language: lang); + } + } + + try { + final casted = plugin as E?; + return casted; + } on Exception { + throw WhixpInternalException( + 'Type ${plugin.runtimeType} can not be casted to the ${E.runtimeType} type', + ); + } + } + + set defaultLanguage(String? language) => setAttribute(_xmlLanguage, language); + + String? get defaultLanguage => _lang(); + + @override + String toString() => WhixpUtils.serialize(_xml) ?? ''; + + @override + bool operator ==(Object element) => + element is BaseElement && + element._name == _name && + element._namespace == _namespace && + element._xml == _xml && + element._parent == _parent; + + @override + int get hashCode => + _name.hashCode ^ _namespace.hashCode ^ _xml.hashCode ^ _parent.hashCode; +} From cc5516bed0157df789b7af35b82ad1dc35dda237 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 18 May 2024 23:49:14 +0400 Subject: [PATCH 07/81] feat(xml): add base object for specific stanza types --- xml/stanza.dart | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 xml/stanza.dart diff --git a/xml/stanza.dart b/xml/stanza.dart new file mode 100644 index 0000000..dae09fb --- /dev/null +++ b/xml/stanza.dart @@ -0,0 +1,66 @@ +import 'package:whixp/src/jid/jid.dart'; + +import 'package:xml/xml.dart'; + +import 'base.dart'; +import 'iq.dart'; +import 'query.dart'; + +part '_registry.dart'; + +enum StanzaType { get, set, result, error, none } + +mixin class BaseElementFactory { + const BaseElementFactory(); + + static Stanza create(XmlElement xml) => + _createRegistry[E]!(xml); + static BaseElement parse(BaseElement element, ElementModel model) => + _parseRegistry[element.runtimeType]!(model); +} + +abstract class Stanza extends BaseElement implements BaseElementFactory { + Stanza(super.name, {super.namespace, super.plugins, super.xml, super.parent}); + + JabberID? get to { + final attribute = getAttribute('to'); + if (attribute.isEmpty) return null; + + return JabberID(attribute); + } + + set to(JabberID? jid) => setAttribute('to', jid?.toString()); + + JabberID? get from { + final attribute = getAttribute('from'); + if (attribute.isEmpty) return null; + + return JabberID(attribute); + } + + set from(JabberID? jid) => setAttribute('from', jid?.toString()); + + StanzaType get type { + final attribute = getAttribute('type'); + switch (attribute) { + case 'get': + return StanzaType.get; + case 'set': + return StanzaType.set; + case 'result': + return StanzaType.result; + case 'error': + return StanzaType.error; + default: + return StanzaType.none; + } + } + + set type(StanzaType type) => setAttribute('type', type.name); +} + +Stanza create(XmlElement xml) => + BaseElementFactory.create(xml); + +BaseElement parse(BaseElement element, ElementModel model) => + BaseElementFactory.parse(element, model); From f3f19625ee191512de053fcd4b3aedd6cfdba34f Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:54:25 +0400 Subject: [PATCH 08/81] chore: Update whixp package version to 2.1.0 and improve description --- pubspec.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index f63474e..ff3f65a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: whixp -description: Streamline Your XMPP Messaging with Lightweight Dart-Based XMPP Client -version: 2.0.1 +description: Streamline your XMPP messaging with a lightweight Dart-based XMPP client +version: 2.1.0 repository: https://github.com/vsevex/whixp issue_tracker: https://github.com/vsevex/whixp/issues topics: [xmpp, im, network, socket] @@ -11,16 +11,12 @@ environment: dependencies: connecta: ^1.2.0 crypto: ^3.0.3 - dartz: ^0.10.1 dnsolve: ^1.0.0 hive: ^2.2.3 - logger: ^2.0.2+1 memoize: ^3.0.0 - meta: ^1.11.0 synchronized: ^3.1.0+1 unorm_dart: ^0.3.0 xml: ^6.5.0 - xpath_selector_xml_parser: ^3.0.1 dev_dependencies: convert: ^3.1.1 From ff6db54888b168562b51fc46c4b236334558f75b Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:55:07 +0400 Subject: [PATCH 09/81] chore: Remove unused example code files --- example/command.dart | 72 ----------------------------------- example/command_user.dart | 39 ------------------- example/component.dart | 13 ------- example/messaging.dart | 60 ----------------------------- example/messaging_second.dart | 65 ------------------------------- example/pubsub.dart | 39 ------------------- example/register.dart | 25 ------------ example/sm.dart | 15 -------- example/vcard.dart | 21 ---------- 9 files changed, 349 deletions(-) delete mode 100644 example/command.dart delete mode 100644 example/command_user.dart delete mode 100644 example/component.dart delete mode 100644 example/messaging.dart delete mode 100644 example/messaging_second.dart delete mode 100644 example/pubsub.dart delete mode 100644 example/register.dart delete mode 100644 example/sm.dart delete mode 100644 example/vcard.dart diff --git a/example/command.dart b/example/command.dart deleted file mode 100644 index c4e5069..0000000 --- a/example/command.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:developer'; - -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = WhixpComponent( - 'push.example.com', - secret: 'pushnotifications', - host: 'example.com', - port: 5275, - logger: Log(enableError: true, enableWarning: true), - provideHivePath: true, - ); - - final adhoc = AdHocCommands(); - final forms = DataForms(); - final ping = Ping(interval: 60, keepalive: true); - - whixp - ..registerPlugin(ServiceDiscovery()) - ..registerPlugin(adhoc) - ..registerPlugin(forms) - ..registerPlugin(ping); - whixp.connect(); - - Map? handleBroadcastComplete( - Form payload, - Map? session, - ) { - final form = payload; - - final broadcast = form.getValues()['broadcast']; - log(broadcast.toString()); - - if (session != null) { - session['payload'] = null; - session['next'] = null; - } - - return session; - } - - Map? handleBroadcast( - IQ iq, - Map? session, [ - _, - ]) { - final form = forms.createForm(title: 'Broadcast Form'); - form['instructions'] = 'Send a broadcast request to the JID'; - form.addField( - variable: 'broadcast', - formType: 'text-single', - label: 'you want to hangout?', - ); - - if (session != null) { - session['payload'] = form; - session['next'] = handleBroadcastComplete(form, session); - session['hasNext'] = false; - } - - return session; - } - - whixp.addEventHandler('sessionStart', (_) { - adhoc.addCommand( - node: 'broadcasting', - name: 'Broadcast', - handler: handleBroadcast, - ); - }); -} diff --git a/example/command_user.dart b/example/command_user.dart deleted file mode 100644 index 8ff837e..0000000 --- a/example/command_user.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = Whixp( - 'alyosha@example.com/mobile', - 'passwd', - host: 'example.com', - logger: Log(enableError: true, enableWarning: true), - hivePathName: 'whixpsecondary', - provideHivePath: true, - ); - - final adhoc = AdHocCommands(); - final forms = DataForms(); - whixp.registerPlugin(ServiceDiscovery()); - whixp.registerPlugin(adhoc); - whixp.registerPlugin(forms); - whixp.connect(); - - whixp.addEventHandler('streamNegotiated', (_) { - adhoc.startCommand( - JabberID('push.example.com'), - 'broadcasting', - { - 'next': (IQ iq, Map? session) { - final form = forms.createForm(formType: 'submit'); - if (session != null) { - form.addField(variable: 'broadcast', value: session['broadcast']); - session['payload'] = form; - session['next'] = null; - adhoc.continueCommand(session); - } - - return session; - }, - }, - ); - }); -} diff --git a/example/component.dart b/example/component.dart deleted file mode 100644 index 5082dc5..0000000 --- a/example/component.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:whixp/whixp.dart'; - -void main() { - final component = WhixpComponent( - 'push.example.com', - secret: 'pushnotifications', - host: 'example.com', - port: 5275, - logger: Log(enableError: true, enableWarning: true), - provideHivePath: true, - ); - component.connect(); -} diff --git a/example/messaging.dart b/example/messaging.dart deleted file mode 100644 index c0d600a..0000000 --- a/example/messaging.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:developer'; - -import 'package:whixp/whixp.dart'; - -void main() { - const jid = 'vsevex@example.com'; - const password = 'securepassword'; - const name = 'Vsevolod'; - - final whixp = Whixp( - jid, - password, - host: 'example.com', - whitespaceKeepAlive: false, - logger: Log(enableError: true, enableWarning: true), - - /// change whatever folder name you want, do not use in Flutter - hivePathName: 'whixpFirst', - - /// just use in Dart projects - provideHivePath: true, - ); - - final ping = Ping(interval: 60, keepalive: true); - final register = InBandRegistration(); - - whixp - ..clientRoster.autoAuthorize = false - ..clientRoster.autoSubscribe = false - ..registerPlugin(register) - ..registerPlugin(ping) - ..addEventHandler('streamNegotiated', (_) { - whixp.sendPresence(); - if (!whixp.clientRoster.hasJID('alyosha@example.com')) { - (whixp.clientRoster['alyosha@example.com'] as RosterItem).subscribe(); - } - }) - ..addEventHandler('rosterSubscriptionRequest', (request) { - if (request!.from == JabberID('alyosha@example.com')) { - (whixp.clientRoster['alyosha@example.com'] as RosterItem).authorize(); - } - }) - ..addEventHandler('message', (message) { - if (message != null) { - log('Message from: ${message.from!.bare}'); - } - }) - ..addEventHandler
('register', (_) { - final response = whixp.makeIQSet(); - final register = response['register'] as Register; - - register['username'] = JabberID(jid).user; - register['password'] = password; - register['name'] = name; - - response.sendIQ(callback: (iq) => log('Registered!!!')); - }); - - whixp.connect(); -} diff --git a/example/messaging_second.dart b/example/messaging_second.dart deleted file mode 100644 index 9897b95..0000000 --- a/example/messaging_second.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:developer'; - -import 'package:whixp/whixp.dart'; - -void main() { - const jid = 'alyosha@example.com'; - const password = 'othersecurepassword'; - const name = 'Alyosha'; - - final whixp = Whixp( - jid, - password, - host: 'example.com', - whitespaceKeepAlive: false, - logger: Log(enableError: true, enableWarning: true), - - /// change whatever folder name you want, do not use in Flutter - hivePathName: 'whixpSecond', - - /// just use in Dart projects - provideHivePath: true, - ); - - final ping = Ping(interval: 60, keepalive: true); - final register = InBandRegistration(); - - whixp - ..clientRoster.autoAuthorize = false - ..clientRoster.autoSubscribe = false - ..registerPlugin(register) - ..registerPlugin(ping) - ..addEventHandler('streamNegotiated', (_) => whixp.sendPresence()) - ..addEventHandler('rosterSubscriptionRequest', (request) { - if (request!.from == JabberID('vsevex@example.com')) { - (whixp.clientRoster['vsevex@example.com'] as RosterItem).authorize(); - } - }) - ..addEventHandler('presenceAvailable', (data) async { - if (data!.from!.bare == 'vsevex@example.com') { - await Future.delayed(const Duration(seconds: 5), () { - whixp.sendMessage( - JabberID('vsevex@example.com'), - messageBody: 'Hello from $name!!!', - ); - }); - } - }) - ..addEventHandler('register', (_) { - final response = whixp.makeIQSet(); - final register = response['register'] as Register; - - register['username'] = JabberID(jid).user; - register['password'] = password; - register['name'] = name; - - response.sendIQ( - callback: (iq) { - (whixp.clientRoster['vsevex@example.com'] as RosterItem).subscribe(); - log('Registered!!!'); - }, - ); - }); - - whixp.connect(); -} diff --git a/example/pubsub.dart b/example/pubsub.dart deleted file mode 100644 index 67a6f46..0000000 --- a/example/pubsub.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:developer'; - -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = Whixp( - 'vsevex@example.com/mobile', - 'passwd', - host: 'example.com', - logger: Log(enableError: true, enableWarning: true), - ); - - final disco = ServiceDiscovery(); - final pubsub = PubSub(); - final rsm = RSM(); - - whixp - ..registerPlugin(disco) - ..registerPlugin(rsm) - ..registerPlugin(pubsub); - - whixp.connect(); - whixp.addEventHandler('sessionStart', (_) async { - whixp.getRoster(); - whixp.sendPresence(); - - final payload = AtomEntry(); - payload['title'] = "Ink & Echoes: A Writer's Prelude"; - payload['summary'] = - 'oin the adventure where every word is a brushstroke, crafting a spellbinding tapestry that beckons readers into the magical realm of storytelling.'; - - await pubsub.publish( - JabberID('pubsub.example.com'), - 'senatus', - payload: payload, - callback: (iq) => log('$payload is published!'), - ); - }); -} diff --git a/example/register.dart b/example/register.dart deleted file mode 100644 index 65b9eaa..0000000 --- a/example/register.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = Whixp( - 'vsevex@example.com/desktop', - 'passwd', - host: 'example.com', - provideHivePath: true, - ); - - final inbandregistration = InBandRegistration(); - whixp.registerPlugin(inbandregistration); - - whixp.addEventHandler('register', (data) { - final response = whixp.makeIQSet(); - final register = response['register'] as Register; - register['username'] = 'vsevex'; - register['password'] = 'passwd'; - register['name'] = 'Vsevolod'; - - response.sendIQ(); - }); - - whixp.connect(); -} diff --git a/example/sm.dart b/example/sm.dart deleted file mode 100644 index 06d89ed..0000000 --- a/example/sm.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = Whixp( - 'vsevex@example.com/desktop', - 'passwd', - host: 'example.com', - logger: Log(enableError: true, enableWarning: true), - provideHivePath: true, - ); - - final management = StreamManagement(smID: 'someID'); - whixp.connect(); - whixp.registerPlugin(management); -} diff --git a/example/vcard.dart b/example/vcard.dart deleted file mode 100644 index 35d17bb..0000000 --- a/example/vcard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:whixp/whixp.dart'; - -void main() { - final whixp = Whixp( - 'vsevex@example.com/desktop', - 'passwd', - host: 'example.com', - logger: Log(enableError: true, enableWarning: true), - provideHivePath: true, - ); - - final vCard = VCardTemp(); - whixp.registerPlugin(vCard); - whixp.connect(); - whixp.addEventHandler('streamNegotiated', (_) async { - final stanza = VCardTempStanza(); - stanza['FN'] = 'Vsevolod'; - stanza['NICKNAME'] = 'vsevex'; - vCard.publish(stanza); - }); -} From 951317fed836fe5e520fc87e9a2dfd8c79aa198c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:55:46 +0400 Subject: [PATCH 10/81] refactor(xml): remove unused `test` code files --- test/base_test.dart | 720 ----------------------------------------- test/command_test.dart | 100 ------ test/roster_test.dart | 93 ------ test/stanza_test.dart | 59 ---- test/time_test.dart | 62 ---- 5 files changed, 1034 deletions(-) delete mode 100644 test/base_test.dart delete mode 100644 test/command_test.dart delete mode 100644 test/roster_test.dart delete mode 100644 test/stanza_test.dart delete mode 100644 test/time_test.dart diff --git a/test/base_test.dart b/test/base_test.dart deleted file mode 100644 index 802895e..0000000 --- a/test/base_test.dart +++ /dev/null @@ -1,720 +0,0 @@ -import 'package:dartz/dartz.dart'; - -import 'package:test/test.dart'; - -import 'package:whixp/src/stream/base.dart'; - -import 'package:xml/xml.dart' as xml; - -import 'class/base.dart'; -import 'test_base.dart' as tester; - -void main() { - group('fix namespace test caases', () { - test('fixing namespaces in an XPath expression', () { - const namespace = 'http://jabber.org/protocol/disco#items'; - const result = '{$namespace}test/bar/{abc}baz'; - - expect( - fixNamespace(result), - equals( - const Tuple2( - '//', - null, - ), - ), - ); - }); - }); - group('xml base test method and property test cases', () { - test('extended name must return stanza correctly', () { - final stanza = createTestStanza(name: 'foo/bar/baz', namespace: 'test'); - - tester.check(stanza, ''); - }); - - test('must extract languages after assigning to the stanza', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: {'test'}, - subInterfaces: {'test'}, - languageInterfaces: {'test'}, - ); - - final data = { - 'en': 'hi', - 'az': 'salam', - 'ru': 'blyat', - }; - stanza['test|*'] = data; - - tester.check( - stanza, - 'hisalamblyat', - ); - - final getData = stanza['test|*']; - - expect(getData, equals(data)); - - stanza.delete('test|*'); - tester.check(stanza, ''); - }); - - test( - 'deleting interfaces with no default language set must complete successfully', - () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: {'test'}, - subInterfaces: {'test'}, - languageInterfaces: {'test'}, - ); - - stanza['test'] = 'salam'; - stanza['test|no'] = 'hert'; - stanza['test|en'] = 'hi'; - - tester.check( - stanza, - 'salamcarthi', - ); - - stanza.delete('test'); - tester.check( - stanza, - 'carthi', - ); - }, - ); - - test('interfaces must be deleted when a default language set', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: const {'test'}, - subInterfaces: const {'test'}, - languageInterfaces: const {'test'}, - ); - - stanza['lang'] = 'az'; - stanza['test'] = 'salam'; - stanza['test|no'] = 'cart'; - stanza['test|en'] = 'hi'; - - tester.check( - stanza, - 'salamcarthi', - ); - - stanza.delete('test'); - tester.check( - stanza, - 'carthi', - ); - - stanza.delete('test|no'); - tester.check( - stanza, - 'hi', - ); - }); - - test('must reset an interface when no default lang is used', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: const {'test'}, - subInterfaces: const {'test'}, - languageInterfaces: const {'test'}, - ); - - stanza['test'] = 'salam'; - stanza['test|en'] = 'hi'; - - tester.check( - stanza, - 'salamhi', - ); - - stanza['test'] = 'cart'; - stanza['test|en'] = 'blya'; - - tester.check( - stanza, - 'cartblya', - ); - - expect(stanza['test|en'], equals('blya')); - expect(stanza['test'], equals('cart')); - }); - - test( - 'resetting an interface when a default language is used must work properly', - () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: const {'test'}, - subInterfaces: const {'test'}, - languageInterfaces: const {'test'}, - ); - - stanza['lang'] = 'az'; - stanza['test'] = 'salam'; - stanza['test|en'] = 'hi'; - - tester.check( - stanza, - 'salamhi', - ); - - stanza['test|ru'] = 'blyat'; - tester.check( - stanza, - 'salamhiblyat', - ); - - expect(stanza['test|ru'], equals('blyat')); - }); - - test('specifying various languages', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: const {'test'}, - subInterfaces: const {'test'}, - languageInterfaces: const {'test'}, - ); - - stanza['lang'] = 'az'; - stanza['test'] = 'salam'; - stanza['test|en'] = 'hi'; - - tester.check( - stanza, - 'salamhi', - ); - - expect(stanza['test|az'], equals('salam')); - expect(stanza['test|en'], equals('hi')); - }); - - test( - 'must finish the retrieval of the contents of a sub element successfully', - () { - final stanza = createTestStanza( - name: 'blya', - namespace: 'test', - interfaces: const {'cart'}, - getters: { - const Symbol('cart'): (args, base) => - base.getSubText('/wrapper/cart', def: 'zort'), - }, - setters: { - const Symbol('cart'): (value, args, base) { - final wrapper = xml.XmlElement(xml.XmlName('wrapper')); - final cart = xml.XmlElement(xml.XmlName('cart')); - cart.innerText = value as String; - wrapper.children.add(cart); - base.element!.children.add(wrapper); - }, - }, - ); - - expect(stanza['cart'], equals('zort')); - stanza['cart'] = 'hehe'; - tester.check( - stanza, - 'hehe', - ); - expect(stanza['cart'], equals('hehe')); - }, - ); - - test('setting the contents of sub element must work properly', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: {'hehe', 'boo'}, - getters: { - const Symbol('hehe'): (args, base) => - base.getSubText('/wrapper/hehe'), - const Symbol('boo'): (args, base) => base.getSubText('/wrapper/boo'), - }, - setters: { - const Symbol('hehe'): (value, args, base) => - base.setSubText('/wrapper/hehe', text: value as String), - const Symbol('boo'): (value, args, base) => - base.setSubText('/wrapper/boo', text: value as String), - }, - ); - - stanza['hehe'] = 'blya'; - stanza['boo'] = 'blya2'; - tester.check( - stanza, - 'blyablya2', - ); - stanza.setSubText('/wrapper/hehe', text: '', keep: true); - tester.check( - stanza, - 'blya2', - useValues: false, - ); - stanza['hehe'] = 'a'; - stanza.setSubText('/wrapper/hehe', text: ''); - tester.check( - stanza, - 'blya2', - ); - }); - - test('must return correct stanza after removal of substanzas', () { - final stanza = createTestStanza( - name: 'lerko', - namespace: 'test', - interfaces: {'hehe', 'boo'}, - setters: { - const Symbol('hehe'): (value, args, base) => base - .setSubText('/wrapper/herto/herto1/hehe', text: value as String), - const Symbol('boo'): (value, args, base) => base - .setSubText('/wrapper/herto/herto2/boo', text: value as String), - }, - getters: { - const Symbol('hehe'): (args, base) => - base.getSubText('/wrapper/herto/herto1/hehe'), - const Symbol('boo'): (args, base) => - base.getSubText('/wrapper/herto/herto2/boo'), - }, - deleters: { - const Symbol('hehe'): (args, base) => - base.deleteSub('/wrapper/herto/herto1/hehe'), - const Symbol('boo'): (args, base) => - base.deleteSub('/wrapper/herto/herto2/boo'), - }, - ); - - stanza['hehe'] = 'cart'; - stanza['boo'] = 'blya'; - tester.check( - stanza, - 'cartblya', - ); - stanza.delete('hehe'); - stanza.delete('boo'); - tester.check( - stanza, - '', - useValues: false, - ); - stanza['hehe'] = 'blyat'; - stanza['boo'] = 'zort'; - - stanza.deleteSub('/wrapper/herto/herto1/hehe', all: true); - tester.check( - stanza, - 'blya', - ); - }); - - test('must return false for non available property', () { - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar'}, - boolInterfaces: {'bar'}, - ); - - tester.check(stanza, ''); - - expect(stanza['bar'], isFalse); - - stanza['bar'] = true; - tester.check(stanza, ''); - - stanza['bar'] = false; - tester.check(stanza, ''); - }); - - test('must override interfaces', () { - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - ); - final overriderStanza = createTestStanza( - name: 'overrider', - namespace: 'test', - pluginAttribute: 'overrider', - interfaces: {'bar'}, - overrides: ['set_bar'], - includeNamespace: false, - setters: { - const Symbol('set_bar'): (value, args, base) { - if (!(value as String).startsWith('override-')) { - base.parent?.setAttribute('bar', 'override-$value'); - } else { - base.parent?.setAttribute('bar', value); - } - }, - }, - ); - - stanza['bar'] = 'foo'; - tester.check(stanza, ''); - - registerStanzaPlugin(stanza, overriderStanza, overrides: true); - stanza['bar'] = 'foo'; - tester.check( - stanza, - '', - ); - }); - - test('XMLBase.isExtension property usage test', () { - final extension = createTestStanza( - name: 'extended', - namespace: 'test', - pluginAttribute: 'extended', - interfaces: {'extended'}, - includeNamespace: false, - isExtension: true, - setters: { - const Symbol('extended'): (value, args, base) => - base.element!.innerText = value as String, - }, - getters: { - const Symbol('extended'): (args, base) => base.element!.innerText, - }, - deleters: { - const Symbol('extended'): (args, base) => - base.parent!.element!.children.remove(base.element), - }, - ); - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - ); - - registerStanzaPlugin(stanza, extension); - stanza['extended'] = 'testing'; - - tester.check( - stanza, - 'testing', - ); - - // expect(stanza['extended'], equals('testing')); - }); - - test('`values` getter test', () { - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - ); - final substanza = createTestStanza( - name: 'subfoo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - ); - final plugin = createTestStanza( - name: 'foo2', - namespace: 'test', - interfaces: {'bar', 'baz'}, - pluginAttribute: 'foo2', - includeNamespace: false, - ); - - registerStanzaPlugin(stanza, plugin, iterable: true); - - stanza['bar'] = 'a'; - (stanza['foo2'] as XMLBase)['baz'] = 'b'; - substanza['bar'] = 'c'; - stanza.add(substanza); - - expect( - stanza.values, - equals( - { - 'lang': '', - 'bar': 'a', - 'baz': '', - 'foo2': {'lang': '', 'bar': '', 'baz': 'b'}, - 'substanzas': [ - { - 'lang': '', - 'bar': '', - 'baz': 'b', - '__childtag__': '{test}foo2', - }, - { - 'lang': '', - 'bar': 'c', - 'baz': '', - '__childtag__': '{test}subfoo', - } - ], - }, - ), - ); - }); - - test('accessing stanza interfaces', () { - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar', 'baz', 'cart'}, - subInterfaces: {'bar'}, - getters: {const Symbol('cart'): (args, base) => 'cart'}, - ); - final plugin = createTestStanza( - name: 'foobar', - namespace: 'test', - pluginAttribute: 'foobar', - interfaces: {'xeem'}, - includeNamespace: false, - ); - - registerStanzaPlugin(stanza, stanza, iterable: true); - registerStanzaPlugin(stanza, plugin); - - final substanza = stanza.copy(); - stanza.add(substanza); - stanza.values = { - 'bar': 'a', - 'baz': 'b', - 'cart': 'gup', - 'foobar': {'xeem': 'c'}, - }; - - final expected = { - 'substanzas': [substanza], - 'bar': 'a', - 'baz': 'b', - 'cart': 'cart', - }; - - for (final item in expected.entries) { - final result = stanza[item.key]; - expect(result, equals(item.value)); - } - - expect((stanza['foobar'] as XMLBase)['xeem'], equals('c')); - }); - - test('`values` setter test', () { - final stanza = createTestStanza( - name: 'foo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - ); - final substanza = createTestStanza( - name: 'subfoo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - includeNamespace: false, - ); - final plugin = createTestStanza( - name: 'pluginfoo', - namespace: 'test', - interfaces: {'bar', 'baz'}, - pluginAttribute: 'pluginfoo', - includeNamespace: false, - ); - - registerStanzaPlugin(stanza, substanza, iterable: true); - registerStanzaPlugin(stanza, plugin); - const values = { - 'bar': 'a', - 'baz': '', - 'pluginfoo': {'bar': '', 'baz': 'b'}, - 'substanzas': [ - { - 'bar': 'c', - 'baz': '', - '__childtag__': '{test}subfoo', - } - ], - }; - stanza.values = values; - tester.check( - stanza, - '', - ); - }); - - test( - 'retrieving multi_attribute substanzas using _Multi multifactory', - () { - final stanza = createTestStanza(name: 'foo', namespace: 'test'); - final multistanzaFirst = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaSecond = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - final multistanzaThird = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaFourth = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - - registerStanzaPlugin(stanza, multistanzaFirst, iterable: true); - registerStanzaPlugin(stanza, multistanzaSecond, iterable: true); - - stanza.add(multistanzaFirst); - stanza.add(multistanzaSecond); - stanza.add(multistanzaThird); - stanza.add(multistanzaFourth); - - tester.check( - stanza, - '', - useValues: false, - ); - - final bars = stanza['bars']; - final bazs = stanza['bazs']; - - for (final bar in bars as List) { - tester.check(bar, ''); - } - - for (final baz in bazs as List) { - tester.check(baz, ''); - } - - expect(bars.length, equals(2)); - expect(bazs.length, equals(2)); - }, - ); - - test( - 'test setting multi_attribute substanzas', - () { - final stanza = createTestStanza(name: 'foo', namespace: 'test'); - final multistanzaFirst = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaSecond = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - final multistanzaThird = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaFourth = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - - registerStanzaPlugin(stanza, multistanzaFirst, iterable: true); - registerStanzaPlugin(stanza, multistanzaSecond, iterable: true); - - stanza['bars'] = [multistanzaFirst, multistanzaThird]; - stanza['bazs'] = [multistanzaSecond, multistanzaFourth]; - - tester.check( - stanza, - '', - useValues: false, - ); - - expect((stanza['substanzas'] as List).length, equals(4)); - - stanza['bars'] = [multistanzaFirst]; - - tester.check( - stanza, - '', - useValues: false, - ); - - expect((stanza['substanzas'] as List).length, equals(3)); - }, - ); - - test( - 'must delete multi_attribute substanzas properly', - () { - final stanza = createTestStanza(name: 'foo', namespace: 'test'); - final multistanzaFirst = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaSecond = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - final multistanzaThird = createTestStanza( - name: 'bar', - namespace: 'test', - pluginAttribute: 'bar', - pluginMultiAttribute: 'bars', - ); - final multistanzaFourth = MultiTestStanza2( - name: 'baz', - namespace: 'test', - pluginAttribute: 'baz', - pluginMultiAttribute: 'bazs', - ); - - registerStanzaPlugin(stanza, multistanzaFirst, iterable: true); - registerStanzaPlugin(stanza, multistanzaSecond, iterable: true); - - stanza['bars'] = [multistanzaFirst, multistanzaThird]; - stanza['bazs'] = [multistanzaSecond, multistanzaFourth]; - - tester.check( - stanza, - '', - useValues: false, - ); - - expect((stanza['substanzas'] as List).length, equals(4)); - - stanza.delete('bars'); - - tester.check( - stanza, - '', - useValues: false, - ); - - expect((stanza['substanzas'] as List).length, equals(2)); - }, - ); - }); -} diff --git a/test/command_test.dart b/test/command_test.dart deleted file mode 100644 index b316270..0000000 --- a/test/command_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:whixp/whixp.dart'; - -import 'test_base.dart'; - -void main() { - late IQ iq; - - setUp(() => iq = IQ(generateID: false)..enable('command')); - - group('command stanza from Ad-Hoc test cases', () { - test('using action attribute', () { - iq['type'] = 'set'; - final command = iq['command'] as Command; - command['node'] = 'cart'; - - command['action'] = 'execute'; - expect(command['action'], equals('execute')); - expect(command.action, equals('execute')); - - command['action'] = 'complete'; - expect(command['action'], equals('complete')); - expect(command.action, equals('complete')); - - command['action'] = 'cancel'; - expect(command['action'], equals('cancel')); - expect(command.action, equals('cancel')); - }); - - test('setting next action in a command stanza', () { - final command = iq['command'] as Command; - iq['type'] = 'result'; - command['node'] = 'cart'; - command['actions'] = ['prev', 'next']; - - check( - iq, - '', - ); - }); - - test('must properly retrieve next actions from a command stanza', () { - final command = iq['command'] as Command; - command['node'] = 'cart'; - command['actions'] = ['prev', 'next']; - - final results = command['actions']; - final expected = ['prev', 'next']; - expect(results, equals(expected)); - }); - - test('must properly delete all actions from command stanza', () { - final command = iq['command'] as Command; - iq['type'] = 'result'; - command['node'] = 'cart'; - command['actions'] = ['prev', 'next']; - - /// or `command.deleteActions()` - command.delete('actions'); - check( - iq, - '', - ); - }); - - test('adding a command note', () { - final command = iq['command'] as Command; - iq['type'] = 'result'; - command['node'] = 'cart'; - command.addNote('something happened, blyat!', 'warning'); - - check( - iq, - 'Something happened, blyat!', - ); - }); - - test('command notes test case', () { - final command = iq['command'] as Command; - iq['type'] = 'result'; - command['node'] = 'cart'; - - final notes = { - 'info': 'xeeem', - 'warning': 'gup', - 'error': 'some error happened', - }; - - command['notes'] = notes; - - expect(command['notes'], equals(notes)); - - check( - iq, - 'xeeemgupsome error happened', - ); - }); - }); -} diff --git a/test/roster_test.dart b/test/roster_test.dart deleted file mode 100644 index 931d377..0000000 --- a/test/roster_test.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/roster.dart'; - -import 'test_base.dart'; - -void main() { - late IQ iq; - - setUp(() => iq = IQ(generateID: false)); - - group('iq roster test cases', () { - test('must properly add items to a roster stanza', () { - (iq['roster'] as Roster)['items'] = { - 'vsevex@example.com': { - 'name': 'Vsevolod', - 'subscription': 'both', - 'groups': ['cart', 'hella'], - }, - 'alyosha@example.com': { - 'name': 'Alyosha', - 'subscription': 'both', - 'groups': ['gup'], - }, - }; - - check( - iq, - 'carthellagup', - ); - }); - - test('get items from roster', () { - const items = { - 'vsevex@example.com': { - 'lang': '', - 'jid': 'vsevex@example.com', - 'name': 'Vsevolod', - 'subscription': 'both', - 'ask': '', - 'approved': '', - 'groups': ['cart', 'hella'], - }, - 'alyosha@example.com': { - 'lang': '', - 'jid': 'alyosha@example.com', - 'name': 'Alyosha', - 'subscription': 'both', - 'ask': '', - 'approved': '', - 'groups': ['gup'], - }, - }; - - (iq['roster'] as Roster)['items'] = { - 'vsevex@example.com': { - 'name': 'Vsevolod', - 'subscription': 'both', - 'groups': ['cart', 'hella'], - }, - 'alyosha@example.com': { - 'name': 'Alyosha', - 'subscription': 'both', - 'groups': ['gup'], - }, - }; - - expect((iq['roster'] as Roster)['items'], items); - }); - - test('must properly delete roster items', () { - (iq['roster'] as Roster)['items'] = { - 'vsevex@example.com': { - 'name': 'Vsevolod', - 'subscription': 'both', - 'groups': ['cart', 'hella'], - }, - 'alyosha@example.com': { - 'name': 'Alyosha', - 'subscription': 'both', - 'groups': ['gup'], - }, - }; - - (iq['roster'] as Roster).delete('items'); - check( - iq, - '', - ); - }); - }); -} diff --git a/test/stanza_test.dart b/test/stanza_test.dart deleted file mode 100644 index 638e1b0..0000000 --- a/test/stanza_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -void main() { - group('stanza base test cases for methods', () { - test('must properly set "to" interface of base', () { - final stanza = StanzaBase(); - stanza['to'] = 'hert@cart.net'; - expect(stanza['to'], equals('hert@cart.net')); - stanza.setTo('hert@cart1.net'); - expect(stanza['to'], equals('hert@cart1.net')); - }); - - test('must properly set "from" interface of base', () { - final stanza = StanzaBase(); - stanza['from'] = 'hert@cart.net'; - expect(stanza['from'], equals('hert@cart.net')); - stanza.setFrom('hert@cart1.net'); - expect(stanza['from'], equals('hert@cart1.net')); - }); - - test('"payload" interface of base various tests', () { - final stanza = StanzaBase(); - expect(stanza.payload, isEmpty); - - stanza['payload'] = xml.XmlElement(xml.XmlName('cart')); - expect(stanza.payload.length, equals(1)); - - stanza.setPayload([xml.XmlElement(xml.XmlName('cart'))]); - expect(stanza.payload.length, equals(2)); - - stanza.deletePayload(); - expect(stanza.payload, isEmpty); - - stanza['payload'] = xml.XmlElement(xml.XmlName('cart')); - expect(stanza.payload.length, equals(1)); - - stanza.delete('payload'); - expect(stanza.payload, isEmpty); - }); - - test('reply functionality must work properly', () { - final stanza = StanzaBase(); - - stanza.setTo('cart@hert.org'); - stanza.setFrom('lerko@hert.org'); - stanza.setPayload([WhixpUtils.xmlElement('foo', namespace: 'test')]); - - final reply = stanza.reply(copiedStanza: stanza); - - expect(reply['to'], equals('lerko@hert.org')); - expect(reply.payload, isEmpty); - }); - }); -} diff --git a/test/time_test.dart b/test/time_test.dart deleted file mode 100644 index cfbc417..0000000 --- a/test/time_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:whixp/src/plugins/time/time.dart'; - -void main() { - group('XMPP Date and Time Profiles test cases', () { - test( - 'must properly parse the DateTime object from the given String', - () { - const time = '2024-01-21T12:30:00.000Z'; - final parsed = parse(time); - - expect(parsed, equals(DateTime.utc(2024, 1, 21, 12, 30))); - }, - ); - - test( - 'must properly format the passed DateTime object to corresponding String objecdt', - () { - final time = DateTime.utc(2024, 8, 1, 01, 30); - final formatted = format(time); - - expect(formatted, equals('2024-08-01T01:30:00.000Z')); - }, - ); - - test( - 'format only date when passing DateTime object', - () { - final time = DateTime(2024, 8, 1, 01, 30); - final formatted = formatDate(time); - - expect(formatted, equals('2024-08-01T00:00:00.000Z')); - }, - ); - - test( - 'format only time when passing DateTime object', - () { - final time = DateTime(2024, 8, 1, 01, 30); - final formatted = formatTime(time, useZulu: true); - - expect(formatted, equals('01:30:00.000Z')); - }, - ); - - test('must properly create and return DateTime object', () { - final dateTime = date(year: 2024, month: 8, day: 1, asString: false); - - expect(dateTime, equals(DateTime.utc(2024, 8))); - }); - - test( - 'must properly create and return dateTime in String format', - () { - final dateTime = date(year: 2024, month: 8, day: 1); - - expect(dateTime, '2024-08-01T00:00:00.000Z'); - }, - ); - }); -} From 3caf59a10cc22407fdc5cce367af07a73bd477ab Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:56:41 +0400 Subject: [PATCH 11/81] refactor(xml): Remove unused code files --- lib/src/handler/callback.dart | 56 - lib/src/handler/waiter.dart | 44 - lib/src/plugins/base.dart | 186 -- lib/src/plugins/bind/bind.dart | 62 - lib/src/plugins/bind/stanza.dart | 13 - lib/src/plugins/mechanisms/stanza/stanza.dart | 47 - lib/src/plugins/pep/pep.dart | 128 -- lib/src/plugins/ping/ping.dart | 254 --- lib/src/plugins/ping/stanza.dart | 23 - lib/src/plugins/preapproval/preapproval.dart | 29 - lib/src/plugins/preapproval/stanza.dart | 11 - lib/src/plugins/pubsub/event.dart | 374 ---- lib/src/plugins/register/register.dart | 254 --- lib/src/plugins/register/stanza.dart | 149 -- lib/src/plugins/rosterver/rosterver.dart | 29 - lib/src/plugins/rosterver/stanza.dart | 11 - lib/src/plugins/rsm/rsm.dart | 202 -- lib/src/plugins/rsm/stanza.dart | 143 -- lib/src/plugins/session/session.dart | 53 - lib/src/plugins/session/stanza.dart | 47 - lib/src/plugins/sm/sm.dart | 364 ---- lib/src/plugins/sm/stanza.dart | 317 ---- lib/src/plugins/starttls/stanza.dart | 47 - lib/src/plugins/starttls/starttls.dart | 63 - lib/src/stanza/root.dart | 78 - lib/src/stream/base.dart | 1660 ----------------- lib/src/stream/matcher/base.dart | 14 - lib/src/stream/matcher/id.dart | 74 - lib/src/stream/matcher/many.dart | 26 - lib/src/stream/matcher/matcher.dart | 6 - lib/src/stream/matcher/stanza.dart | 28 - lib/src/stream/matcher/xml.dart | 84 - lib/src/stream/matcher/xpath.dart | 43 - lib/src/stream/stanza.dart | 194 -- test/class/base.dart | 65 - test/class/property.dart | 7 - xml/_extensions.dart | 318 ---- xml/_model.dart | 28 - xml/_registrator.dart | 31 - xml/_registry.dart | 13 - xml/_static.dart | 3 - xml/base.dart | 206 -- xml/stanza.dart | 66 - 43 files changed, 5850 deletions(-) delete mode 100644 lib/src/handler/callback.dart delete mode 100644 lib/src/handler/waiter.dart delete mode 100644 lib/src/plugins/base.dart delete mode 100644 lib/src/plugins/bind/bind.dart delete mode 100644 lib/src/plugins/bind/stanza.dart delete mode 100644 lib/src/plugins/mechanisms/stanza/stanza.dart delete mode 100644 lib/src/plugins/pep/pep.dart delete mode 100644 lib/src/plugins/ping/ping.dart delete mode 100644 lib/src/plugins/ping/stanza.dart delete mode 100644 lib/src/plugins/preapproval/preapproval.dart delete mode 100644 lib/src/plugins/preapproval/stanza.dart delete mode 100644 lib/src/plugins/pubsub/event.dart delete mode 100644 lib/src/plugins/register/register.dart delete mode 100644 lib/src/plugins/register/stanza.dart delete mode 100644 lib/src/plugins/rosterver/rosterver.dart delete mode 100644 lib/src/plugins/rosterver/stanza.dart delete mode 100644 lib/src/plugins/rsm/rsm.dart delete mode 100644 lib/src/plugins/rsm/stanza.dart delete mode 100644 lib/src/plugins/session/session.dart delete mode 100644 lib/src/plugins/session/stanza.dart delete mode 100644 lib/src/plugins/sm/sm.dart delete mode 100644 lib/src/plugins/sm/stanza.dart delete mode 100644 lib/src/plugins/starttls/stanza.dart delete mode 100644 lib/src/plugins/starttls/starttls.dart delete mode 100644 lib/src/stanza/root.dart delete mode 100644 lib/src/stream/base.dart delete mode 100644 lib/src/stream/matcher/base.dart delete mode 100644 lib/src/stream/matcher/id.dart delete mode 100644 lib/src/stream/matcher/many.dart delete mode 100644 lib/src/stream/matcher/matcher.dart delete mode 100644 lib/src/stream/matcher/stanza.dart delete mode 100644 lib/src/stream/matcher/xml.dart delete mode 100644 lib/src/stream/matcher/xpath.dart delete mode 100644 lib/src/stream/stanza.dart delete mode 100644 test/class/base.dart delete mode 100644 test/class/property.dart delete mode 100644 xml/_extensions.dart delete mode 100644 xml/_model.dart delete mode 100644 xml/_registrator.dart delete mode 100644 xml/_registry.dart delete mode 100644 xml/_static.dart delete mode 100644 xml/base.dart delete mode 100644 xml/stanza.dart diff --git a/lib/src/handler/callback.dart b/lib/src/handler/callback.dart deleted file mode 100644 index 48072a4..0000000 --- a/lib/src/handler/callback.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of 'handler.dart'; - -/// A speciliazed [Handler] implementation for handling XMPP stanzas with -/// a sync callback. -/// -/// Extends [Handler]. -/// -/// Allows you to define a handler with a sync callback function that gets -/// executed when the handler matches a stanza based on the provided matcher. -/// -/// ### Example: -/// ```dart -/// final handler = CallbackHandler('idOfStanza', (stanza) { -/// log(stanza); -/// /// ...do something with matched stanza. -/// }); -/// ``` -/// -/// For more information refer to [Handler]. -class CallbackHandler extends Handler { - /// Creates an instance of [CallbackHandler] with the specified parameters. - CallbackHandler(super.name, this.callback, {required super.matcher}); - - /// The sync callback function to be executed when the handler matches a - /// stanza. - void Function(StanzaBase stanza) callback; - - @override - void run(StanzaBase stanza) => callback.call(stanza); -} - -/// A speciliazed [Handler] implementation for handling XMPP stanzas with -/// a async callback. -/// -/// Extends [Handler]. -/// -/// Allows you to define a handler with a async callback function that gets -/// executed when the handler matches a stanza based on the provided matcher. -/// -/// ### Example: -/// ```dart -/// final handler = FutureCallbackHandler('idOfStanza', (stanza) { -/// log(stanza); -/// /// ...do something with matched stanza. -/// }); -/// ``` -/// -/// For more information refer to [Handler]. -class FutureCallbackHandler extends Handler { - FutureCallbackHandler(super.name, this.callback, {required super.matcher}); - - final Future Function(StanzaBase stanza) callback; - - @override - Future run(StanzaBase payload) => callback(payload); -} diff --git a/lib/src/handler/waiter.dart b/lib/src/handler/waiter.dart deleted file mode 100644 index 382c0e2..0000000 --- a/lib/src/handler/waiter.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:async'; - -import 'package:synchronized/extension.dart'; - -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/stream/base.dart'; - -class Waiter extends Handler { - Waiter(super.name, {required super.matcher, super.transport}); - - final completer = Completer(); - - @override - FutureOr run(StanzaBase payload) { - if (!completer.isCompleted) { - completer.complete(payload); - } - } - - /// Blocks an event handler while waiting for a stanza to arrive. - /// - /// [timeout] is represented in seconds. - Future wait({int timeout = 10}) async { - if (transport == null) { - throw Exception('wait() called without a transport'); - } - - try { - await synchronized(() => completer.future).timeout( - Duration(seconds: timeout), - onTimeout: () { - completer.complete(); - throw TimeoutException('Timed out waiting for $name'); - }, - ); - } on TimeoutException { - if (transport != null) { - transport!.removeHandler(name); - } - } - - return completer.future; - } -} diff --git a/lib/src/plugins/base.dart b/lib/src/plugins/base.dart deleted file mode 100644 index 5ecc1c9..0000000 --- a/lib/src/plugins/base.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import 'package:synchronized/synchronized.dart'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/whixp.dart'; - -/// Manages the registration and activation of XMPP plugins. -/// -/// Provides functionality for registering, enabling, and managing XMPP plugins. -/// -/// ### Example: -/// ```dart -/// final manager = PluginManager(); -/// manager.register('starttls', FeatureStartTLS()); -/// ``` -class PluginManager { - /// A [Set] containing the names of currently enabled plugins. - final enabledPlugins = {}; - - /// A [Map] containing the names and instances of currently active plugins. - final activePlugins = {}; - - /// A reentrant lock used to synchronize access to the plugin manager's data - /// structures. - final _lock = Lock(reentrant: true); - - /// A [Map] containing registered plugins with their names as keys. - final _pluginRegistery = {}; - - /// A [Map] containing plugin dependencies, where each entry maps a plugin - /// name to its dependents. - final _pluginDependents = >{}; - - /// A [Completer] used to signal the completion of locked operations. - late Completer _lockCompleter; - - /// Registers a plugin with the specified [name]. - void register(String name, PluginBase plugin) { - _lockCompleter = Completer(); - - _lockCompleter.complete( - _lock.synchronized(() async { - _pluginRegistery[name] = plugin; - if (_pluginDependents.containsKey(name) && - _pluginDependents[name] != null && - _pluginDependents[name]!.isNotEmpty) { - for (final dependent in plugin._dependencies) { - _pluginDependents[dependent]!.add(name); - } - } else { - _pluginDependents[name] = {}; - } - }), - ); - } - - /// Gets [PluginBase] instance if there is any registered one under specific - /// [name]. If [enableIfRegistered] passed as true, then plugin activates - /// if it is not active yet. Otherwise, returns `null`. - T? getPluginInstance(String name, {bool enableIfRegistered = true}) { - if (_pluginRegistery[name] != null && enabledPlugins.contains(name)) { - return _pluginRegistery[name] as T; - } else if (_pluginRegistery[name] != null) { - if (enableIfRegistered) { - enable(name); - } - return _pluginRegistery[name] as T; - } - return null; - } - - /// Enables a plugin and its dependencies. - void enable(String name, {Set? enabled}) { - final enabledTemp = enabled ?? {}; - _lockCompleter = Completer(); - - _lockCompleter.complete( - _lock.synchronized(() { - /// Indicates that the plugin is already enabled. - if (enabledTemp.contains(name)) { - return; - } - enabledTemp.add(name); - enabledPlugins.add(name); - if (_pluginRegistery.containsKey(name) && - _pluginRegistery[name] != null) { - final plugin = _pluginRegistery[name]!; - activePlugins[name] = plugin; - if (plugin._dependencies.isNotEmpty) { - for (final dependency in plugin._dependencies) { - enable(dependency, enabled: enabledTemp); - } - } - plugin._initialize(); - } - return; - }), - ); - } - - /// Checks if a plugin with the given [name] is registered. - bool registered(String name) => _pluginRegistery.containsKey(name); -} - -/// An abstract class representing the base structure for XMPP plugins. -/// -/// Implementations of this class are intended to provide functionality related -/// to specific XMPP features or extensions. -/// -/// ### Example: -/// ```dart -/// class FeatureStartTLS extends BasePlugin { -/// const FeatureStartTLS(this._features, {required super.base}) -/// : super( -/// 'starttls', -/// description: 'Stream Feature: STARTTLS', -/// ); -/// } -/// ``` -abstract class PluginBase { - /// Creates an instance [PluginBase] with the specified parameters. - PluginBase( - /// Short name - this.name, { - /// Long name - String description = '', - - /// Plugin related dependencies - Set dependencies = const {}, - }) { - _description = description; - _dependencies = dependencies; - } - - /// A short name for the plugin based on the implemented specification. - /// - /// For example, a plugin for StartTLS would use "starttls". - final String name; - - /// A longer name for the plugin, describing its purpose. For example a - /// plugin for StartTLS would use "Stream Feature: STARTTLS" as its - /// description value. - late final String _description; - - /// Some plugins may depend on others in order to function properly. Any - /// plugins names included in [dependencies] will be initialized as - /// needed if this plugin is enabled. - late final Set _dependencies; - - /// [WhixpBase] instance to use accross the plugin implementation. - @internal - late final WhixpBase base; - - /// Initializes the plugin. Concrete implementations should override this - /// method to perform necessary setup or initialization. - void _initialize() { - base.addEventHandler( - 'sessionBind', - (jid) => sessionBind(jid.toString()), - ); - base.addEventHandler('sessionEnd', (_) => pluginEnd()); - if (base.transport.sessionBind) { - sessionBind(base.transport.boundJID.toString()); - } - pluginInitialize(); - Log.instance.debug('Loaded plugin: $_description'); - } - - /// Initialize plugin state, such as registering event handlers. - @internal - void pluginInitialize(); - - /// Cleanup plugin state, and prepare for plugin removal. - @internal - void pluginEnd(); - - /// Initialize plugin state based on the bound [jid]. - @internal - void sessionBind(String? jid); - - @override - String toString() => 'Plugin: $name: $_description'; -} diff --git a/lib/src/plugins/bind/bind.dart b/lib/src/plugins/bind/bind.dart deleted file mode 100644 index 01ee7d5..0000000 --- a/lib/src/plugins/bind/bind.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:meta/meta.dart'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -part 'stanza.dart'; - -class FeatureBind extends PluginBase { - FeatureBind() : super('bind', description: 'Resource Binding'); - - StanzaBase? _features; - - late final IQ _iq; - - @override - void pluginInitialize() { - final bind = BindStanza(); - - _iq = IQ(transport: base.transport); - base.registerFeature('bind', _handleBindResource, order: 10000); - _iq.registerPlugin(bind); - } - - Future _handleBindResource(StanzaBase stanza) async { - _features = stanza; - _iq['type'] = 'set'; - _iq.enable('bind'); - - if (base.requestedJID.resource.isNotEmpty) { - (_iq['bind'] as XMLBase)['resource'] = base.requestedJID.resource; - } - - await _iq.sendIQ(callback: _onBindResponse); - } - - void _onBindResponse(IQ response) { - base.transport.boundJID = - JabberID((response['bind'] as XMLBase)['jid'] as String); - - base.transport.sessionBind = true; - base.transport.emit('sessionBind', data: base.transport.boundJID); - - base.features.add('bind'); - - if (!(_features!['features'] as Map) - .containsKey('session')) { - base.transport.sessionStarted = true; - base.transport.emit('sessionStart'); - } - } - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} -} diff --git a/lib/src/plugins/bind/stanza.dart b/lib/src/plugins/bind/stanza.dart deleted file mode 100644 index e242456..0000000 --- a/lib/src/plugins/bind/stanza.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of 'bind.dart'; - -@internal -class BindStanza extends XMLBase { - BindStanza({super.element}) - : super( - name: 'bind', - namespace: WhixpUtils.getNamespace('BIND'), - interfaces: {'resource', 'jid'}, - subInterfaces: {'resource', 'jid'}, - pluginAttribute: 'bind', - ); -} diff --git a/lib/src/plugins/mechanisms/stanza/stanza.dart b/lib/src/plugins/mechanisms/stanza/stanza.dart deleted file mode 100644 index c5fde6f..0000000 --- a/lib/src/plugins/mechanisms/stanza/stanza.dart +++ /dev/null @@ -1,47 +0,0 @@ -part of '../feature.dart'; - -@internal -class Mechanisms extends XMLBase { - Mechanisms() - : super( - name: 'mechanisms', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'mechanisms', 'required'}, - pluginAttribute: 'mechanisms', - isExtension: true, - getters: { - const Symbol('required'): (_, __) => true, - const Symbol('mechanisms'): (args, base) { - final results = []; - final mechs = base.element!.findAllElements('mechanism').toList(); - if (mechs.isNotEmpty) { - for (final mech in mechs) { - results.add(mech.innerText); - } - } - return results; - }, - }, - setters: { - const Symbol('mechanisms'): (values, args, base) { - base.delete('mechanisms'); - for (final value in values as List) { - final mech = xml.XmlElement(xml.XmlName('mechanism')); - mech.innerText = value; - base.add(mech); - } - }, - }, - deleters: { - const Symbol('mechanisms'): (args, base) { - final mechs = base.element!.findAllElements('mechanism').toList(); - if (mechs.isNotEmpty) { - for (final mech in mechs) { - base.element!.children.remove(mech); - } - } - }, - }, - ); -} diff --git a/lib/src/plugins/pep/pep.dart b/lib/src/plugins/pep/pep.dart deleted file mode 100644 index 5ab9df2..0000000 --- a/lib/src/plugins/pep/pep.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; - -/// Personal eventing provides a way for a Jabber/XMPP user to send updates or -/// "events" to other users, who are typically contacts in the user's roster. -/// -/// An event can be anything that a user wants to make known to other people, -/// such as those described in User Geolocation (XEP-0080), User Mood -/// (XEP-0107), User Activity (XEP-0108), and User Tune (XEP-0118). While the -/// XMPP Publish-Subscribe (XEP-0060) extension ("pubsub") can be used to -/// broadcast such events associated, the full pubsub protocol is often -/// thought of as complicated and therefore has not been widely implemented. -/// -/// see -class PEP extends PluginBase { - PEP() - : super( - 'PEP', - description: 'XEP-0163: Personal Eventing Protocol', - dependencies: {'disco', 'pubsub'}, - ); - - late final PubSub _pubsub; - - /// If [PubSub] stanza is not registered by user, do not hesitate to register - /// the corresponding plugin, 'cause [PEP] requires initialization of the - /// [PubSub]. - @override - void pluginInitialize() { - if (base.getPluginInstance('pubsub') == null) { - _pubsub = PubSub(); - base.registerPlugin(_pubsub); - } - } - - /// Setups and configures events and registers [stanza] for the given PEP - /// stanza. - /// - /// * Adds service discovery feature for the PEP content. - /// * Registers discovery interest in the PEP content. - /// * Maps events from the PEP content's `namespace` to the given [name]. - void registerPEP(String name, XMLBase stanza) { - stanza.registerPlugin(PubSubEventItem()); - - addInterest([stanza.namespace]); - - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.addFeature(stanza.namespace); - } - - final pubsub = base.getPluginInstance('pubsub'); - if (pubsub != null) { - pubsub.mapNodeEvent(stanza.namespace, name); - } - } - - /// Marks an interest in a PEP subscription by including a [ServiceDiscovery] - /// feature with the '+notify' extension. - /// - /// [namespaces] is the [List] of namespaces to register interests, such as - /// 'http://jabber.org/protocol/tune'. - void addInterest(List namespaces, {JabberID? jid}) { - for (final namespace in namespaces) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.addFeature('$namespace+notify', jid: jid); - } - } - } - - /// Marks an interest in a PEP subscription by including a [ServiceDiscovery] - /// feature with the '+notify' extension. - void removeInterest(List namespaces, {JabberID? jid}) { - for (final namespace in namespaces) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.removeFeature('$namespace+notify', jid: jid); - } - } - } - - /// Publishes a [PEP] update. - /// - /// This is just a thin wrapper around the [PubSub]'s [publish] method to set - /// the defaults expected by [PEP]. - FutureOr publish( - JabberID jid, - XMLBase stanza, { - String? node, - String? id, - Form? options, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - node ??= stanza.namespace; - id ??= 'current'; - - return _pubsub.publish( - jid, - node, - id: id, - iqFrom: jid, - payload: stanza.element, - options: options, - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - /// Do not implement. - @override - void pluginEnd() {} -} diff --git a/lib/src/plugins/ping/ping.dart b/lib/src/plugins/ping/ping.dart deleted file mode 100644 index 9910974..0000000 --- a/lib/src/plugins/ping/ping.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'dart:async'; - -import 'package:dartz/dartz.dart'; - -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/disco/disco.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; - -part 'stanza.dart'; - -/// Given that XMPP is reliant on TCP connections, the underlying connection may -/// be canceled without the application's knowledge. For identifying broken -/// connections, ping stanzas are an alternative to whitespace-based keepalive -/// approaches. -/// -/// see -class Ping extends PluginBase { - Ping({ - /// Time between keepalive pings. Represented in seconds. Defaults to `300`. - int interval = 300, - - /// Indicates the waiting time for a ping response. Defaults to `30` (in - /// seconds) - int timeout = 30, - - /// Indicates whether periodically send ping requests to the server. - /// - /// If a ping is not answered, the connection will be reset. Defaults to - /// `false`. - bool keepalive = false, - }) : super( - 'ping', - description: 'XEP-0199: XMPP Ping', - dependencies: {'disco'}, - ) { - _interval = interval; - _timeout = timeout; - _keepAlive = keepalive; - } - - /// time between keepalive pings. Represented in seconds. Defaults to `300`. - late final int _interval; - - /// Indicates the waiting time for a ping response. Defaults to `30` (in - /// seconds). - late final int _timeout; - - /// Indicates whether periodically send ping requests to the server. - /// - /// If a ping is not answered, the connection will be reset. Defaults to - /// `false`. - late final bool _keepAlive; - - late final IQ _iq; - late List _pendingTasks; - - @override - void pluginInitialize() { - _iq = IQ(transport: base.transport); - - _pendingTasks = []; - - base.transport.registerHandler( - CallbackHandler( - 'Ping', - (iq) => _handlePing(iq as IQ), - matcher: StanzaPathMatcher('iq@type=get/ping'), - ), - ); - - if (_keepAlive) { - base - ..addEventHandler('sessionStart', (_) => _enableKeepalive()) - ..addEventHandler('sessionResume', (_) => _enableKeepalive()) - ..addEventHandler('disconnected', (_) => _disableKeepalive()); - } - } - - /// Cancels all pending ping features. - void _clearPendingFeatures() { - if (_pendingTasks.isNotEmpty) { - Log.instance.debug('Clearing $_pendingTasks pending pings'); - for (final task in _pendingTasks) { - _pendingTasks.remove(task); - } - _pendingTasks.clear(); - } - } - - void _enableKeepalive() { - void handler() { - final temp = []; - if (_pendingTasks.isNotEmpty) { - for (final task in _pendingTasks) { - task.run(); - temp.add(task); - } - } - for (final task in temp) { - _pendingTasks.remove(task); - } - - _pendingTasks.add(Task(_keepalive)); - } - - handler.call(); - - base.transport.schedule( - 'pingalive', - handler, - seconds: _interval, - repeat: true, - ); - } - - void _disableKeepalive() { - _clearPendingFeatures(); - base.transport.cancelSchedule('pingalive'); - } - - Future _keepalive() async { - Log.instance.info('Keepalive ping is called'); - - await ping( - jid: base.transport.boundJID, - iqFrom: base.transport.boundJID, - timeout: _timeout, - timeoutCallback: (_) { - Log.instance.debug( - 'Did not receive ping back in time.\nRequesting reconnection', - ); - base.transport.reconnect(); - }, - ); - } - - /// Sends a ping request. - /// - /// [timeout] represents callback waiting timeout in `seconds`. - FutureOr sendPing( - JabberID jid, { - JabberID? iqFrom, - FutureOr Function(IQ stanza)? callback, - FutureOr Function(StanzaError stanza)? failureCallback, - FutureOr Function()? timeoutCallback, - int? timeout, - }) { - _iq['type'] = 'get'; - _iq['to'] = jid.toString(); - if (iqFrom != null) { - _iq['from'] = iqFrom.toString(); - } - _iq.enable('ping'); - - timeout ??= _timeout; - - return _iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Sends a ping request and calculates Round Trip Time (RTT). - Future ping({ - JabberID? jid, - JabberID? iqFrom, - FutureOr Function(StanzaBase stanza)? timeoutCallback, - int? timeout, - }) async { - bool ownHost = false; - late String rawJID = jid.toString(); - if (jid == null) { - if (base.isComponent) { - rawJID = base.transport.boundJID.server; - } else { - rawJID = base.transport.boundJID.host; - } - } - - if (rawJID == base.transport.boundJID.host || - (base.isComponent && rawJID == base.transport.boundJID.server)) { - ownHost = true; - } - - timeout ??= _timeout; - - final start = DateTime.now(); - late int rtt; - - Log.instance.debug('Pinging "$rawJID"'); - - try { - return sendPing( - JabberID(rawJID), - iqFrom: iqFrom, - timeout: timeout, - failureCallback: (stanza) { - if (ownHost) { - rtt = DateTime.now().difference(start).inSeconds; - Log.instance - .debug('Pinged "$rawJID", Round Trip Time in seconds: $rtt'); - } - }, - ); - } on Exception { - final rtt = DateTime.now().difference(start).inSeconds; - Log.instance.debug('Pinged "$rawJID", Round Trip Time in seconds: $rtt'); - return null; - } - } - - /// Automatically reply to ping requests. - void _handlePing(IQ iq) { - Log.instance.debug('Ping by ${iq['from']}'); - iq.replyIQ() - ..transport = base.transport - ..sendIQ(); - } - - @override - void pluginEnd() { - final disco = base.getPluginInstance( - 'disco', - enableIfRegistered: false, - ); - if (disco != null) { - disco.removeFeature(PingStanza().namespace); - } - - base.transport.removeHandler('Ping'); - if (_keepAlive) { - base.transport - ..removeEventHandler('sessionStart', handler: _enableKeepalive) - ..removeEventHandler('sessionResume', handler: _enableKeepalive) - ..removeEventHandler('disconnected', handler: _disableKeepalive); - } - } - - @override - void sessionBind(String? jid) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.addFeature(PingStanza().namespace); - } - } -} diff --git a/lib/src/plugins/ping/stanza.dart b/lib/src/plugins/ping/stanza.dart deleted file mode 100644 index 9ca53e2..0000000 --- a/lib/src/plugins/ping/stanza.dart +++ /dev/null @@ -1,23 +0,0 @@ -part of 'ping.dart'; - -/// Given that XMPP is reliant on TCP connections, the underlying connection may -/// be canceled without the application's knowledge. For identifying broken -/// connections, ping stanzas are an alternative to whitespace-based keepalive -/// approaches. -class PingStanza extends XMLBase { - /// ```xml - /// ping - /// - /// - /// - /// - /// pong - /// - /// ``` - PingStanza() - : super( - name: 'ping', - namespace: 'urn:xmpp:ping', - pluginAttribute: 'ping', - ); -} diff --git a/lib/src/plugins/preapproval/preapproval.dart b/lib/src/plugins/preapproval/preapproval.dart deleted file mode 100644 index 1e0680c..0000000 --- a/lib/src/plugins/preapproval/preapproval.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -part 'stanza.dart'; - -class FeaturePreApproval extends PluginBase { - FeaturePreApproval() - : super('preapproval', description: 'Subscription Pre-Approval'); - - @override - void pluginInitialize() => base.registerFeature( - 'preapproval', - (_) { - Log.instance.debug('Server supports subscription pre-approvals'); - return base.features.add('preapproval'); - }, - order: 9001, - ); - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} -} diff --git a/lib/src/plugins/preapproval/stanza.dart b/lib/src/plugins/preapproval/stanza.dart deleted file mode 100644 index 74e2d95..0000000 --- a/lib/src/plugins/preapproval/stanza.dart +++ /dev/null @@ -1,11 +0,0 @@ -part of 'preapproval.dart'; - -class PreApproval extends XMLBase { - PreApproval() - : super( - name: 'sub', - namespace: WhixpUtils.getNamespace('PREAPPROVAL'), - interfaces: const {}, - pluginAttribute: 'preapproval', - ); -} diff --git a/lib/src/plugins/pubsub/event.dart b/lib/src/plugins/pubsub/event.dart deleted file mode 100644 index e1548c2..0000000 --- a/lib/src/plugins/pubsub/event.dart +++ /dev/null @@ -1,374 +0,0 @@ -part of 'pubsub.dart'; - -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// [ ... ENTRY ... ] -/// -/// -/// -/// -/// ``` -class PubSubEvent extends XMLBase { - PubSubEvent({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'event', - namespace: _$event, - pluginAttribute: 'pubsub_event', - interfaces: {}, - ) { - registerPlugin(PubSubEventCollection()); - registerPlugin(PubSubEventConfiguration()); - registerPlugin(PubSubEventPurge()); - registerPlugin(PubSubEventDelete()); - registerPlugin(PubSubEventItems()); - registerPlugin(PubSubEventSubscription()); - } - - @override - PubSubEvent copy({xml.XmlElement? element, XMLBase? parent}) => PubSubEvent( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubEventItem extends XMLBase { - PubSubEventItem({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'item', - namespace: WhixpUtils.getNamespace('CLIENT'), - includeNamespace: false, - pluginAttribute: 'item', - interfaces: {'id', 'payload', 'node', 'publisher'}, - ) { - addGetters({ - const Symbol('payload'): (args, base) => _payload, - }); - - addSetters({ - const Symbol('payload'): (value, args, base) => - _setPayload(value as xml.XmlElement), - }); - - addDeleters({ - const Symbol('payload'): (args, base) => _deletePayload(), - }); - } - - void _setPayload(xml.XmlElement value) => element!.children.add(value.copy()); - - xml.XmlElement? get _payload { - if (element!.childElements.isNotEmpty) { - return element!.childElements.first; - } - return null; - } - - void _deletePayload() { - for (final child in element!.children) { - element!.children.remove(child); - } - } - - @override - PubSubEventItem copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventItem( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class PubSubEventItems extends XMLBase { - PubSubEventItems({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.element, - super.parent, - }) : super( - name: 'items', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'items', - interfaces: {'node'}, - ) { - registerPlugin(PubSubEventItem(), iterable: true); - registerPlugin(PubSubEventRetract(), iterable: true); - } - - List get items { - if (iterables.isNotEmpty) { - return iterables - .map((iterable) => PubSubItem(element: iterable.element)) - .toList(); - } - return []; - } - - @override - PubSubEventItems copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventItems( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - element: element, - parent: parent, - ); -} - -class PubSubEventRetract extends XMLBase { - PubSubEventRetract({super.element, super.parent}) - : super( - name: 'retract', - namespace: WhixpUtils.getNamespace('CLIENT'), - includeNamespace: false, - pluginAttribute: 'retract', - interfaces: {'id'}, - ); - - @override - PubSubEventRetract copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventRetract(element: element, parent: parent); -} - -class PubSubEventCollection extends XMLBase { - PubSubEventCollection({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'collection', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'collection', - interfaces: {'node'}, - ) { - registerPlugin(PubSubEventAssociate()); - registerPlugin(PubSubEventDisassociate()); - } - - @override - PubSubEventCollection copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventCollection( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubEventAssociate extends XMLBase { - PubSubEventAssociate({super.element, super.parent}) - : super( - name: 'associate', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'associate', - interfaces: {'node'}, - ); - - @override - PubSubEventAssociate copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventAssociate(element: element, parent: parent); -} - -class PubSubEventDisassociate extends XMLBase { - PubSubEventDisassociate({super.element, super.parent}) - : super( - name: 'disassociate', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'disassociate', - interfaces: {'node'}, - ); - - @override - PubSubEventDisassociate copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventDisassociate(element: element, parent: parent); -} - -class PubSubEventConfiguration extends XMLBase { - PubSubEventConfiguration({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'configuration', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'configuration', - interfaces: {'node'}, - ) { - registerPlugin(Form()); - } - - @override - PubSubEventConfiguration copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventConfiguration( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubEventPurge extends XMLBase { - PubSubEventPurge({super.element, super.parent}) - : super( - name: 'purge', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'purge', - interfaces: {'node'}, - ); - - @override - PubSubEventPurge copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventPurge(element: element, parent: parent); -} - -class PubSubEventDelete extends XMLBase { - PubSubEventDelete({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'delete', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'delete', - interfaces: {'node', 'redirect'}, - ) { - addGetters({ - const Symbol('redirect'): (args, base) => _redirect, - }); - - addSetters({ - const Symbol('redirect'): (value, args, base) => - _setRedirect(value as String), - }); - - addDeleters({ - const Symbol('redirect'): (args, base) => _deleteRedirect(), - }); - } - - String get _redirect { - final redirect = element!.getElement('redirect', namespace: namespace); - if (redirect != null) { - return redirect.getAttribute('uri') ?? ''; - } - return ''; - } - - void _setRedirect(String uri) { - delete('redirect'); - final redirect = WhixpUtils.xmlElement('redirect'); - redirect.setAttribute('uri', uri); - element!.children.add(redirect); - } - - void _deleteRedirect() { - final redirect = element!.getElement('redirect', namespace: namespace); - if (redirect != null) { - element!.children.remove(redirect); - } - } - - @override - PubSubEventDelete copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventDelete( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class PubSubEventSubscription extends XMLBase { - PubSubEventSubscription({ - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'subscription', - namespace: _$event, - includeNamespace: false, - pluginAttribute: 'subscription', - interfaces: { - 'node', - 'expiry', - 'jid', - 'subid', - 'subscription', - }, - ) { - addGetters({ - const Symbol('expiry'): (args, base) => _expiry, - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('expiry'): (value, args, base) => - _setExpiry(value as String), - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); - } - - String get _expiry { - final expiry = getAttribute('expiry'); - if (expiry.toLowerCase() == 'presence') { - return expiry; - } - - /// TODO: parse date - return ''; - } - - void _setExpiry(String value) => setAttribute('expiry', value); - - JabberID? get _jid { - final jid = getAttribute('jid'); - if (jid.isEmpty) { - return null; - } - return JabberID(jid); - } - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - - @override - PubSubEventSubscription copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubEventSubscription( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} diff --git a/lib/src/plugins/register/register.dart b/lib/src/plugins/register/register.dart deleted file mode 100644 index 8c2bcef..0000000 --- a/lib/src/plugins/register/register.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'dart:async'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/features.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -class InBandRegistration extends PluginBase { - InBandRegistration({ - bool createAccount = true, - bool forceRegistration = false, - // String formInstructions = 'Enter your credentials', - }) : super( - 'register', - description: 'XEP-0077: In-Band Registration', - dependencies: {'forms'}, - ) { - _createAccount = createAccount; - _forceRegistration = forceRegistration; - // _formInstructions = formInstructions; - } - - // late final String _formInstructions; - late final bool _createAccount; - late final bool _forceRegistration; - // late final Map> _users; - - @override - void pluginInitialize() { - // if (base.isComponent) { - // final disco = base.getPluginInstance('disco'); - // if (disco != null) { - // disco.addFeature('jabber:iq:register'); - // } - // _users = >{}; - // base.transport.registerHandler( - // FutureCallbackHandler( - // 'registration', - // (stanza) => _handleRegistration(stanza as IQ), - // matcher: StanzaPathMatcher('/iq/register'), - // ), - // ); - // } else { - base.registerFeature( - 'register', - (_) => _handleRegisterFeature(), - order: 50, - ); - // } - - base.addEventHandler('connected', (_) => _forceReg()); - } - - // bool _validateUser(JabberID iqFrom, Register registration) { - // _users[iqFrom.bare] = { - // for (final e in _formFields) e: registration[e], - // }; - - // if (_users[iqFrom.bare]!.containsValue(null)) { - // return false; - // } - // return true; - // } - - // Map? _getUser(IQ iq) { - // final from = iq['from'] as String; - // if (from.isNotEmpty) { - // return _users[JabberID(from).bare]; - // } - // return null; - // } - - // bool _removeUser(IQ iq) { - // final from = iq['from'] as String; - // if (from.isNotEmpty) { - // final result = _users.remove(JabberID(from).bare); - // if (result == null) { - // return false; - // } - // return true; - // } - // throw Exception(); - // } - - void _forceReg() { - if (_forceRegistration) { - base.transport.addFilter(filter: _forceStreamFeature); - } - } - - StanzaBase _forceStreamFeature(StanzaBase? stanza) { - if (stanza != null && stanza is StreamFeatures) { - if (!base.transport.disableStartTLS) { - if (base.features.contains('starttls')) { - return stanza; - } else if (!base.transport.isConnectionSecured) { - return stanza; - } - } - if (base.features.contains('mechanisms')) { - Log.instance.debug('Force adding in-band registration stream feature'); - base.transport.removeFilter(filter: _forceStreamFeature); - stanza.enable('register'); - } - } - return stanza!; - } - - // Future _handleRegistration(IQ iq) async { - // if (iq['type'] == 'get') { - // return _sendForm(iq); - // } else if (iq['type'] == 'set') { - // if ((iq['register'] as Register)['remove'] as bool) { - // try { - // final result = _removeUser(iq); - // if (result) { - // return Future.value(); - // } else { - // _sendError(iq, 404, 'cancel', 'item-not-found', 'User not found'); - // return Future.value(); - // } - // } on Exception { - // final reply = iq.replyIQ(); - // reply.sendIQ(); - // base.transport.emit('userUnregister', data: iq); - // return Future.value(); - // } - // } - - // for (final field in _formFields) { - // if ((iq['register'] as Register)[field] == null) { - // _sendError( - // iq, - // 406, - // 'modify', - // 'not-acceptable', - // 'Please fill in all fields.', - // ); - // return Future.value(); - // } - // } - - // if (!_validateUser( - // JabberID(iq['from'] as String), - // iq['register'] as Register, - // )) { - // _sendError( - // iq, - // 406, - // 'modify', - // 'not-acceptable', - // 'Form attribute can not be null', - // ); - // } else { - // final reply = iq.replyIQ(); - // return reply.sendIQ( - // callback: (iq) { - // base.transport.emit('userRegister', data: iq); - // }, - // ); - // } - // } - // return Future.value(); - // } - - Future _handleRegisterFeature() async { - if (base.features.contains('mechanisms')) { - return false; - } - - if (_createAccount && base.transport.eventHandled('register') > 0) { - await getRegistration( - callback: (iq) => - base.transport.emit('register', data: iq['form'] as Form), - ); - } - return Future.value(false); - } - - Future getRegistration({ - JabberID? jid, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) async { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom)..enable('register'); - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - // Future _sendForm(IQ iq) async { - // final reply = _makeRegistrationForm(iq); - // return reply.sendIQ(); - // } - - // IQ _makeRegistrationForm(IQ iq) { - // final register = iq['register'] as Register; - // Map? user = _getUser(iq); - - // if (user == null) { - // user = {}; - // } else { - // register['registered'] = true; - // } - - // register['instructions'] = _formInstructions; - - // for (final field in _formFields) { - // final data = user[field]; - // if (data != null) { - // register[field] = data; - // } else { - // register._addField(field); - // } - // } - - // final reply = iq.replyIQ(); - // reply.setPayload([register.element!]); - // return reply; - // } - - // void _sendError(IQ iq, int code, String errorType, String name, String text) { - // final reply = iq.replyIQ(); - // reply.setPayload([(iq['register'] as Register).element!]); - // reply.error(); - // final error = reply['error'] as StanzaError; - // error['code'] = code.toString(); - // error['type'] = errorType; - // error['condition'] = name; - // error['text'] = text; - // reply.send(); - // } - - @override - void sessionBind(String? jid) {} - - @override - void pluginEnd() => base.unregisterFeature(name, order: 50); -} diff --git a/lib/src/plugins/register/stanza.dart b/lib/src/plugins/register/stanza.dart deleted file mode 100644 index 79bf2a9..0000000 --- a/lib/src/plugins/register/stanza.dart +++ /dev/null @@ -1,149 +0,0 @@ -part of 'register.dart'; - -class Register extends XMLBase { - Register({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'query', - namespace: 'jabber:iq:register', - pluginAttribute: 'register', - interfaces: _interfaces, - subInterfaces: _interfaces, - ) { - addGetters({ - const Symbol('registered'): (args, base) => isRegistered, - const Symbol('remove'): (args, base) => isRemoved, - const Symbol('fields'): (args, base) => fields, - }); - - addSetters({ - const Symbol('fields'): (value, args, base) => - setFields(value as Set), - }); - - addDeleters({ - const Symbol('fields'): (args, base) => deleteFields(), - }); - - registerPlugin(Form()); - } - - bool get isRegistered { - final present = element!.getElement('registered', namespace: namespace); - return present != null; - } - - void setRegistered(bool value) { - if (value) { - return _addField('remove'); - } - return delete('remove'); - } - - bool get isRemoved { - final remove = element!.getElement('remove', namespace: namespace); - return remove != null; - } - - void _addField(String value) => setSubText(value, text: '', keep: true); - - Set get fields { - final fields = {}; - for (final field in _formFields) { - if (element!.getElement(field, namespace: namespace) != null) { - fields.add(field); - } - } - - return fields; - } - - void setFields(Set fields) { - delete('fields'); - for (final field in fields) { - setSubText(field, text: '', keep: true); - } - } - - void deleteFields() { - for (final field in _formFields) { - deleteSub(field); - } - } - - @override - Register copy({xml.XmlElement? element, XMLBase? parent}) => Register( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class RegisterFeature extends XMLBase { - RegisterFeature({super.element, super.parent}) - : super( - name: 'register', - namespace: 'http://jabber.org/features/iq-register', - pluginAttribute: 'register', - interfaces: {}, - ); - - @override - RegisterFeature copy({xml.XmlElement? element, XMLBase? parent}) => - RegisterFeature(element: element, parent: parent); -} - -const _formFields = { - 'username', - 'password', - 'email', - 'nick', - 'name', - 'first', - 'last', - 'address', - 'city', - 'state', - 'zip', - 'phone', - 'url', - 'date', - 'misc', - 'text', - 'key', -}; - -const _interfaces = { - 'username', - 'password', - 'email', - 'nick', - 'name', - 'first', - 'last', - 'address', - 'city', - 'state', - 'zip', - 'phone', - 'url', - 'date', - 'misc', - 'text', - 'key', - 'registered', - 'remove', - 'instructions', - 'fields', -}; diff --git a/lib/src/plugins/rosterver/rosterver.dart b/lib/src/plugins/rosterver/rosterver.dart deleted file mode 100644 index 1193dad..0000000 --- a/lib/src/plugins/rosterver/rosterver.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -part 'stanza.dart'; - -class FeatureRosterVersioning extends PluginBase { - FeatureRosterVersioning() - : super('rosterversioning', description: 'Roster Versioning'); - - @override - void pluginInitialize() => base.registerFeature( - 'rosterver', - (_) { - Log.instance.warning('Enabling roster versioning'); - return base.features.add('rosterver'); - }, - order: 9000, - ); - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - /// Do not implement. - @override - void pluginEnd() {} -} diff --git a/lib/src/plugins/rosterver/stanza.dart b/lib/src/plugins/rosterver/stanza.dart deleted file mode 100644 index 5f6300d..0000000 --- a/lib/src/plugins/rosterver/stanza.dart +++ /dev/null @@ -1,11 +0,0 @@ -part of 'rosterver.dart'; - -class RosterVersioning extends XMLBase { - RosterVersioning() - : super( - name: 'ver', - namespace: WhixpUtils.getNamespace('VER'), - interfaces: const {}, - pluginAttribute: 'rosterver', - ); -} diff --git a/lib/src/plugins/rsm/rsm.dart b/lib/src/plugins/rsm/rsm.dart deleted file mode 100644 index fdcea76..0000000 --- a/lib/src/plugins/rsm/rsm.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:async'; - -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/disco/disco.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -/// Result Set Management plugin -class RSM extends PluginBase { - RSM() - : super( - 'RSM', - description: 'XEP-0059: Result Set Management', - dependencies: {'disco'}, - ); - - late final ServiceDiscovery? _disco; - - /// Do not implement. - @override - void pluginInitialize() {} - - @override - void sessionBind(String? jid) { - _disco = base.getPluginInstance('disco'); - if (_disco != null) { - _disco.addFeature(RSMStanza().namespace); - } - } - - @override - void pluginEnd() => _disco?.removeFeature(RSMStanza().namespace); - - _ResultIterable iterate( - IQ stanza, - String interface, { - int amount = 10, - bool reverse = false, - String receiveInterface = '', - XMLBase? receiveInterfaceStanza, - T Function(IQ iq)? preCallback, - FutureOr Function(IQ iq)? postCallback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) => - _ResultIterable( - _ResultIterator( - stanza, - interface, - amount: amount, - reverse: reverse, - receiveInterface: - receiveInterface.isEmpty ? interface : receiveInterface, - preCallback: preCallback, - postCallback: postCallback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ), - ); -} - -/// An iterator or Result Set Management -class _ResultIterator { - _ResultIterator( - this._query, - this._interface, { - required int amount, - required bool reverse, - required String receiveInterface, - T Function(IQ iq)? preCallback, - FutureOr Function(IQ iq)? postCallback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) { - _amount = amount; - _reverse = reverse; - _receiveInterface = receiveInterface; - _preCallback = preCallback; - _postCallback = postCallback; - _failureCallback = failureCallback; - _timeoutCallback = timeoutCallback; - _timeout = timeout; - } - - bool _stop = false; - String? start; - - final IQ _query; - final String _interface; - late final int _amount; - late final bool _reverse; - late final String _receiveInterface; - late final T Function(IQ iq)? _preCallback; - late final FutureOr Function(IQ iq)? _postCallback; - FutureOr Function(StanzaError error)? _failureCallback; - FutureOr Function()? _timeoutCallback; - int _timeout = 10; -} - -class _ResultIterable extends Iterable> { - _ResultIterable(this.base); - - final _ResultIterator base; - - @override - Iterator> get iterator => _ResultIteratorBase(base); -} - -class _ResultIteratorBase implements Iterator> { - _ResultIteratorBase(this.iterator); - - final _ResultIterator iterator; - - @override - Future get current async { - iterator._query['id'] = WhixpUtils.getUniqueId('rsm'); - ((iterator._query[iterator._interface] as XMLBase)['rsm'] - as RSMStanza)['max'] = iterator._amount.toString(); - - if (iterator.start != null) { - if (iterator._reverse) { - ((iterator._query[iterator._interface] as XMLBase)['rsm'] - as RSMStanza)['before'] = iterator.start; - } else { - ((iterator._query[iterator._interface] as XMLBase)['rsm'] - as RSMStanza)['after'] = iterator.start; - } - } else if (iterator._reverse) { - ((iterator._query[iterator._interface] as XMLBase)['rsm'] - as RSMStanza)['before'] = true; - } - - try { - if (iterator._preCallback != null) { - iterator._preCallback?.call(iterator._query); - } - IQ? stanza; - - await iterator._query.sendIQ( - callback: (iq) async { - final received = iq[iterator._receiveInterface] as XMLBase; - - if ((received['rsm'] as RSMStanza)['first'] == null && - (received['rsm'] as RSMStanza)['last'] == null) { - throw Exception('Stop stanza iteration: $iq'); - } - - if (iterator._postCallback != null) { - await iterator._postCallback!.call(iq); - } - - if ((received['rsm'] as RSMStanza)['count'] != null && - (received['rsm'] as RSMStanza)['first_index'] != null) { - final count = int.parse( - (received['rsm'] as RSMStanza)['count'] as String, - ); - final first = int.parse((received['rsm'] as RSMStanza).firstIndex); - - final numberItems = - (received['substanzas'] as List).length; - if (first + numberItems == count) { - iterator._stop = true; - } - } - - if (iterator._reverse) { - iterator.start = (received['rsm'] as RSMStanza)['first'] as String; - } else { - iterator.start = (received['rsm'] as RSMStanza)['last'] as String; - } - - stanza = iq; - }, - failureCallback: iterator._failureCallback, - timeoutCallback: iterator._timeoutCallback, - timeout: iterator._timeout, - ); - - return Future.value(stanza); - } on Exception { - iterator._stop = true; - return null; - } - } - - @override - bool moveNext() { - if (iterator._stop) { - return false; - } - return true; - } -} diff --git a/lib/src/plugins/rsm/stanza.dart b/lib/src/plugins/rsm/stanza.dart deleted file mode 100644 index 669b2c8..0000000 --- a/lib/src/plugins/rsm/stanza.dart +++ /dev/null @@ -1,143 +0,0 @@ -part of 'rsm.dart'; - -/// XEP-0059 (Result Set Management) can be used to handle query results. -/// -/// Limiting the quantity of things per answer, for example, or starting at -/// specific points. -/// -/// ### Example: -/// ```xml -/// -/// -/// Pete -/// -/// 10 -/// -/// -/// -/// -/// returns a limited result set -/// -/// -/// -/// -/// Peter -/// Saint-Andre -/// Pete -/// -/// . -/// [8 more items] -/// . -/// -/// Peter -/// Pan -/// Pete -/// -/// -/// -/// ``` -/// -/// see -class RSMStanza extends XMLBase { - /// Creates [RSMStanza] stanza with optional parameters. - /// - /// ### interfaces: - /// - /// __first_index__ is the attribute of __first__ - ///
__after__ is the ID defining from which item to start - ///
__before__ is the ID defining from which item to start when browsing - /// backwards - ///
__max__ is the max amount per response - ///
__first__ is ID for the first item in the response - ///
__last__ is ID for the last item in the response - ///
__index__ is used to set an index to start from - ///
__count__ is the number of remote items available - RSMStanza({super.element, super.parent}) - : super( - name: 'set', - namespace: WhixpUtils.getNamespace('RSM'), - includeNamespace: true, - pluginAttribute: 'rsm', - interfaces: { - 'first_index', - 'first', - 'after', - 'before', - 'count', - 'index', - 'last', - 'max', - }, - subInterfaces: { - 'first', - 'after', - 'before', - 'count', - 'index', - 'last', - 'max', - }, - ); - - /// Returns the value of the `index` attribute for __first__. - String get firstIndex { - final first = element!.getElement('first', namespace: namespace); - if (first != null) { - return first.getAttribute('index') ?? ''; - } - return ''; - } - - /// Sets the `index` attribute for __first__ and creates the element if it - /// does not exist. - void setFirstIndex(String index) { - final first = element!.getElement('first', namespace: namespace); - if (first != null) { - if (index.isNotEmpty) { - first.setAttribute('index', index); - } else if (first.getAttribute('index') != null) { - first.removeAttribute('index'); - } - } else if (index.isNotEmpty) { - final first = WhixpUtils.xmlElement('first'); - first.setAttribute('index', index); - element!.children.add(first); - } - } - - /// Removes the `index` attribute for __first__, but keeps the element. - void deleteFirstIndex() { - final first = element!.getElement('first', namespace: namespace); - if (first != null) { - first.removeAttribute('index', namespace: namespace); - } - } - - /// Sets the [value] of __before__, if the [value] is `true`, then the element - /// will be created without a [value]. - void setBefore(dynamic value) { - final before = element!.getElement('before', namespace: namespace); - if (before == null && value == true) { - setSubText('{$namespace}before', text: '', keep: true); - } else if (value is String) { - setSubText('{$namespace}before', text: value); - } - } - - /// Returns the value of __before__, if it is empty it will return `true`. - dynamic get before { - final before = element!.getElement('before', namespace: namespace); - if (before != null && before.innerText.isEmpty) { - return true; - } else if (before != null) { - return before.innerText; - } - return null; - } - - @override - RSMStanza copy({xml.XmlElement? element, XMLBase? parent}) => RSMStanza( - element: element, - parent: parent, - ); -} diff --git a/lib/src/plugins/session/session.dart b/lib/src/plugins/session/session.dart deleted file mode 100644 index c1901d2..0000000 --- a/lib/src/plugins/session/session.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:meta/meta.dart'; - -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -class FeatureSession extends PluginBase { - FeatureSession() : super('session', description: 'Start Session'); - - late final IQ _iq; - - @override - void pluginInitialize() { - final session = Session(); - _iq = IQ(transport: base.transport); - - base.registerFeature('session', _handleSessionStart, order: 10001); - - _iq.registerPlugin(session); - } - - Future _handleSessionStart(StanzaBase features) async { - if ((features['session'] as XMLBase)['optional'] as bool) { - base.transport.sessionStarted = true; - base.transport.emit('sessionStart'); - return; - } - - _iq['type'] = 'set'; - _iq.enable('session'); - await _iq.sendIQ(callback: _onStartSession); - } - - void _onStartSession(StanzaBase response) { - base.features.add('session'); - - base.transport.sessionStarted = true; - base.transport.emit('sessionStart'); - } - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} -} diff --git a/lib/src/plugins/session/stanza.dart b/lib/src/plugins/session/stanza.dart deleted file mode 100644 index 80bdbeb..0000000 --- a/lib/src/plugins/session/stanza.dart +++ /dev/null @@ -1,47 +0,0 @@ -part of 'session.dart'; - -@internal -class Session extends XMLBase { - Session() - : super( - name: 'session', - namespace: WhixpUtils.getNamespace('SESSION'), - interfaces: {'optional'}, - pluginAttribute: 'session', - ) { - addGetters( - { - const Symbol('optional'): (args, base) { - if (base.element!.getAttribute('xmlns') == namespace) { - return base.element!.getElement('optional') != null; - } - return false; - }, - }, - ); - - addSetters( - { - const Symbol('optional'): (value, args, base) { - if (value != null) { - final optional = xml.XmlElement(xml.XmlName('optional')); - base.element!.children.add(optional); - } else { - delete('optional'); - } - }, - }, - ); - - addDeleters( - { - const Symbol('optional'): (args, base) { - if (base.element!.getAttribute('xmlns') == namespace) { - final optional = base.element!.getElement('optional'); - base.element!.children.remove(optional); - } - }, - }, - ); - } -} diff --git a/lib/src/plugins/sm/sm.dart b/lib/src/plugins/sm/sm.dart deleted file mode 100644 index e38ce53..0000000 --- a/lib/src/plugins/sm/sm.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'dart:async'; -import 'dart:math' as math; - -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/handler/waiter.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/message.dart'; -import 'package:whixp/src/stanza/presence.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; -import 'package:whixp/src/transport.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -final _maxSeq = math.pow(2, 32).toInt(); - -/// Stream management implements these features using short XML elements at the -/// root stream level. -/// -/// These elements are not "stanzas" in the XMPP sense (i.e., not , -/// , or stanzas as defined in RFC 6120) and are not -/// counted or acked in stream management, since they exist for the purpose of -/// managing stanzas themselves. -class StreamManagement extends PluginBase { - /// Example: - /// ```xml - /// - /// - /// - /// - /// ``` - StreamManagement({ - this.lastAck = 0, - this.window = 5, - this.smID, - this.handled = 0, - this.seq = 0, - this.allowResume = true, - }) : super('sm', description: 'Stream Management'); - - /// The last ack number received from the server. - int lastAck; - - /// The number of stanzas to wait between sending ack requests to the server. - /// - /// Setting this to `1` will send an ack request after every sent stanza. - /// Defaults to `5`. - int window; - - /// The stream management ID for the stream. Knowing this value is required - /// in order to do stream resumption. - late String? smID; - - /// A counter of handled incoming stanzas, mod 2^32. - int handled; - - /// A counter of unacked outgoing stanzas, mod 2^32. - int seq; - - /// Control whether or not the ability to resume the stream will be requested - /// when enabling stream management. Defaults to `true`. - final bool allowResume; - - late final Map _unackedQueue; - - late int _windowCounter; - late bool enabledIn; - late bool enabledOut; - - @override - void pluginInitialize() { - if (base.isComponent) return; - - _windowCounter = window; - - enabledIn = false; - enabledOut = false; - _unackedQueue = {}; - - base - ..registerFeature('sm', _handleStreamFeature, restart: true, order: 10100) - ..registerFeature('sm', _handleStreamFeature, restart: true, order: 9000); - - base.transport - ..registerHandler( - CallbackHandler( - 'Stream Management Enabled', - (stanza) => _handleEnabled(stanza as Enabled), - matcher: XPathMatcher(Enabled().tag), - ), - ) - ..registerHandler( - CallbackHandler( - 'Stream Management Resumed', - _handleResumed, - matcher: XPathMatcher(Resumed().tag), - ), - ) - ..registerHandler( - CallbackHandler( - 'Stream Management Ack', - _handleAck, - matcher: XPathMatcher(Ack().tag), - ), - ) - ..registerHandler( - CallbackHandler( - 'Stream Management Failed', - (stanza) => _handleFailed(stanza as Failed), - matcher: XPathMatcher(Failed().tag), - ), - ) - ..registerHandler( - CallbackHandler( - 'Stream Management Request Ack', - _handleRequestAck, - matcher: XPathMatcher(RequestAck().tag), - ), - ); - - base.transport - ..registerStanza(Enable()) - ..registerStanza(Enabled()) - ..registerStanza(Resume()) - ..registerStanza(Resumed()) - ..registerStanza(Failed()) - ..registerStanza(Ack()) - ..registerStanza(RequestAck()) - ..addFilter(filter: _handleIncoming) - ..addFilter(mode: FilterMode.out, filter: _handleOutgoing); - - base - ..addEventHandler('disconnected', _disconnected) - ..addEventHandler('sessionEnd', _sessionEnd); - } - - /// Requests an ack from the server. - void _requestAck() { - Log.instance.debug('Requesting ack'); - final req = RequestAck(); - base.transport.sendRaw(req.toString()); - } - - /// Resets enabled state until we can resume/reenable. - void _disconnected(String? event) { - Log.instance.debug('disconnected, disabling SM'); - base.transport.emit('smDisabled', data: event); - enabledIn = false; - enabledOut = false; - } - - /// Resets stream management state. - void _sessionEnd(_) { - Log.instance.debug('session ended, disabling SM'); - base.transport.emit('smDisabled'); - enabledIn = false; - enabledOut = false; - smID = null; - handled = 0; - seq = 0; - lastAck = 0; - } - - /// Enables or resumes stream management. - /// - /// If no SM-ID is stored, and resource binding has taken place, stream - /// management will be enabled. - /// - /// If an SM-ID is known, and the server allows resumption, the previous - /// stream will be resumed. - Future _handleStreamFeature(StanzaBase features) async { - if (base.features.contains('stream_management')) { - return false; - } - - if (smID != null && allowResume && !base.features.contains('bind')) { - final resume = Resume(transport: base.transport); - resume['h'] = handled; - resume['previd'] = smID; - resume.send(); - Log.instance.info('resuming SM'); - - final waiter = Waiter( - 'resumedOrFailed', - matcher: ManyMatcher([ - XPathMatcher(Resumed().tag), - XPathMatcher(Failed().tag), - ]), - transport: base.transport, - ); - - base.transport.registerHandler(waiter); - - final result = await waiter.wait(timeout: 2); - - if (result != null && result.name == 'resumed') { - return true; - } - await base.transport.emit('sessionEnd'); - } - if (base.features.contains('bind')) { - final enable = Enable(transport: base.transport); - enable['resume'] = allowResume; - enable.send(); - Log.instance.info('enabling SM'); - - final waiter = Waiter( - 'enabledOrFailed', - matcher: ManyMatcher([ - XPathMatcher(Enabled().tag), - XPathMatcher(Failed().tag), - ]), - transport: base.transport, - ); - - base.transport.registerHandler(waiter); - await waiter.wait(timeout: 2); - } - - return false; - } - - StanzaBase _handleIncoming(StanzaBase stanza) { - if (!enabledIn) { - return stanza; - } - - if (stanza is Message || stanza is Presence || stanza is IQ) { - handled = (handled + 1) % _maxSeq; - } - - return stanza; - } - - /// Stores outgoing stanzas in a queue to be acked. - StanzaBase _handleOutgoing(StanzaBase stanza) { - if (stanza is Enable || stanza is Resume) { - enabledOut = true; - _unackedQueue.clear(); - Log.instance.debug('enabling outoing SM: $stanza'); - } - - if (!enabledOut) { - return stanza; - } - - if (stanza is Message || stanza is Presence || stanza is IQ) { - int? seq; - seq = (this.seq + 1) % _maxSeq; - seq = this.seq; - - _unackedQueue[seq] = stanza; - _windowCounter -= 1; - if (_windowCounter == 0) { - _windowCounter = window; - _requestAck(); - } - } - - return stanza; - } - - /// Saves the SM-ID, if provided. - void _handleEnabled(Enabled enabled) { - base.features.add('stream_management'); - if (enabled['id'] != null) { - smID = enabled['id'] as String; - } - enabledIn = true; - handled = 0; - base.transport.emit('smEnabled', data: enabled); - base.transport.endSessionOnDisconnect = false; - } - - void _handleResumed(StanzaBase stanza) { - base.features.add('stream_management'); - enabledIn = true; - _handleAck(stanza); - for (final entry in _unackedQueue.entries) { - base.transport.send(entry.value, useFilters: false); - } - base.transport.emit('sessionResumed', data: stanza); - base.transport.endSessionOnDisconnect = false; - } - - /// Disabled and resets any features used since stream management was - /// requested. - void _handleFailed(Failed failed) { - enabledIn = false; - enabledOut = false; - _unackedQueue.clear(); - base.transport.emit('smFailed', data: failed); - } - - /// Sends the current ack count to the server. - void _handleRequestAck(StanzaBase stanza) { - final ack = Ack(); - ack['h'] = handled; - base.transport.sendRaw(ack.toString()); - } - - /// Processes a server ack by freeing acked stanzas from the queue. - void _handleAck(StanzaBase stanza) { - if (stanza['h'] == lastAck) { - return; - } - - int numAcked = ((stanza['h'] as int) - lastAck) % _maxSeq; - final numUnacked = _unackedQueue.length; - Log.instance.debug( - 'Ack: ${stanza['h']}, Last ack: $lastAck, Unacked: $numUnacked, Num acked: $numAcked, Remaining: ${numUnacked - numAcked}', - ); - - if ((numAcked > _unackedQueue.length) || numAcked < 0) { - Log.instance.error( - 'Inconsistent sequence numbers from the server, ignoring and replacing ours with them', - ); - numAcked = _unackedQueue.length; - } - for (int i = 0; i < numAcked; i++) { - final entries = _unackedQueue.entries; - final seq = entries.last.key; - final stanza = entries.last.value; - _unackedQueue.remove(seq); - base.transport.emit('stanzaAcked', data: stanza); - } - lastAck = stanza['h'] as int; - } - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - @override - void pluginEnd() { - if (base.isComponent) { - return; - } - - // base - // ..unregisterFeature('sm', order: 10100) - // ..unregisterFeature('sm', order: 9000); - // base.transport - // ..removeEventHandler('disconnected', handler: _disconnected) - // ..removeEventHandler('sessionEnd', handler: _sessionEnd) - // ..removeFilter(filter: _handleIncoming) - // ..removeFilter(filter: _handleOutgoing) - // ..removeHandler('Stream Management Enabled') - // ..removeHandler('Stream Management Resumed') - // ..removeHandler('Stream Management Failed') - // ..removeHandler('Stream Management Ack') - // ..removeHandler('Stream Management Request Ack') - // ..removeStanza(Enable()) - // ..removeStanza(Enabled()) - // ..removeStanza(Resume()) - // ..removeStanza(Resumed()) - // ..removeStanza(Ack()) - // ..removeStanza(RequestAck()); - } -} diff --git a/lib/src/plugins/sm/stanza.dart b/lib/src/plugins/sm/stanza.dart deleted file mode 100644 index a2835c1..0000000 --- a/lib/src/plugins/sm/stanza.dart +++ /dev/null @@ -1,317 +0,0 @@ -part of 'sm.dart'; - -class Enable extends StanzaBase { - Enable({ - super.getters, - super.setters, - super.element, - super.parent, - super.transport, - }) : super( - name: 'enable', - namespace: 'urn:xmpp:sm:3', - interfaces: {'max', 'resume'}, - ) { - addGetters({ - const Symbol('resume'): (args, base) => resume, - }); - - addSetters({ - const Symbol('resume'): (value, args, base) => resume = value as bool, - }); - } - - bool get resume => - {'true', '1'}.contains(getAttribute('resume', 'false').toLowerCase()); - - set resume(bool value) { - deleteAttribute('resume'); - setAttribute('resume', value ? 'true' : 'false'); - } - - @override - Enable copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Enable( - getters: getters, - setters: setters, - element: element, - parent: parent, - transport: transport, - ); -} - -class Enabled extends StanzaBase { - Enabled({super.getters, super.setters, super.element, super.parent}) - : super( - name: 'enabled', - namespace: 'urn:xmpp:sm:3', - interfaces: {'id', 'location', 'max', 'resume'}, - ) { - addGetters({ - const Symbol('resume'): (args, base) => resume, - }); - - addSetters({ - const Symbol('resume'): (value, args, base) => resume = value as bool, - }); - } - - bool get resume => - {'true', '1'}.contains(getAttribute('resume', 'false').toLowerCase()); - - set resume(bool value) { - deleteAttribute('resume'); - setAttribute('resume', value ? 'true' : 'false'); - } - - @override - Enabled copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Enabled( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class Resume extends StanzaBase { - Resume({ - super.getters, - super.setters, - super.element, - super.parent, - super.transport, - }) : super( - name: 'resume', - namespace: 'urn:xmpp:sm:3', - interfaces: {'h', 'previd'}, - ) { - addGetters({ - const Symbol('h'): (args, base) => h, - }); - - addSetters({ - const Symbol('h'): (value, args, base) => h = value as int, - }); - } - - int? get h { - final h = getAttribute('h'); - if (h.isNotEmpty) { - return int.parse(h); - } - return null; - } - - set h(int? value) => setAttribute('h', value.toString()); - - @override - Resume copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Resume( - getters: getters, - setters: setters, - element: element, - parent: parent, - transport: transport, - ); -} - -class Resumed extends StanzaBase { - Resumed({super.getters, super.setters, super.element, super.parent}) - : super( - name: 'resumed', - namespace: 'urn:xmpp:sm:3', - interfaces: {'h', 'previd'}, - ) { - addGetters({ - const Symbol('h'): (args, base) => h, - }); - - addSetters({ - const Symbol('h'): (value, args, base) => h = value as int, - }); - } - - int? get h { - final h = getAttribute('h'); - if (h.isNotEmpty) { - return int.parse(h); - } - return null; - } - - set h(int? value) => setAttribute('h', value.toString()); - - @override - Resumed copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Resumed( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class Failed extends StanzaBase { - Failed({super.element, super.parent}) - : super( - name: 'failed', - namespace: 'urn:xmpp:sm:3', - interfaces: {}, - ); - - @override - Failed copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Failed(element: element, parent: parent); -} - -class RequestAck extends StanzaBase { - RequestAck({super.element, super.parent}) - : super(name: 'r', namespace: 'urn:xmpp:sm:3', interfaces: {}); - - @override - RequestAck copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - RequestAck(element: element, parent: parent); -} - -class Ack extends StanzaBase { - Ack({super.getters, super.setters, super.element, super.parent}) - : super( - name: 'a', - namespace: 'urn:xmpp:sm:3', - interfaces: {'h'}, - ) { - addGetters({ - const Symbol('h'): (args, base) => h, - }); - - addSetters({ - const Symbol('h'): (value, args, base) => h = value as int, - }); - } - - int? get h { - final h = getAttribute('h'); - if (h.isNotEmpty) { - return int.parse(h); - } - return null; - } - - set h(int? value) => setAttribute('h', value.toString()); - - @override - Ack copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Ack( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class StreamManagementStanza extends StanzaBase { - StreamManagementStanza({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'sm', - namespace: 'urn:xmpp:sm:3', - pluginAttribute: 'sm', - interfaces: {'required', 'optional'}, - ) { - addGetters({ - const Symbol('required'): (args, base) => required, - const Symbol('optional'): (args, base) => optional, - }); - - addSetters({ - const Symbol('required'): (value, args, base) => required = value as bool, - const Symbol('optional'): (value, args, base) => optional = value as bool, - }); - - addDeleters({ - const Symbol('required'): (args, base) => deleteRequired(), - const Symbol('optional'): (args, base) => deleteOptional(), - }); - } - - bool get required => - element!.getElement('required', namespace: namespace) != null; - - set required(bool value) { - delete('required'); - if (value) { - setSubText('required', text: '', keep: true); - } - } - - void deleteRequired() => deleteSub('required'); - - bool get optional => - element!.getElement('optional', namespace: namespace) != null; - - set optional(bool value) { - delete('optional'); - if (value) { - setSubText('optional', text: '', keep: true); - } - } - - void deleteOptional() => deleteSub('optional'); - - @override - StreamManagementStanza copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - StreamManagementStanza( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -extension XMLElementTag on xml.XmlElement { - String get tag => '{${getAttribute('xmlns')}}${name.qualified}'; -} diff --git a/lib/src/plugins/starttls/stanza.dart b/lib/src/plugins/starttls/stanza.dart deleted file mode 100644 index 3cbaa10..0000000 --- a/lib/src/plugins/starttls/stanza.dart +++ /dev/null @@ -1,47 +0,0 @@ -part of 'starttls.dart'; - -/// ```xml -/// -/// ``` -@internal -class StartTLS extends StanzaBase { - StartTLS() - : super( - name: 'starttls', - namespace: WhixpUtils.getNamespace('STARTTLS'), - interfaces: {'required'}, - pluginAttribute: 'starttls', - getters: { - const Symbol('required'): (args, base) => true, - }, - ); -} - -/// ```xml -/// -/// ``` -@internal -class Proceed extends StanzaBase { - Proceed() - : super( - name: 'proceed', - namespace: WhixpUtils.getNamespace('STARTTLS'), - interfaces: const {}, - ); - - @override - void exception(dynamic excp) => throw excp as Exception; -} - -/// ```xml -/// -/// ``` -@internal -class Failure extends StanzaBase { - Failure() - : super( - name: 'failure', - namespace: WhixpUtils.getNamespace('STARTTLS'), - interfaces: const {}, - ); -} diff --git a/lib/src/plugins/starttls/starttls.dart b/lib/src/plugins/starttls/starttls.dart deleted file mode 100644 index 6dccfea..0000000 --- a/lib/src/plugins/starttls/starttls.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:meta/meta.dart'; - -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; -import 'package:whixp/src/utils/utils.dart'; - -part 'stanza.dart'; - -class FeatureStartTLS extends PluginBase { - FeatureStartTLS() - : super( - 'starttls', - description: 'Stream Feature: STARTTLS', - ); - - @override - void pluginInitialize() { - final proceed = Proceed(); - final failure = Failure(); - - base.transport.registerHandler( - FutureCallbackHandler( - 'STARTTLS Proceed', - (_) => _handleStartTLSProceed(), - matcher: XPathMatcher(proceed.tag), - ), - ); - base.registerFeature('starttls', _handleStartTLS, restart: true, order: 0); - base.transport.registerStanza(proceed); - base.transport.registerStanza(failure); - } - - /// Handle notification that the server supports TLS. - bool _handleStartTLS(StanzaBase features) { - final stanza = StartTLS(); - - if (base.features.contains('starttls')) { - return false; - } else if (base.transport.disableStartTLS) { - return false; - } else { - base.transport.send(stanza); - return true; - } - } - - /// Restart the XML stream when TLS is accepted. - Future _handleStartTLSProceed() async { - if (await base.transport.startTLS()) { - base.features.add('starttls'); - } - } - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} -} diff --git a/lib/src/stanza/root.dart b/lib/src/stanza/root.dart deleted file mode 100644 index 23a6009..0000000 --- a/lib/src/stanza/root.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:whixp/src/exception.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; - -/// Top-level stanza in a Transport. -/// -/// Provides a more XMPP specific exception handler than the provided by the -/// generic [StanzaBase] class. -abstract class RootStanza extends StanzaBase { - /// All parameters are extended from [StanzaBase]. For more information please - /// take a look at [StanzaBase]. - RootStanza({ - super.stanzaType, - super.stanzaTo, - super.stanzaFrom, - super.stanzaID, - super.name, - super.namespace, - super.transport, - super.interfaces, - super.subInterfaces, - super.languageInterfaces, - super.receive, - super.includeNamespace, - super.types, - super.getters, - super.setters, - super.deleters, - super.pluginAttribute, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginMultiAttribute, - super.pluginIterables, - super.overrides, - super.isExtension, - super.boolInterfaces, - super.element, - super.parent, - }); - - /// Create and send an error reply. - /// - /// Typically called when an event handler raises an exception. - /// - /// The error's type and text content are based on the exception object's - /// type and content. - @override - void exception(dynamic excp) { - if (excp is StanzaException) { - if (excp.message == 'IQ error has occured') { - final stanza = (this as IQ).replyIQ(); - stanza.transport = transport; - final error = stanza['error'] as XMLBase; - error['condition'] = 'undefined-condition'; - error['text'] = 'External error'; - error['type'] = 'cancel'; - stanza.send(); - } else if (excp.condition == 'remote-server-timeout') { - final stanza = reply(copiedStanza: copy()); - stanza.enable('error'); - final error = stanza['error'] as XMLBase; - error['condition'] = 'remote-server-timeout'; - error['type'] = 'wait'; - stanza.send(); - } else { - final id = this['id']; - final stanza = reply(copiedStanza: copy()); - stanza.enable('error'); - stanza['id'] = id; - final error = stanza['error'] as XMLBase; - error['condition'] = 'undefined-condition'; - error['text'] = 'Whixp error occured'; - error['type'] = 'cancel'; - stanza.send(); - } - } - } -} diff --git a/lib/src/stream/base.dart b/lib/src/stream/base.dart deleted file mode 100644 index 237b451..0000000 --- a/lib/src/stream/base.dart +++ /dev/null @@ -1,1660 +0,0 @@ -import 'package:dartz/dartz.dart'; - -import 'package:meta/meta.dart'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/transport.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; -import 'package:xpath_selector_xml_parser/xpath_selector_xml_parser.dart'; - -part 'stanza.dart'; - -/// Gets two parameter; The first one is usually refers to language of the -/// stanza. The second is for processing over current stanza and keeps [XMLBase] -/// instance. -typedef _GetterOrDeleter = dynamic Function(dynamic args, XMLBase base); - -/// Gets three params; The first one is usually refers to language of the -/// current stanza. The second one is the parameter to set. The third one is -/// [XMLBase] and refers to current stanza instance. -typedef _Setter = void Function(dynamic args, dynamic value, XMLBase base); - -/// Applies the stanza's namespace to elements in an [xPath] expression. -/// -/// [split] indicates if the fixed XPath should be left as a list of element -/// names of element names with namespaces. Defaults to `false`. -/// -/// [propogateNamespace] overrides propagating parent element namespaces to -/// child elements. Useful if you wish to simply split an XPath that has -/// non-specified namepsaces, adnd child and parent namespaces are konwn not to -/// always match. Defaults to `true`. -Tuple2?> fixNamespace( - String xPath, { - bool split = false, - bool propogateNamespace = true, - String defaultNamespace = '', -}) { - final fixed = []; - - final namespaceBlocks = xPath.split('{'); - for (final block in namespaceBlocks) { - late String namespace; - late List elements; - if (block.contains('}')) { - final namespaceBlockSplit = block.split('}'); - namespace = namespaceBlockSplit[0]; - elements = namespaceBlockSplit[1].split('/'); - } else { - namespace = defaultNamespace; - elements = block.split('/'); - } - - for (final element in elements) { - late String tag; - if (element.isNotEmpty) { - if (propogateNamespace && element[0] != '*') { - tag = '<$element xmlns="$namespace"/>'; - } else { - tag = element; - } - fixed.add(tag); - } - } - } - - if (split) { - return Tuple2(null, fixed); - } - return Tuple2(fixed.join('/'), null); -} - -/// Associates a [stanza] object as a plugin for another stanza. -/// -/// [plugin] stanzas marked as iterable will be included in the list of -/// substanzas for the parent, using `parent['subsstanzas']`. If the attribute -/// `pluginMultiAttribute` was defined for the plugin, then the substanza set -/// can be filtered to only instances of the plugin class. -/// -/// For instance, given a plugin class `Foo` with -/// `pluginMultiAttribute = 'foos'` then: -/// parent['foos'] -/// would return a collection of all `Foo` substanzas. -void registerStanzaPlugin( - XMLBase stanza, - XMLBase plugin, { - /// Indicates if the plugin stanza should be included in the parent stanza's - /// iterable [substanzas] interface results. - bool iterable = false, - - /// Indicates if the plugin should be allowed to override the interface - /// handlers for the parent stanza, based on the plugin's [overrides] field. - bool overrides = false, -}) { - final tag = '{${plugin.namespace}}${plugin.name}'; - - stanza.pluginAttributeMapping[plugin.pluginAttribute] = plugin; - stanza.pluginTagMapping[tag] = plugin; - - if (iterable) { - stanza.pluginIterables.add(plugin); - if (plugin.pluginMultiAttribute != null && - plugin.pluginMultiAttribute!.isNotEmpty) { - final multiplugin = multifactory(plugin, plugin.pluginMultiAttribute!); - registerStanzaPlugin(stanza, multiplugin); - } - } - if (overrides) { - for (final interface in plugin.overrides) { - stanza.pluginOverrides[interface] = plugin.pluginAttribute; - } - } -} - -/// Returns a [XMLBase] class for handling reoccuring child stanzas. -XMLBase multifactory(XMLBase stanza, String pluginAttribute) { - final multistanza = _Multi( - stanza.runtimeType, - pluginAttribute: pluginAttribute, - interfaces: {pluginAttribute}, - languageInterfaces: {pluginAttribute}, - isExtension: true, - ); - - multistanza - ..addGetters({ - Symbol(pluginAttribute): (args, base) => - multistanza.getMulti(base, args as String?), - }) - ..addSetters({ - Symbol(pluginAttribute): (value, args, base) => - multistanza.setMulti(base, value as List, args as String?), - }) - ..addDeleters({ - Symbol(pluginAttribute): (args, base) => - multistanza.deleteMulti(base, args as String?), - }); - - return multistanza; -} - -typedef _MultiFilter = bool Function(XMLBase); - -/// Multifactory class. Responsible to create an stanza with provided -/// substanzas. -class _Multi extends XMLBase { - _Multi( - this._multistanza, { - super.getters, - super.setters, - super.deleters, - super.pluginAttribute, - super.interfaces, - super.languageInterfaces, - super.isExtension, - super.element, - super.parent, - }); - late final Type _multistanza; - - List getMulti(XMLBase base, [String? lang]) { - final parent = failWithoutParent(base); - final iterable = _XMLBaseIterable(parent); - - final result = (lang == null || lang.isEmpty) || lang == '*' - ? iterable.where(pluginFilter()) - : iterable.where(pluginLanguageFilter(lang)); - - return result.toList(); - } - - @override - bool setup([xml.XmlElement? element]) { - this.element = WhixpUtils.xmlElement(''); - return false; - } - - void setMulti(XMLBase base, List value, [String? language]) { - final parent = failWithoutParent(base); - _deleters[Symbol(pluginAttribute)]?.call(language, base); - for (final sub in value) { - parent.add(sub as XMLBase); - } - } - - XMLBase failWithoutParent(XMLBase base) { - XMLBase? parent; - if (base.parent != null) { - parent = base.parent; - } - if (parent == null) { - throw ArgumentError('No stanza parent for multifactory'); - } - - return parent; - } - - void deleteMulti(XMLBase base, [String? language]) { - final parent = failWithoutParent(base); - final iterable = _XMLBaseIterable(parent); - final result = (language == null || language.isEmpty) || language == '*' - ? iterable.where(pluginFilter()).toList() - : iterable.where(pluginLanguageFilter(language)).toList(); - - if (result.isEmpty) { - parent._plugins.remove(Tuple2(pluginAttribute, '')); - parent._loadedPlugins.remove(pluginAttribute); - - parent.element!.children.remove(element); - } else { - while (result.isNotEmpty) { - final stanza = result.removeLast(); - parent.iterables.remove(stanza); - parent.element!.children.remove(stanza.element); - } - } - } - - _MultiFilter pluginFilter() => (x) => x.runtimeType == _multistanza; - - _MultiFilter pluginLanguageFilter(String? language) => - (x) => x.runtimeType == _multistanza && x['lang'] == language; - - @override - _Multi copy({xml.XmlElement? element, XMLBase? parent}) => _Multi( - _multistanza, - getters: getters, - setters: setters, - deleters: deleters, - pluginAttribute: pluginAttribute, - interfaces: interfaces, - languageInterfaces: languageInterfaces, - isExtension: isExtension, - element: element, - parent: parent, - ); -} - -class _XMLBaseIterable extends Iterable { - _XMLBaseIterable(this.parent); - final XMLBase parent; - - @override - Iterator get iterator => _XMLBaseIterator(parent); -} - -class _XMLBaseIterator implements Iterator { - final XMLBase parent; - - _XMLBaseIterator(this.parent); - - @override - XMLBase get current { - return parent.iterables[parent._index - 1]; - } - - @override - bool moveNext() { - if (parent._index >= parent.iterables.length) { - parent._index = 0; - return false; - } else { - parent._incrementIndex.call(); - return true; - } - } -} - -/// ## XMLBase -/// -/// Designed for efficient manipulation of XML elements. Provides a set of -/// methods and functionalities to create, modify, and interact with XML -/// structures. -/// -/// Serves as a base and flexible XML manipulation tool for this package, -/// designed to simplify the creation, modification, and interaction with -/// XML. It offers a comprehensive set of features for constructing XML -/// elements, managing attributes, handling child elements, and incorporating -/// dynamic plugins to extend functionality. -/// -/// [XMLBase] is created to accommodate a wide range of XML manipulation tasks. -/// Whether you are creating simple XML elements or dealing with complex -/// structures, this class provides a versatile foundation. -/// -/// The extensibility of [XMLBase] is the key strength. You can integrate and -/// manage plugins, which may include extensions or custom functionality. -/// -/// Stanzas are defined by their name, namespace, and interfaces. For example, -/// a simplistic Message stanza could be defined as: -/// ```dart -/// class Message extends XMLBase { -/// Message() -/// : super( -/// name: 'message', -/// interfaces: {'to', 'from', 'type', 'body'}, -/// subInterfaces: {'body'}, -/// ); -/// } -/// ``` -/// -/// The resulting Message stanza's content may be accessed as so: -/// ```dart -/// message['to'] = 'vsevex@example.com'; -/// message['body'] = 'salam'; -/// log(message['body']); /// outputs 'salam' -/// message.delete('body'); -/// log(message['body']); /// will output empty string -/// ``` -/// -/// ### Example: -/// ```dart -/// class TestStanza extends XMLBase { -/// TestStanza({ -/// super.name, -/// super.namespace, -/// super.pluginAttribute, -/// super.pluginMultiAttribute, -/// super.overrides, -/// super.interfaces, -/// super.subInterfaces, -/// }); -/// } -/// -/// void main() { -/// final stanza = TestStanza({name: 'stanza'}); -/// /// ...do whatever manipulation you want -/// stanza['test'] = 'salam'; -/// /// will add a 'test' child and assign 'salam' text to it -/// } -/// ``` -/// -/// Extending stanzas through the use of plugins (simple stanza that has -/// pluginAttribute value) is like the following: -/// ```dart -/// class MessagePlugin extends XMLBase { -/// MessagePlugin() -/// : super( -/// name: 'message', -/// interfaces: {'cart', 'hert'}, -/// pluginAttribute: 'custom', -/// ); -/// } -/// ``` -/// -/// The plugin stanza class myst be associated with its intended container -/// stanza by using [registerStanzaPlugin] method: -/// ```dart -/// final plugin = MessagePlugin(); -/// message.registerPlugin(plugin); -/// ``` -/// -/// The plugin may then be accessed as if it were built-in to the parent stanza: -/// ```dart -/// message['custom']['cart'] = 'test'; -/// ``` -class XMLBase { - /// ## XMLBase - /// - /// Default constructor for creating an empty XML element. - /// - /// [XMLBase] is designed with customization in mind. Users can extend the - /// class and override methods, allowing for the manipulation of custom fields - /// and the implementation of specialized behavior when needed. - XMLBase({ - /// If no `name` is passed, sets the default name of the stanza to `stanza` - this.name = 'stanza', - - /// If `null`, then default stanza namespace will be used - String? namespace, - this.transport, - this.pluginAttribute = 'plugin', - this.pluginMultiAttribute, - this.overrides = const [], - Map? pluginTagMapping, - Map? pluginAttributeMapping, - - /// Defaults to predefined ones - this.interfaces = const {'type', 'to', 'from', 'id', 'payload'}, - this.subInterfaces = const {}, - this.boolInterfaces = const {}, - this.languageInterfaces = const {}, - Map? pluginOverrides, - Set? pluginIterables, - this.receive = false, - this.isExtension = false, - this.includeNamespace = true, - Map? getters, - Map? setters, - Map? deleters, - this.element, - this.parent, - }) { - this.pluginTagMapping = pluginTagMapping ?? {}; - this.pluginAttributeMapping = pluginAttributeMapping ?? {}; - this.pluginOverrides = pluginOverrides ?? {}; - this.pluginIterables = pluginIterables ?? {}; - - /// Defaults to `CLIENT`. - this.namespace = namespace ?? WhixpUtils.getNamespace('CLIENT'); - - /// Equal tag to the tag name. - tag = _tagName; - - if (getters != null) addGetters(getters); - if (setters != null) addSetters(setters); - if (deleters != null) addDeleters(deleters); - - if (setup(element)) return; - final children = >{}; - - for (final child in element!.childElements.toSet()) { - /// Must assign one of the namespaces. - final namespace = child.getAttribute('xmlns') ?? - element!.getAttribute('xmlns') ?? - WhixpUtils.getNamespace('CLIENT'); - final tag = '{$namespace}${child.localName}'; - - if (this.pluginTagMapping.containsKey(tag) && - this.pluginTagMapping[tag] != null) { - final pluginClass = this.pluginTagMapping[tag]; - children.add(Tuple2(child, pluginClass)); - } - } - - /// Growable iterable fix - for (final child in children) { - initPlugin( - child.value2!.pluginAttribute, - existingXML: child.value1, - reuse: false, - ); - } - } - - /// Index to keep for iterables. - int _index = 0; - - /// The XML tag name of the element, not including any namespace prefixes. - @internal - final String name; - - /// The XML namespace for the element. Given ``, then - /// `namespace = "bar"` should be used. - /// - /// Defaults namespace in the constructor scope to `jabber:client` since this - /// is being used in an XMPP library. - @internal - late String namespace; - - /// Unique identifiers of plugins across [XMLBase] classes. - @internal - final String pluginAttribute; - - /// [XMLBase] subclasses that are intended to be an iterable group of items, - /// the `pluginMultiAttribute` value defines an interface for the parent - /// stanza which returns the entire group of matching `substanzas`. - @internal - final String? pluginMultiAttribute; - - /// In some cases you may wish to override the behaviour of one of the - /// parent stanza's interfaces. The `overrides` list specifies the interface - /// name and access method to be overridden. For example, to override setting - /// the parent's `condition` interface you would use: - /// - /// ```dart - /// overrides = ['condition']; - /// ``` - /// - /// Getting and deleting the `condition` interface would not be affected. - @internal - final List overrides; - - /// A mapping of root element tag names - /// (in `<$name xmlns="$namespace"/>` format) to the plugin classes - /// responsible for them. - @internal - late final Map pluginTagMapping; - - /// When there is a need to indicate initialize plugin or get plugin we will - /// use [pluginAttributeMapping] keeper for this. - @internal - late final Map pluginAttributeMapping; - - /// The set of keys that the stanza provides for accessing and manipulating - /// the underlying XML object. This [Set] may be augmented with the - /// [pluginAttribute] value of any registered stanza plugins. - final Set interfaces; - - /// A subset of `interfaces` which maps interfaces to direct subelements of - /// the underlaying XML object. Using this [Set], the text of these - /// subelements may be set, retrieved, or removed without needing to define - /// custom methods. - @internal - final Set subInterfaces; - - /// A subset of [interfaces] which maps to the presence of subelements to - /// boolean values. Using this [Set] allows for quickly checking for the - /// existence of empty subelements. - @internal - final Set boolInterfaces; - - /// A subset of [interfaces] which maps to the presence of subelements to - /// language values. - @internal - final Set languageInterfaces; - - /// A [Map] of interface operations to the overriding functions. - /// - /// For instance, after overriding the `set` operation for the interface - /// `body`, [pluginOverrides] would be: - /// - /// ```dart - /// log(pluginOverrides); /// outputs {'body': Function()} - /// ``` - @internal - late final Map pluginOverrides; - - /// The set of stanza classes that can be iterated over using the `substanzas` - /// interface. - @internal - late final Set pluginIterables; - - /// Declares if stanza is incoming or outgoing stanza. Defaults to false. - @internal - final bool receive; - - /// If you need to add a new interface to an existing stanza, you can create - /// a plugin and set `isExtension = true`. Be sure to set the - /// [pluginAttribute] value to the desired interface name, and that it is the - /// only interface listed in [interfaces]. Requests for the new interface - /// from the parent stanza will be passed to the plugin directly. - @internal - final bool isExtension; - - /// Indicates that this stanza or stanza plugin should include [namespace]. - /// You need to specify this value in order to add namespace to your stanza, - /// 'cause defaults to `false`. - @internal - final bool includeNamespace; - - /// The helper [Map] contains all the required `setter` methods when there is - /// a need to override the current setter method. - late final Map _setters = {}; - - /// The helper [Map] contains all the required `getter` methods when there is - /// a need to override the current getter method. - late final Map _getters = - {}; - - /// The helper [Map] contains all the required `delete` methods when there is - /// a need to override the current delete method. - late final Map _deleters = - {}; - - @internal - final iterables = []; - - /// Keeps all initialized plugins across stanza. - final _plugins = , XMLBase>{}; - - /// Keeps all loaded plugins across stanza. - final _loadedPlugins = {}; - - /// The underlying [element] for the stanza. - @internal - xml.XmlElement? element; - - /// The parent [XMLBase] element for the stanza. - @internal - final XMLBase? parent; - - /// Underlying [Transport] for this stanza class. Helps to interact with - /// socket. - @internal - late Transport? transport; - - /// Tag name for the stanza in the format of "<$name xmlns="$namespace"/>". - @internal - late final String tag; - - /// The stanza's XML contents initializer. - /// - /// Will return `true` if XML was generated according to the stanza's - /// definition instead of building a stanza object from an existing XML - /// object. - @internal - bool setup([xml.XmlElement? element]) { - if (this.element != null) { - return false; - } - if (this.element == null && element != null) { - this.element = element; - return false; - } - - xml.XmlElement lastXML = xml.XmlElement(xml.XmlName('')); - int index = 0; - for (final ename in name.split('/')) { - final newElement = index == 0 && includeNamespace - ? WhixpUtils.xmlElement(ename, namespace: namespace) - : WhixpUtils.xmlElement(ename); - if (this.element == null) { - this.element = newElement; - } else { - lastXML.children.add(newElement); - } - lastXML = newElement; - index++; - } - - if (parent != null) { - parent!.element!.children.add(this.element!); - } - - return true; - } - - /// Enables and initializes a stanza plugin. - @internal - XMLBase enable(String attribute, [String? language]) => - initPlugin(attribute, language: language); - - /// Responsible to retrieve a stanza plugin through the passed [name] and - /// [language]. - /// - /// If [check] is true, then the method returns null instead of creating the - /// object. - @internal - XMLBase? getPlugin(String name, {String? language, bool check = false}) { - /// If passed `language` is null, then try to retrieve it through built-in - /// method. - final lang = (language == null || language.isEmpty) ? _getLang : language; - - if (!pluginAttributeMapping.containsKey(name)) { - return null; - } - - final plugin = pluginAttributeMapping[name]; - - if (plugin == null) return null; - - if (plugin.isExtension) { - if (_plugins[Tuple2(name, '')] != null) { - return _plugins[Tuple2(name, '')]; - } else { - return check ? null : initPlugin(name, language: lang); - } - } else { - if (_plugins[Tuple2(name, lang)] != null) { - return _plugins[Tuple2(name, lang)]; - } else { - return check ? null : initPlugin(name, language: lang); - } - } - } - - /// Responsible to enable and initialize a stanza plugin. - @internal - XMLBase initPlugin( - String attribute, { - String? language, - xml.XmlElement? existingXML, - bool reuse = true, - XMLBase? element, - }) { - final defaultLanguage = _getLang; - final lang = - (language == null || language.isEmpty) ? defaultLanguage : language; - - late final pluginClass = pluginAttributeMapping[attribute]!; - - if (pluginClass.isExtension && _plugins[Tuple2(attribute, '')] != null) { - return _plugins[Tuple2(attribute, '')]!; - } - if (reuse && _plugins[Tuple2(attribute, lang)] != null) { - return _plugins[Tuple2(attribute, lang)]!; - } - - late XMLBase plugin; - - if (element != null) { - plugin = element; - } else { - plugin = pluginClass.copy(element: existingXML, parent: this); - } - - if (plugin.isExtension) { - _plugins[Tuple2(attribute, '')] = plugin; - } else { - if (lang != defaultLanguage) plugin['lang'] = lang; - - _plugins[Tuple2(attribute, lang)] = plugin; - } - - if (pluginIterables.contains(pluginClass)) { - iterables.add(plugin); - if (pluginClass.pluginMultiAttribute != null) { - initPlugin(pluginClass.pluginMultiAttribute!); - } - } - - /// Assign `attribute` to the list to indicate that this plugin is loaded - /// already. - _loadedPlugins.add(attribute); - - return plugin; - } - - /// Returns the text contents of a sub element. - /// - /// In case the element does not exist, or it has not textual content, a [def] - /// value can be returned instead. An empty string is returned if no other - /// default is supplied. - /// - /// [String] or [Map] of String should be returned. If language is not defined - /// then all sub texts will be returned. - @internal - dynamic getSubText( - String name, { - String def = '', - String? language, - }) { - final castedName = '/${_fixNamespace(name).value1!}'; - if (language != null && language == '*') { - return _getAllSubText(name, def: def); - } - - final defaultLanguage = _getLang; - final lang = - (language == null || language.isEmpty) ? defaultLanguage : language; - - final stanzas = element!.queryXPath(castedName).nodes; - - if (stanzas.isEmpty) return def; - - String? result; - for (final stanza in stanzas) { - if (stanza.isElement) { - final node = stanza.node; - if ((_lang(node) ?? defaultLanguage) == lang) { - if (node.innerText.isEmpty) return def; - - result = node.innerText; - break; - } - if (stanza.node.innerText.isNotEmpty) { - result = stanza.node.innerText; - } - } - } - if (result != null) return result; - - return def; - } - - /// Returns all sub text of the element. - Map _getAllSubText( - String name, { - String def = '', - String? language, - }) { - final castedName = _fixNamespace(name).value1!; - - final defaultLanguage = _getLang; - final results = {}; - final stanzas = element!.findAllElements(castedName); - if (stanzas.isNotEmpty) { - for (final stanza in stanzas) { - final stanzaLanguage = _lang(stanza) ?? defaultLanguage; - - if (!(language != null) || - language == "*" || - language == defaultLanguage) { - late String text; - if (stanza.innerText.isEmpty) { - text = def; - } else { - text = stanza.innerText; - } - - results[stanzaLanguage] = text; - } - } - } - - return results; - } - - /// Sets the [text] contents of a sub element. - /// - /// In case the element does not exist, a element will be created, and its - /// text contents will be set. - /// - /// If the [text] is set to an empty string, or null, then the element will be - /// removed, unless [keep] is set to `true`. - @internal - xml.XmlNode? setSubText( - String name, { - String? text, - bool keep = false, - String? language, - }) { - final defaultLanguage = _getLang; - final lang = - (language == null || language.isEmpty) ? defaultLanguage : language; - - if ((text == null || text.isEmpty) && !keep) { - deleteSub(name, language: lang); - return null; - } - - final path = _fixNamespace(name, split: true).value2!; - final castedName = path.last; - - late xml.XmlNode? parent = element; - late List elements = []; - - List missingPath = []; - final searchOrder = List.from(path)..removeLast(); - - while (searchOrder.isNotEmpty) { - parent = element!.queryXPath('/${searchOrder.join('/')}').node?.node; - - final ename = searchOrder.removeLast(); - if (parent != null) { - break; - } else { - missingPath.add(ename); - } - } - missingPath = missingPath.reversed.toList(); - - if (parent != null) { - elements = element! - .queryXPath('/${path.join('/')}') - .nodes - .map((item) => item.node) - .toList(); - } else { - parent = element; - elements = []; - } - - for (final ename in missingPath) { - final tempElement = xml.XmlElement(xml.XmlName(ename)); - parent!.children.add(tempElement); - parent = tempElement; - } - - for (final element in elements) { - final elanguage = _lang(element) ?? defaultLanguage; - if ((lang.isEmpty && elanguage == defaultLanguage) || lang == elanguage) { - element.innerText = text!; - return element; - } - } - - final tempElement = xml.XmlElement(xml.XmlName(castedName)); - tempElement.innerText = text!; - - if (lang.isNotEmpty && lang != defaultLanguage) { - tempElement.setAttribute('xml:lang', lang); - } - parent!.children.add(tempElement); - return tempElement; - } - - /// Set text to wherever sub element under [name]. - void _setAllSubText( - String name, { - required Map values, - bool keep = false, - String? language, - }) { - deleteSub(name, language: language); - for (final entry in values.entries) { - if (!(language != null) || language == "*" || entry.key == language) { - setSubText(name, text: entry.value, keep: keep, language: entry.key); - } - } - } - - /// Remove sub elements that match the given [name] or XPath. - /// - /// If the element is in a path, then any parent elements that become empty - /// after deleting the element may also be deleted if requested by setting - /// [all] to `true`. - @internal - void deleteSub(String name, {bool all = false, String? language}) { - final path = _fixNamespace(name, split: true).value2!; - final originalTarget = path.last; - - final defaultLanguage = _getLang; - final lang = - (language == null || language.isEmpty) ? defaultLanguage : language; - - Iterable enumerate(List iterable) sync* { - for (int i = 0; i < iterable.length; i++) { - yield i; - } - } - - late xml.XmlNode? parent = element; - for (final level in enumerate(path)) { - final elementPath = path.sublist(0, path.length - level).join('/'); - final parentPath = (level > 0) - ? path.sublist(0, path.length - level - 1).join('/') - : null; - - final elements = element! - .queryXPath('/$elementPath') - .nodes - .map((item) => item.node) - .toList(); - if (parentPath != null && parentPath.isNotEmpty) { - parent = element!.queryXPath('/$parentPath').node?.node; - } - if (elements.isNotEmpty) { - parent ??= element; - for (final element in elements) { - if (element is xml.XmlElement) { - if (element.name.qualified == originalTarget || - element.children.isEmpty) { - final elementLanguage = _lang(element) ?? defaultLanguage; - - if (lang == '*' || elementLanguage == lang) { - if (parent!.children[level].innerXml - .contains(element.toXmlString())) { - parent.children[level].innerXml = parent - .children[level].innerXml - .replaceFirst(element.toXmlString(), ''); - } - if (parent.children.contains(element)) { - parent.children.remove(element); - } - } - } - } - } - } - if (!all) { - return; - } - } - } - - Tuple2?> _fixNamespace( - String xPath, { - bool split = false, - bool propogateNamespace = false, - }) => - fixNamespace( - xPath, - split: split, - propogateNamespace: propogateNamespace, - defaultNamespace: namespace, - ); - - /// Return the value of top level attribute of the XML object. - /// - /// In case the attribute has not been set, a [def] value can be returned - /// instead. An empty string is returned if not other default is supplied. - @internal - String getAttribute(String name, [String def = '']) { - if (element == null) return def; - return element!.getAttribute(name == 'lang' ? 'xml:lang' : name) ?? def; - } - - /// Set the value of a top level [attribute] of the XML object. - /// - /// If the new [value] is null or an empty string, then the attribute will be - /// removed. - @internal - void setAttribute( - String attribute, [ - String? value, - ]) { - if (value == null || value.isEmpty) { - return; - } - element!.setAttribute(attribute == 'lang' ? 'xml:lang' : attribute, value); - } - - /// Deletes attribute under [name] If there is not [element] associated, - /// returns from the function. - @internal - void deleteAttribute(String name) { - if (element == null) return; - if (element!.getAttribute(name) != null) element!.removeAttribute(name); - } - - /// Return the value of a stanza interface using operator overload. - /// - /// ### Example: - /// ```dart - /// final element = XMLBase(); - /// log(element['body']); /// this must print out 'message contents' - /// ``` - /// - /// Stanza interfaces are typically mapped directly to the underlying XML - /// object, but can be overridden by the presence of a `getAttribute` method - /// (or `foo` getter where the interface is named `foo`, etc). - /// - /// The search order for interface value retrieval for an interface named - /// `foo` is: - /// * The list of substanzas (`substanzas`) - /// * The result of calling the `getFood` override handler - /// * The result of calling `foo` getter - /// * The contents of the `foo` subelement, if `foo` is listed in - /// `subInterfaces` - /// * True or false depending on the existence of a `foo` subelement and `foo` - /// is in `boolInterfaces` - /// * The value of the `foo` attribute of the XML object - /// * The plugin named `foo` - /// * An empty string - /// - /// The search for an element will go through the passed `fullAttribute`. - dynamic operator [](String fullAttribute) { - final split = '$fullAttribute|'.split('|'); - final attribute = split[0]; - final language = split[1]; - - /// Check for if `languageInterfaces` contains both `language` and - /// `attribute` values, then assign `args` values respective to the check. - final Map args = (languageInterfaces.contains(language) && - languageInterfaces.contains(attribute)) - ? {'lang': language} - : {}; - - if (attribute == 'substanzas') { - return iterables; - } else if (interfaces.contains(attribute) || attribute == 'lang') { - final getMethod = attribute.toLowerCase(); - - if (pluginOverrides.isNotEmpty) { - final name = pluginOverrides[getMethod]; - - if (name != null && name.isNotEmpty) { - final plugin = getPlugin(name, language: language); - - if (plugin != null) { - final handler = plugin._getters[Symbol(getMethod)]; - - if (handler != null) return handler.call(args['lang'], plugin); - } - } - } - if (_getters.containsKey(Symbol(getMethod))) { - return _getters[Symbol(getMethod)]?.call(args['lang'], this); - } else { - if (subInterfaces.contains(attribute)) { - return getSubText(attribute, language: language); - } else if (boolInterfaces.contains(attribute)) { - return element!.getElement(attribute, namespace: namespace) != null; - } else { - return getAttribute(attribute); - } - } - } else if (pluginAttributeMapping.containsKey(attribute)) { - final plugin = getPlugin(attribute, language: language); - - if (plugin != null && plugin.isExtension) { - return plugin[fullAttribute]; - } - - return plugin; - } else { - return ''; - } - } - - /// Set the [value] of a stanza interface using operator overloading through - /// the passed [attribute] string. - /// - /// ### Example: - /// ```dart - /// final element = XMLBase(); - /// element['body'] = 'hert!'; - /// log(element['body']); /// must output 'hert!' - /// ``` - /// - /// Stanza interfaces are typically mapped directly to the underlying XML - /// object, but can be overridden by the presence of a `setAttribute` method - /// (or `foo` setter where the interface is named `foo`, etc.). - void operator []=(String attribute, dynamic value) { - final fullAttribute = attribute; - final attributeLanguage = '$attribute|'.split('|'); - final attrib = attributeLanguage[0]; - final lang = attributeLanguage[1].isEmpty ? null : attributeLanguage[1]; - final args = {}; - - if (languageInterfaces.contains(lang) && - languageInterfaces.contains(attrib)) { - args['lang'] = lang; - } - - if (interfaces.contains(attrib) || attrib == 'lang') { - if (value != null) { - final setMethod = attrib.toLowerCase(); - - if (pluginOverrides.isNotEmpty) { - final name = pluginOverrides[setMethod]; - - if (name != null && name.isNotEmpty) { - final plugin = getPlugin(name, language: lang); - - if (plugin != null) { - final handler = plugin._setters[Symbol(setMethod)]; - - if (handler != null) { - return handler.call( - value, - args['lang'], - plugin, - ); - } - } - } - } - if (_setters.containsKey(Symbol(setMethod))) { - _setters[Symbol(setMethod)]?.call(value, args['lang'], this); - } else { - if (subInterfaces.contains(attrib)) { - dynamic subvalue; - if (value is JabberID) { - subvalue = value.toString(); - } - subvalue ??= value; - if (lang == '*') { - return _setAllSubText( - attrib, - values: subvalue as Map, - language: '*', - ); - } - setSubText(attrib, text: subvalue as String?, language: lang); - return; - } else if (boolInterfaces.contains(attrib)) { - if (value != null && value as bool) { - setSubText(attrib, text: '', keep: true, language: lang); - return; - } else { - setSubText(attrib, text: '', language: lang); - return; - } - } else { - return setAttribute( - attrib, - (value != null && value is JabberID) - ? value.toString() - : value as String?, - ); - } - } - } - } else if (pluginAttributeMapping.containsKey(attrib) && - pluginAttributeMapping[attrib] != null) { - final plugin = getPlugin(attrib, language: lang); - if (plugin != null) { - plugin[fullAttribute] = value; - } - } - - return; - } - - /// Delete the value of a stanza interface. - /// - /// Stanza interfaces are typically mapped directly to the underlying XML - /// object, but can be overridden by the presence of [noSuchMethod] by adding - /// [Function] with [Symbol] key under [gettersAndSetters] [Map]. - @internal - void delete(String attribute) { - final fullAttribute = attribute; - final attributeLanguage = '$attribute|'.split('|'); - final attrib = attributeLanguage[0]; - final lang = attributeLanguage[1].isNotEmpty ? attributeLanguage[1] : null; - final args = {}; - - if (languageInterfaces.contains(lang) && - languageInterfaces.contains(attrib)) { - args['lang'] = lang; - } - - if (interfaces.contains(attrib) || attrib == 'lang') { - final deleteMethod = attrib.toLowerCase(); - - if (pluginOverrides.isNotEmpty) { - final name = pluginOverrides[deleteMethod]; - - if (name != null && name.isNotEmpty) { - final plugin = getPlugin(attrib, language: lang); - - if (plugin != null) { - final handler = plugin._deleters[Symbol(deleteMethod)]; - - if (handler != null) { - handler.call(args['lang'], plugin); - return; - } - } - } - } - if (_deleters.containsKey(Symbol(deleteMethod))) { - _deleters[Symbol(deleteMethod)]?.call(args['lang'], this); - } else { - if (subInterfaces.contains(attrib)) { - return deleteSub(attrib, language: lang); - } else if (boolInterfaces.contains(attrib)) { - return deleteSub(attrib, language: lang); - } else { - return deleteAttribute(attrib); - } - } - } else if (pluginAttributeMapping.containsKey(attrib) && - pluginAttributeMapping[attrib] != null) { - final plugin = getPlugin(attrib, language: lang, check: true); - if (plugin == null) { - return; - } - if (plugin.isExtension) { - plugin.delete(fullAttribute); - _plugins.remove(Tuple2(attrib, '')); - } else { - _plugins.remove(Tuple2(attrib, plugin['lang'])); - } - try { - element!.children.remove(plugin.element); - } catch (_) {} - } - } - - /// Add either an [xml.XmlElement] object or substanza to this stanza object. - /// - /// If a substanza object is appended, it will be added to the list of - /// iterable stanzas. - /// - /// Allows stanza objects to be used like lists. - XMLBase add(dynamic item) { - late Tuple2 tuple; - if (item is xml.XmlElement) { - tuple = Tuple2(item, null); - } else { - try { - tuple = Tuple2(null, item as XMLBase); - } on Exception { - if (dynamic is! XMLBase || dynamic is! xml.XmlElement) { - throw ArgumentError( - 'The item that is going to be added should be either XMLBase or Xml Element', - ); - } - } - } - if (tuple.value1 != null) { - if (tuple.value1!.nodeType == xml.XmlNodeType.ELEMENT) { - return _addXML(tuple.value1!); - } else { - throw ArgumentError('The provided element is not in type of XmlNode'); - } - } - if (tuple.value2 != null) { - final base = tuple.value2!; - element?.children.add(base.element!); - if (base == pluginTagMapping[base._tagName]) { - initPlugin( - base.pluginAttribute, - existingXML: base.element, - element: base, - reuse: false, - ); - } else if (pluginIterables.contains(base)) { - iterables.add(base); - if (base.pluginMultiAttribute != null && - base.pluginMultiAttribute!.isNotEmpty) { - initPlugin(base.pluginMultiAttribute!); - } - } else { - iterables.add(base); - } - } - - return this; - } - - /// Adds child element to the underlying XML element. - XMLBase _addXML(xml.XmlElement element) => - this..element!.children.add(element); - - /// Returns the namespaced name of the stanza's root element. - /// - /// The format for the tag name is: '{namespace}elementName'. - String get _tagName => '{$namespace}$name'; - - /// Gets current language of the xml element with xPath query. - String? _lang(xml.XmlNode element) { - final result = element - .queryXPath( - "//@*[local-name()='xml:lang' and namespace-uri()='${WhixpUtils.getNamespace('XML')}']", - ) - .node; - - if (result == null) return null; - - return result.node.getAttribute('xml:lang'); - } - - /// Gets language from underlying parent. - String get _getLang { - if (element == null) return ''; - - final result = _lang(element!); - if (result == null && parent != null) { - return parent!['lang'] as String; - } - return result ?? ''; - } - - /// Getter for stanza plugins list. - @internal - Map, XMLBase> get plugins => _plugins; - - /// Compares a stanza object with an XPath-like expression. - /// - /// If the XPath matches the contents o the stanza obejct, the match is - /// succesfull. - /// - /// The XPath expression may include checks for stanza attributes. - @internal - bool match(Tuple2?> xPath) { - late List xpath; - if (xPath.value1 != null) { - xpath = _fixNamespace(xPath.value1!, split: true).value2!; - } else { - xpath = xPath.value2!; - } - - /// Extract the tag name and attribute checks for the first XPath node - final components = xpath[0].split('@'); - final tag = components[0]; - final attributes = components.sublist(1); - - if (!{name, '{$namespace}$name'}.contains(tag) && - !_loadedPlugins.contains(tag) && - !pluginAttribute.contains(tag)) { - return false; - } - - /// Checks the rest of the XPath against any substanzas - bool matchedSubstanzas = false; - for (final substanza in iterables) { - if (xpath.sublist(1).isEmpty) { - break; - } - matchedSubstanzas = substanza.match(Tuple2(null, xpath.sublist(1))); - if (matchedSubstanzas) { - break; - } - } - - /// Checks attribute values - for (final attribute in attributes) { - final name = attribute.split('=')[0]; - final value = attribute.split('=')[1]; - - if (this[name] != value) { - return false; - } - } - - /// Checks sub interfaces - if (xpath.length > 1) { - final nextTag = xpath[1]; - if (subInterfaces.contains(nextTag) && this[nextTag] != null) { - return true; - } - } - - /// Attempt to continue matching the XPath using the stanza's plugin - if (!matchedSubstanzas && xpath.length > 1) { - final nextTag = xpath[1].split('@')[0].split('}').last; - final tags = {}; - int i = 0; - - for (final entry in _plugins.entries) { - if (entry.key.value1 == nextTag) { - tags[i] = entry.key.value2.isEmpty ? null : entry.key.value2; - i++; - } - } - - for (final entry in tags.entries) { - final plugin = getPlugin(nextTag, language: entry.value); - if (plugin != null && plugin.match(Tuple2(null, xpath.sublist(1)))) { - return true; - } - } - return false; - } - - /// Everything matched - return true; - } - - /// Returns the names of all stanza interfaces provided by the stanza object. - /// - /// Allows stanza objects to be used as [Map]. - @internal - List get keys { - final buffer = []; - for (final x in interfaces) { - buffer.add(x); - } - for (final x in _loadedPlugins) { - buffer.add(x); - } - buffer.add('lang'); - if (iterables.isNotEmpty) { - buffer.add('substanzas'); - } - return buffer; - } - - /// Set multiple stanza interface [values] using [Map]. - /// - /// Stanza plugin values may be set using nested [Map]s. - set _values(Map values) { - final iterableInterfaces = [ - for (final p in pluginIterables) p.pluginAttribute, - ]; - - if (values.containsKey('lang')) { - this['lang'] = values['lang']; - } - - if (values.containsKey('substanzas')) { - for (final stanza in iterables) { - element!.children.remove(stanza.element); - } - iterables.clear(); - - final substanzas = values['substanzas'] as List>; - for (final submap in substanzas) { - if (submap.containsKey('__childtag__')) { - for (final subclass in pluginIterables) { - final childtag = '{${subclass.namespace}}${subclass.name}'; - if (submap['__childtag__'] == childtag) { - final sub = subclass.copy(parent: this); - sub.values = submap; - iterables.add(sub); - } - } - } - } - } - - for (final entry in values.entries) { - final fullInterface = entry.key; - final interfaceLanguage = '${entry.key}|'.split('|'); - final interface = interfaceLanguage[0]; - final language = interfaceLanguage[1]; - if (interface == 'lang') { - continue; - } else if (interface == 'substanzas') { - continue; - } else if (interfaces.contains(interface)) { - this[fullInterface] = entry.value; - } else if (pluginAttributeMapping.containsKey(interface)) { - if (!iterableInterfaces.contains(interface)) { - final plugin = getPlugin(interface, language: language); - if (plugin != null) { - plugin.values = entry.value as Map; - } - } - } - } - return; - } - - /// Returns a JSON/Map version of the XML content exposed through the stanza's - /// interfaces. - Map get _values { - final values = {}; - values['lang'] = this['lang']; - - for (final interface in interfaces) { - if (this[interface] is JabberID) { - values[interface] = (this[interface] as JabberID).jid; - } else { - values[interface] = this[interface]; - } - if (languageInterfaces.contains(interface)) { - values['$interface|*'] = this['$interface|*']; - } - } - for (final plugin in _plugins.entries) { - final lang = plugin.value['lang']; - if (lang != null && (lang as String).isNotEmpty) { - values['${plugin.key.value1}|$lang'] = plugin.value.values; - } else { - values[plugin.key.value1] = plugin.value.values; - } - } - if (iterables.isNotEmpty) { - final iter = >[]; - for (final stanza in iterables) { - iter.add(stanza.values); - if (iter.length - 1 >= 0) { - iter[iter.length - 1]['__childtag__'] = stanza.tag; - } - } - values['substanzas'] = iter; - } - return values; - } - - /// Remove all XML element contents and plugins. - @internal - void clear() { - element!.children.clear(); - - for (final plugin in _plugins.keys) { - _plugins.remove(plugin); - } - } - - /// Returns a JSON/Map version of the XML content exposed through the stanza's - /// interfaces. - @internal - Map get values => _values; - - /// Set multiple stanza interface [values] using [Map]. - /// - /// Stanza plugin values may be set using nested [Map]s. - set values(Map values) => _values = values; - - /// Getter for private [Map] [_getters]. - @internal - Map get getters => _getters; - - /// Getter for private [Map] [_setters]. - @internal - Map get setters => _setters; - - /// Getter for private [Map] [_deleters]. - @internal - Map get deleters => _deleters; - - /// You need to override this method in order to create a copy from an - /// existing object due Dart do not have deep copy support for now. - /// - /// ### Example: - /// ```dart - /// class SimpleStanza extends XMLBase { - /// SimpleStanza({super.element, super.parent}); - /// - /// @override - /// XMLBase copy({xml.XmlElement? element, XMLBase? parent}) => - /// SimpleStanza(element: element, parent: parent); - /// } - /// ``` - @internal - XMLBase copy({xml.XmlElement? element, XMLBase? parent}) => XMLBase( - name: name, - namespace: namespace, - pluginAttribute: pluginAttribute, - pluginMultiAttribute: pluginMultiAttribute, - overrides: overrides, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - interfaces: interfaces, - subInterfaces: subInterfaces, - boolInterfaces: boolInterfaces, - languageInterfaces: languageInterfaces, - pluginIterables: pluginIterables, - getters: _getters, - setters: _setters, - deleters: _deleters, - isExtension: isExtension, - includeNamespace: includeNamespace, - element: element, - parent: parent, - ); - - /// Add a custom getter function to the [XMLBase] class, allowing users to - /// extend behavior of the class by defining custom getters for specific - /// attributes. - /// - /// ### Example: - /// ```dart - /// final base = XMLBase('someStanza'); - /// base.addGetters({Symbol('value'): (args, base) { - /// base.element.children!.remove(someChild); /// calling "value" getter will remove child - /// }}); - /// ``` - @internal - void addGetters(Map getters) => - _getters.addAll(getters); - - /// Adds custom setter functions to the [XMLBase] class, enabling users to - /// extend the class by defining custom setters for specific attributes. - /// - /// ### Example: - /// ```dart - /// final base = XMLBase('someStanza'); - /// base.addSetters({Symbol('value'): (args, value, base) { - /// base.element.children!.remove(someChild); /// calling "value" setter will remove child - /// }}); - /// ``` - @internal - void addSetters(Map setters) => _setters.addAll(setters); - - /// Adds custom deleter functions to the [XMLBase] class, allowing users to - /// extend the class by defining custom deleter functions for specific - /// attributes. - /// - /// ### Example: - /// ```dart - /// final base = XMLBase('someStanza'); - /// base.addDeleters({Symbol('value'): (args, value, base) { - /// base.element.children!.remove(someChild); /// calling "value" setter will remove child - /// }}); - /// ``` - @internal - void addDeleters(Map deleters) => - _deleters.addAll(deleters); - - /// When iterating over [XMLBase], helps to increment our plugin index. - void _incrementIndex() => _index++; - - /// Returns a string serialization of the underlying XML object. - @override - String toString() => WhixpUtils.serialize(element) ?? ''; -} - -/// Extender for [registerStanzaPlugin] method. -extension RegisterStanza on XMLBase { - /// Does what [registerStanzaPlugin] does. But without primary stanza. 'Cause - /// it is called as the part of the primary stanza and do not need it to - /// passed. - /// - /// [iterable] flag indicates if the plugin stanza should be included in the - /// parent stanza's iterable [substanzas] interface results. - /// - /// [overrides] flag indicates if the plugin should be allowed to override the - /// interface handlers for the parent stanza, based on the plugin's - /// [overrides] field. - @internal - void registerPlugin( - XMLBase plugin, { - bool iterable = false, - bool overrides = false, - }) => - registerStanzaPlugin( - this, - plugin, - iterable: iterable, - overrides: overrides, - ); -} diff --git a/lib/src/stream/matcher/base.dart b/lib/src/stream/matcher/base.dart deleted file mode 100644 index 24409a5..0000000 --- a/lib/src/stream/matcher/base.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; - -/// Base class for stanza matchers. -/// -/// Stanza matchers are used to pick stanzas out of the XML stream and pass -/// them to the appropriate stream handlers. -abstract class BaseMatcher { - BaseMatcher(this.criteria); - - final dynamic criteria; - - /// Checks if a stanza matches the stored criteria. - bool match(XMLBase base) => false; -} diff --git a/lib/src/stream/matcher/id.dart b/lib/src/stream/matcher/id.dart deleted file mode 100644 index 7c69020..0000000 --- a/lib/src/stream/matcher/id.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; - -/// Selects stanzas that have the same stanza 'id' interface value as the -/// desired ID. -class MatcherID extends BaseMatcher { - MatcherID(super.criteria); - - @override - bool match(XMLBase base) => base['id'] == criteria as String; -} - -/// The IDSender matcher selects stanzas that have the same stanza 'id' -/// interface value as the desired ID, and that the 'from' value is one of a -/// set of approved entities that can respond to a request. -class MatchIDSender extends BaseMatcher { - MatchIDSender(super.criteria) - : assert(criteria.runtimeType is! IDMatcherCriteria); - - /// Compare the given stanza's `id` attribute to the stored `id` value, and - /// verify the sender's JID. - @override - bool match(XMLBase base) { - final selfJID = (criteria as IDMatcherCriteria).self; - final peerJID = (criteria as IDMatcherCriteria).peer; - - late final allowed = {}; - allowed[''] = true; - allowed[selfJID.bare] = true; - allowed[selfJID.domain] = true; - allowed[peerJID.full] = true; - allowed[peerJID.bare] = true; - allowed[peerJID.domain] = true; - - final from = base['from']; - - try { - return base['id'] == (criteria as IDMatcherCriteria).id && allowed[from]!; - } catch (_) { - return false; - } - } -} - -/// Represents the criteria for matching stanzas. -/// -/// Instances of this class store information necessary for matching stanzas -/// based on their [id] attribute and sender's/peer's Jabber IDs. -class IDMatcherCriteria { - /// Creates a new instance with the give parameters. - const IDMatcherCriteria(this.self, this.peer, this.id); - - /// The Jabber ID of the sender. - final JabberID self; - - /// The Jabber ID of the peer. - final JabberID peer; - - /// The unique identifier used for matching stanzas. - final String id; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is IDMatcherCriteria && - runtimeType == other.runtimeType && - self == other.self && - peer == other.peer && - id == other.id; - - @override - int get hashCode => self.hashCode ^ peer.hashCode ^ id.hashCode; -} diff --git a/lib/src/stream/matcher/many.dart b/lib/src/stream/matcher/many.dart deleted file mode 100644 index 5d054ed..0000000 --- a/lib/src/stream/matcher/many.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; - -/// The [ManyMatcher] matcher may compare a stanza against multiple criteria. -/// -/// It is essentially an OR relation combining multiple matchers. Each of the -/// criteria must implement a `match()` method. -class ManyMatcher extends BaseMatcher { - /// The [ManyMatcher] matcher may compare a stanza against multiple criteria. - /// - /// It is essentially an OR relation combining multiple matchers. Each of the - /// criteria must implement a `match()` method. - ManyMatcher(super.criteria); - - /// Match a stanza against multiple criteria. The match is successful if one - /// of the criteria matches. Each of the criteria must implement a - /// `match()` method. - @override - bool match(XMLBase base) { - for (final matcher in criteria as Iterable) { - if (matcher.match(base)) return true; - } - - return false; - } -} diff --git a/lib/src/stream/matcher/matcher.dart b/lib/src/stream/matcher/matcher.dart deleted file mode 100644 index 75239df..0000000 --- a/lib/src/stream/matcher/matcher.dart +++ /dev/null @@ -1,6 +0,0 @@ -export 'base.dart'; -export 'id.dart'; -export 'many.dart'; -export 'stanza.dart'; -export 'xml.dart'; -export 'xpath.dart'; diff --git a/lib/src/stream/matcher/stanza.dart b/lib/src/stream/matcher/stanza.dart deleted file mode 100644 index 697e0c9..0000000 --- a/lib/src/stream/matcher/stanza.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:dartz/dartz.dart'; - -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -/// Selects stanzas that match a given "stanza path", which is similar to a -/// normal XPath except that it uses the interfaces and plugins of the stanza -/// instead of the actual, underlying XML. -class StanzaPathMatcher extends BaseMatcher { - /// Compares a stanza against a "stanza path". A stanza path is similar to - /// XPath expression, but uses the stanza's interfaces and plugins instead - /// of underlying XML. - StanzaPathMatcher(super.criteria); - - @override - bool match(XMLBase base) { - final rawCriteria = fixNamespace( - criteria as String, - split: true, - propogateNamespace: false, - defaultNamespace: WhixpUtils.getNamespace('CLIENT'), - ); - - return base.match(rawCriteria) || - base.match(Tuple2(criteria as String, null)); - } -} diff --git a/lib/src/stream/matcher/xml.dart b/lib/src/stream/matcher/xml.dart deleted file mode 100644 index ba1e8aa..0000000 --- a/lib/src/stream/matcher/xml.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -/// This matcher selects stanzas whose XML matches a certain XML pattern, or -/// mask. For example, the mask may be used to match message stanzas with body -/// elements: -/// ```xml -/// -/// ``` -class XMLMaskMatcher extends BaseMatcher { - XMLMaskMatcher(super.criteria, {String? defaultNamespace}) { - _defaultNamespace = defaultNamespace ?? WhixpUtils.getNamespace('CLIENT'); - } - - late final String _defaultNamespace; - - @override - bool match(XMLBase base) => _maskCompare( - base.element, - xml.XmlDocument.parse(criteria as String).rootElement, - ); - - /// Compares an XML object against an XML mask. - /// - /// Compares the provided [mask] element with the supplied [source] element. - /// Whether namespaces should be respected during the comparison. - bool _maskCompare( - xml.XmlElement? source, - xml.XmlElement mask, { - bool useNamespace = false, - }) { - if (source == null) { - return false; - } - - final sourceTag = '{${source.getAttribute('xmlns')}}${source.localName}'; - final maskTag = - '{$_defaultNamespace}{${mask.getAttribute('xmlns')}}${mask.localName}'; - if (!{_defaultNamespace, maskTag}.contains(sourceTag)) { - return false; - } - - if (mask.innerText.isNotEmpty && - source.innerText.isNotEmpty && - source.innerText.trim() != mask.innerText.trim()) { - return false; - } - - for (final item in mask.attributes) { - if (source.getAttribute(item.localName) != item.value) { - return false; - } - } - - final matchedElements = {}; - for (final subelement in mask.childElements) { - bool matched = false; - for (final other in source.findAllElements( - subelement.localName, - namespace: subelement.getAttribute('xmlns'), - )) { - matchedElements[other] = false; - if (_maskCompare( - subelement, - other, - useNamespace: useNamespace, - )) { - if (matchedElements[other] == null) { - matchedElements[other] = true; - matched = true; - } - } - } - if (!matched) { - return false; - } - } - - return true; - } -} diff --git a/lib/src/stream/matcher/xpath.dart b/lib/src/stream/matcher/xpath.dart deleted file mode 100644 index b07c9cc..0000000 --- a/lib/src/stream/matcher/xpath.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; - -/// A matcher class for comparing stanzas against a "stanza path". -/// -/// The [XPathMatcher] class extends [BaseMatcher] and provides an -/// implementation for comparing a stanza against a stanza path. The stanza path -/// is similar to an XPath expression but uses the stanza's interfaces and -/// plugins instead of the underlying XML. -class XPathMatcher extends BaseMatcher { - /// Constructs an [XPathMatcher] with the specified criteria. - /// - /// The [criteria] parameter represents the stanza path against which stanzas - /// will be compared. - XPathMatcher(super.criteria); - - /// Compare a stanza against a "stanza path". A stanza path is similar to an - /// XPath expression, but uses the stanza's interfaces and plugins instead of - /// the underlying XML. - /// - /// ### Example: - /// ```dart - /// final matcher = XPathMatcher('{namespace}name'); - /// ``` - /// - /// __Note__: Actually, this class is not matches stanzas over the XPath expression. - /// It is just placeholder for the actual name, instead XPath, it uses custom - /// element tag for comperison. - @override - bool match(XMLBase base) { - final element = base.element; - - /// Namespace fix. - final rawCriteria = fixNamespace(criteria as String, split: true).value2; - - /// Retrieves the XML tag of the [base.element]. - final tag = - '<${element!.localName} xmlns="${element.getAttribute('xmlns') ?? ''}"/>'; - - /// Compare the stored criteria with the XML tag of the stanza. - return rawCriteria?.contains(tag) ?? false; - } -} diff --git a/lib/src/stream/stanza.dart b/lib/src/stream/stanza.dart deleted file mode 100644 index d531319..0000000 --- a/lib/src/stream/stanza.dart +++ /dev/null @@ -1,194 +0,0 @@ -part of 'base.dart'; - -/// Provides the foundation for all other stanza objects used by [Whixp], and -/// defines a basic set of interfaces common to nearly all stanzas. -/// -/// These interfaces are the `id`, `type`, `to`, and `from` attributes. An -/// additional interface, `payload` is available to access the XML contents of -/// the stanza. Most stanza objects will provided more specific interfaces, -/// however. -class StanzaBase extends XMLBase { - /// All parameters are extended from [XMLBase]. For more information please - /// take a look at [XMLBase]. - StanzaBase({ - /// A [JabberID] representing the receipient's JID - JabberID? stanzaTo, - - /// A [JabberID] representing the sender's JID - JabberID? stanzaFrom, - - /// The type of stanza, typically will be `normal`, `error`, `get` or `set`, - /// etc. - String? stanzaType, - - ///An optional unique identifier that can be used to associate stanzas - String? stanzaID, - this.types = const {}, - super.name, - super.namespace, - super.transport, - super.pluginAttribute, - super.pluginMultiAttribute, - super.overrides, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.interfaces, - super.subInterfaces, - super.boolInterfaces, - super.languageInterfaces, - super.pluginOverrides, - super.pluginIterables, - super.receive, - super.isExtension, - super.includeNamespace, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) { - if (transport != null) { - namespace = transport!.defaultNamespace; - } - - if (stanzaType != null) { - this['type'] = stanzaType; - } - if (stanzaTo != null) { - this['to'] = stanzaTo; - } - if (stanzaFrom != null) { - this['from'] = stanzaFrom; - } - if (stanzaID != null) { - this['id'] = stanzaID; - } - - addSetters({ - const Symbol('payload'): (value, args, base) => - setPayload([value as xml.XmlElement]), - }); - - addDeleters({const Symbol('payload'): (_, __) => deletePayload()}); - } - - late Set types; - - /// Sets the stanza's `type` attribute. - void setType(String value) { - if (types.contains(value)) { - element!.setAttribute('type', value); - } - } - - /// Returns the value of stanza's `to` attribute. - JabberID? get to => - getAttribute('to').isEmpty ? null : JabberID(getAttribute('to')); - - /// Set the default `to` attribute of the stanza according to the passed [to] - /// value. - void setTo(String to) => setAttribute('to', to); - - /// Returns the value of stanza's `from` attribute. - JabberID? get from => - getAttribute('from').isEmpty ? null : JabberID(getAttribute('from')); - - /// Set the default `to` attribute of the stanza according to the passed - /// [frpm] value. - void setFrom(String from) => setAttribute('from', from); - - /// Returns a [Iterable] of XML child elements. - Iterable get payload => element!.childElements; - - /// Add [xml.XmlElement] content to the stanza. - void setPayload(List values) { - for (final value in values) { - add(value); - } - } - - /// Remove the XML contents of the stanza. - void deletePayload() => clear(); - - /// Prepares the stanza for sending a reply. - /// - /// Swaps the `from` and `to` attributes. - /// - /// If [clear] is `true`, then also remove the stanza's contents to make room - /// for the reply content. - /// - /// For client streams, the `from` attribute is removed. - S reply({required S copiedStanza, bool clear = true}) { - final newStanza = copiedStanza; - - if (transport != null && transport!.isComponent) { - newStanza['from'] = this['to']; - newStanza['to'] = this['from']; - } else { - newStanza['to'] = this['from']; - newStanza.delete('from'); - } - if (clear) { - newStanza.clear(); - } - - return newStanza; - } - - /// Set the stanza's type to `error`. - StanzaBase error() { - this['type'] = 'error'; - return this; - } - - /// Called if no handlers have been registered to process this stanza. - /// - /// Mean to be overridden. - void unhandled([Transport? transport]) { - return; - } - - /// Handle exceptions thrown during stanza processing. - /// - /// Meant to be overridden. - void exception(dynamic excp) {} - - void send() { - if (transport != null) { - transport!.send(this); - } else { - Log.instance.warning('Tried to send stanza without a transport: $this'); - } - } - - @override - StanzaBase copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - StanzaBase( - name: name, - namespace: namespace, - pluginAttribute: pluginAttribute, - pluginMultiAttribute: pluginMultiAttribute, - overrides: overrides, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - interfaces: interfaces, - subInterfaces: subInterfaces, - boolInterfaces: boolInterfaces, - languageInterfaces: languageInterfaces, - pluginOverrides: pluginOverrides, - pluginIterables: pluginIterables, - receive: receive, - isExtension: isExtension, - includeNamespace: includeNamespace, - transport: transport, - getters: _getters, - setters: _setters, - deleters: _deleters, - element: element, - parent: parent, - ); -} diff --git a/test/class/base.dart b/test/class/base.dart deleted file mode 100644 index a7dc248..0000000 --- a/test/class/base.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; - -XMLBase createTestStanza({ - required String name, - String? namespace, - String pluginAttribute = 'plugin', - String? pluginMultiAttribute, - List overrides = const [], - Set interfaces = const {}, - Set subInterfaces = const {}, - Set boolInterfaces = const {}, - Set languageInterfaces = const {}, - Map? getters, - Map? setters, - Map? deleters, - bool isExtension = false, - bool includeNamespace = true, -}) => - _TestStanza( - name: name, - namespace: namespace, - pluginAttribute: pluginAttribute, - pluginMultiAttribute: pluginMultiAttribute, - overrides: overrides, - interfaces: interfaces, - subInterfaces: subInterfaces, - boolInterfaces: boolInterfaces, - languageInterfaces: languageInterfaces, - getters: getters, - setters: setters, - deleters: deleters, - isExtension: isExtension, - includeNamespace: includeNamespace, - ); - -class _TestStanza extends XMLBase { - _TestStanza({ - super.name, - super.namespace, - super.pluginAttribute, - super.pluginMultiAttribute, - super.overrides, - super.interfaces, - super.subInterfaces, - super.boolInterfaces, - super.languageInterfaces, - super.getters, - super.setters, - super.deleters, - super.isExtension, - super.includeNamespace, - }); -} - -class MultiTestStanza2 extends XMLBase { - MultiTestStanza2({ - super.name, - super.namespace, - super.pluginAttribute, - super.pluginMultiAttribute, - super.includeNamespace = true, - super.element, - super.parent, - }); -} diff --git a/test/class/property.dart b/test/class/property.dart deleted file mode 100644 index 0209aff..0000000 --- a/test/class/property.dart +++ /dev/null @@ -1,7 +0,0 @@ -// class PropertyTestClass { -// int firstProperty = 42; -// final secondProperty = 100; -// final dynamic nullProperty = null; - -// int intMethod() => 0; -// } diff --git a/xml/_extensions.dart b/xml/_extensions.dart deleted file mode 100644 index 5c64a02..0000000 --- a/xml/_extensions.dart +++ /dev/null @@ -1,318 +0,0 @@ -part of 'base.dart'; - -extension Attribute on BaseElement { - String getAttribute( - String attribute, [ - String absence = '', - String? namespace, - ]) { - if (attribute == 'lang' || attribute == 'language') { - return _xml.getAttribute(_xmlLanguage, namespace: namespace) ?? absence; - } else { - return _xml.getAttribute(attribute, namespace: namespace) ?? absence; - } - } - - void setAttribute(String attribute, [String? value, String? namespace]) { - if (value == null) return deleteAttribute(attribute, namespace); - if (attribute == 'lang' || attribute == 'language') { - _xml.setAttribute(_xmlLanguage, value, namespace: namespace); - } else { - _xml.setAttribute(attribute, value, namespace: namespace); - } - } - - void deleteAttribute(String attribute, [String? namespace]) => - _xml.removeAttribute(attribute, namespace: namespace); -} - -extension SubTextGetters on BaseElement { - String getSubText(String name, {String absence = '', String? language}) { - assert( - language != '*', - 'Instead call `getAllSubtext` to get all language texts', - ); - - final stanzas = _xml.findAllElements(name); - if (stanzas.isEmpty) return absence; - - String? result; - for (final stanza in stanzas) { - if (_lang(stanza) == language) { - if (stanza.innerText.isEmpty) return absence; - - result = stanza.innerText; - break; - } - if (stanza.innerText.isNotEmpty) { - result = stanza.innerText; - } - } - - return result ?? absence; - } - - Map getAllSubText( - String name, { - String absence = '', - String? language, - }) { - final casted = _fixedNamespace(name).join('/'); - - final results = {}; - final stanzas = _xml.findAllElements(casted); - if (stanzas.isNotEmpty) { - for (final stanza in stanzas) { - final stanzaLanguage = _lang(stanza) ?? defaultLanguage; - - if (language == stanzaLanguage || language == '*') { - late String text; - if (stanza.innerText.isEmpty) { - text = absence; - } else { - text = stanza.innerText; - } - - results[stanzaLanguage] = text; - } - } - } - - return results; - } -} - -extension SubTextSetters on BaseElement { - XmlNode? setSubText( - String name, { - String text = '', - String? language, - bool keep = false, - }) { - /// Set language to an empty string beforehand, 'cause it will lead to - /// unexpected behaviour like adding an empty language key to the stanza. - language = language ?? ''; - final lang = language.isNotEmpty ? language : defaultLanguage; - - if (text.isEmpty && !keep) { - removeSubElement(name, language: lang); - return null; - } - - final path = _fixedNamespace(name); - final casted = path.last; - - XmlNode? parent; - final elements = []; - - List missingPath = []; - final searchOrder = List.from(path)..removeLast(); - - while (searchOrder.isNotEmpty) { - parent = _xml.xpath('/${searchOrder.join('/')}').firstOrNull; - - final searched = searchOrder.removeLast(); - if (parent != null) break; - missingPath.add(searched); - } - - missingPath = missingPath.reversed.toList(); - - if (parent != null) { - elements.addAll(_xml.xpath('/${path.join('/')}')); - } else { - parent = _xml; - elements.clear(); - } - - for (final missing in missingPath) { - final temporary = WhixpUtils.xmlElement(missing); - parent?.children.add(temporary); - parent = temporary; - } - - for (final element in elements) { - final language = _lang(element) ?? defaultLanguage; - if ((lang == null && language == defaultLanguage) || lang == language) { - _xml.innerText = text; - return _xml; - } - } - - final temporary = WhixpUtils.xmlElement(casted); - temporary.innerText = text; - - if (lang != null && lang != defaultLanguage) { - temporary.setAttribute(_xmlLanguage, lang); - } - - parent?.children.add(temporary); - return temporary; - } - - void setAllSubText( - String name, { - String? language, - Map values = const {}, - }) { - assert(values.isNotEmpty, 'Subtext values to be set can not be empty'); - removeSubElement(name, language: language); - for (final entry in values.entries) { - if (language == null || language == '*' || entry.key == language) { - setSubText(name, text: entry.value, language: entry.key); - } - } - } -} - -extension SubTextRemovers on BaseElement { - void removeSubText(String name, {bool all = false, String? language}) => - removeSubElement(name, all: all, language: language, onlyContent: true); -} - -extension SubElementRemovers on BaseElement { - void removeSubElement( - String name, { - bool all = false, - bool onlyContent = false, - String? language, - }) { - final path = _fixedNamespace(name); - final target = path.last; - - final lang = language ?? defaultLanguage; - - Iterable enumerate(List iterable) sync* { - for (int i = 0; i < iterable.length; i++) { - yield i; - } - } - - XmlNode parent = _xml; - for (final level in enumerate(path)) { - final elementPath = path.sublist(0, path.length - level).join('/'); - final parentPath = - (level > 0) ? path.sublist(0, path.length - level - 1).join('/') : ''; - - final elements = _xml.xpath('/$elementPath'); - if (parentPath.isNotEmpty) { - parent = _xml.xpath('/$parentPath').firstOrNull ?? _xml; - } - - for (final element in elements.toList()) { - if (element is XmlElement) { - if (element.name.qualified == target || element.children.isEmpty) { - final elementLanguage = _lang(element); - if (lang == '*' || elementLanguage == lang) { - final result = parent.children.remove(element); - if (onlyContent && result) { - parent.children.add(element..innerText = ''); - } - } - } - } - } - - if (!all) return; - } - } -} - -extension XMLManipulator on BaseElement { - XmlElement get xml => _xml; - - Iterable get childElements => _xml.childElements; - - XmlElement? getElement(String name) => _xml.getElement(name); -} - -extension Registrator on BaseElement { - void register(String name, BaseElement element) => - _ElementPluginRegistrator().register(name, element); - - void unregister(String name) => _ElementPluginRegistrator().unregister(name); -} - -extension Interfaces on BaseElement { - void setInterface(String name, String value, {String? language}) { - final lang = language ?? defaultLanguage; - assert(lang != '*', 'Use `setInterfaces` method instead'); - - setSubText(name, text: value, language: lang); - } - - void setInterfaces( - String name, - Map values, { - String? language, - }) { - final lang = language ?? defaultLanguage; - - return setAllSubText(name, values: values, language: lang); - } - - List getInterfaces(String name, {String? language}) { - final lang = language ?? defaultLanguage; - - final elements = _xml.findAllElements(name); - final interfaces = []; - if (elements.isEmpty) return interfaces; - - for (final element in elements) { - if (_lang(element) == lang || lang == '*') { - interfaces.add(element); - } - } - - return interfaces; - } - - void removeInterface(String name, {String? language}) { - final lang = language ?? defaultLanguage; - - if (_plugins.contains(name)) { - final plugin = get(name, language: lang); - if (plugin == null) return; - - _registeredPlugins.remove(name); - _xml.children.remove(plugin._xml); - } else { - removeSubElement(name, language: lang); - } - } - - XmlNode? setEmptyInterface(String name, bool value, {String? language}) { - if (value) { - return setSubText(name, keep: true, language: language); - } else { - return setSubText(name, language: language); - } - } - - bool containsInterface(String name) => _xml.getElement(name) != null; -} - -extension Language on BaseElement { - String? _lang([XmlNode? xml]) { - if (xml != null) { - return xml - .copy() - .xpath('//*[@$_xmlLanguage]') - .firstOrNull - ?.getAttribute(_xmlLanguage); - } - - /// Get default language by the lookup to the root element of the root - /// element. - return _xml.getAttribute(_xmlLanguage); - } -} - -extension Tag on BaseElement { - String get tag { - if (_namespace != null) { - return '$_namespace$_name'; - } - return _name; - } -} diff --git a/xml/_model.dart b/xml/_model.dart deleted file mode 100644 index 21d588a..0000000 --- a/xml/_model.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of 'base.dart'; - -class ElementModel { - const ElementModel(this.name, this.namespace, {this.xml, this.parent}); - - final String name; - final String? namespace; - final XmlElement? xml; - final BaseElement? parent; - - String _getter(String? data) => data ?? 'NOT included'; - - @override - String toString() => - '''Element Model: namespace => ${_getter(namespace)}, parent => ${parent?._name}'''; - - @override - bool operator ==(Object element) => - element is ElementModel && - element.name == name && - element.namespace == namespace && - element.xml == xml && - element.parent == parent; - - @override - int get hashCode => - name.hashCode ^ namespace.hashCode ^ xml.hashCode ^ parent.hashCode; -} diff --git a/xml/_registrator.dart b/xml/_registrator.dart deleted file mode 100644 index a84b075..0000000 --- a/xml/_registrator.dart +++ /dev/null @@ -1,31 +0,0 @@ -part of 'base.dart'; - -class _ElementPluginRegistrator { - factory _ElementPluginRegistrator() => _instance; - - _ElementPluginRegistrator._(); - - static final _ElementPluginRegistrator _instance = - _ElementPluginRegistrator._(); - - final _plugins = {}; - - BaseElement get(String name) { - assert(_plugins.containsKey(name), '$name plugin is not registered'); - return _plugins[name]!; - } - - void register(String name, BaseElement element) { - if (_plugins.containsKey(name)) return; - - _plugins[name] = element; - } - - void unregister(String name) { - if (!_plugins.containsKey(name)) return; - - _plugins.remove(name); - } - - void clear() => _plugins.clear(); -} diff --git a/xml/_registry.dart b/xml/_registry.dart deleted file mode 100644 index c6f4e98..0000000 --- a/xml/_registry.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of 'stanza.dart'; - -final _createRegistry = { - IQ: (xml) => IQ(xml: xml), - Query: (xml) => Query(xml: xml), -}; - -final _parseRegistry = { - IQ: (model) => - IQ(namespace: model.namespace, xml: model.xml, parent: model.parent), - Query: (model) => - Query(namespace: model.namespace, xml: model.xml, parent: model.parent), -}; diff --git a/xml/_static.dart b/xml/_static.dart deleted file mode 100644 index f7a19ec..0000000 --- a/xml/_static.dart +++ /dev/null @@ -1,3 +0,0 @@ -part of 'base.dart'; - -const String _xmlLanguage = 'xml:lang'; diff --git a/xml/base.dart b/xml/base.dart deleted file mode 100644 index bdb0e1c..0000000 --- a/xml/base.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:dartz/dartz.dart'; - -import 'package:whixp/src/exception.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart'; -import 'package:xml/xpath.dart'; - -import 'stanza.dart'; - -part '_extensions.dart'; -part '_model.dart'; -part '_registrator.dart'; -part '_static.dart'; - -List _fixNamespace( - String xPath, { - String? absenceNamespace, - bool propogateNamespace = true, -}) { - final fixed = []; - - final namespaceBlocks = xPath.split('{'); - for (final block in namespaceBlocks) { - late String? namespace; - late List elements; - if (block.contains('}')) { - final namespaceBlockSplit = block.split('}'); - namespace = namespaceBlockSplit[0]; - elements = namespaceBlockSplit[1].split('/'); - } else { - namespace = absenceNamespace; - elements = block.split('/'); - } - - for (final element in elements) { - late String tag; - if (element.isNotEmpty) { - if (propogateNamespace && element[0] != '*') { - if (namespace != null) { - tag = '<$element xmlns="$namespace"/>'; - } else { - tag = '<$element/>'; - } - } else { - tag = element; - } - fixed.add(tag); - } - } - } - - return fixed; -} - -abstract class BaseElement implements BaseElementFactory { - BaseElement( - String name, { - String? namespace, - Set plugins = const {}, - XmlElement? xml, - BaseElement? parent, - }) { - _name = name; - _namespace = namespace; - _parent = parent; - _plugins = plugins; - - /// Whenever the element is initialized, create an empty list of plugins. - _registeredPlugins = >{}; - - final parse = !_setup(xml); - - if (parse) { - final childElements = _xml.childElements.toList().reversed.toList(); - final elements = >[]; - - if (childElements.isEmpty) return; - - for (final element in childElements) { - final name = element.localName; - - if (_plugins.contains(name)) { - final plugin = _ElementPluginRegistrator().get(name); - elements.add(Tuple2(element, plugin)); - } - } - - for (final element in elements) { - _initPlugin(element.value2._name, element.value1, element.value2); - } - } - } - - late final String _name; - late final String? _namespace; - late final XmlElement _xml; - late final BaseElement? _parent; - late final Set _plugins; - late final Map> _registeredPlugins; - - bool _setup([XmlElement? xml]) { - _ElementPluginRegistrator().register(_name, this); - if (xml != null) { - _xml = xml; - return false; - } - - final parts = _name.split('/'); - - XmlElement? lastxml; - for (final splitted in parts) { - final newxml = lastxml == null - ? WhixpUtils.xmlElement(splitted, namespace: _namespace) - : WhixpUtils.xmlElement(splitted); - - if (lastxml == null) { - lastxml = newxml; - } else { - lastxml.children.add(newxml); - } - } - - _xml = lastxml ?? XmlElement(XmlName('')); - - if (_parent != null) { - _parent._xml.children.add(_xml); - } - - return true; - } - - BaseElement _initPlugin( - String pluginName, - XmlElement existingXml, - BaseElement currentElement, { - String? language, - }) { - final name = existingXml.localName; - final namespace = existingXml.getAttribute('xmlns'); - final plugin = BaseElementFactory.parse( - currentElement, - ElementModel(name, namespace, xml: existingXml, parent: this), - ); - final lang = language ?? defaultLanguage; - - _registeredPlugins[pluginName] = Tuple2(lang, plugin); - - return plugin; - } - - List _fixedNamespace(String xPath) => _fixNamespace( - xPath, - absenceNamespace: _namespace, - propogateNamespace: false, - ); - - E? get( - String name, { - String? language, - bool fallbackInitialize = false, - }) { - final lang = language ?? defaultLanguage; - - BaseElement? plugin; - if (_registeredPlugins.containsKey(name)) { - final existing = _registeredPlugins[name]; - if (existing != null && existing.value1 == lang) { - plugin = existing.value2; - } - } else if (fallbackInitialize) { - final element = _xml.getElement(name); - if (element != null) { - plugin = _initPlugin(name, element, this, language: lang); - } - } - - try { - final casted = plugin as E?; - return casted; - } on Exception { - throw WhixpInternalException( - 'Type ${plugin.runtimeType} can not be casted to the ${E.runtimeType} type', - ); - } - } - - set defaultLanguage(String? language) => setAttribute(_xmlLanguage, language); - - String? get defaultLanguage => _lang(); - - @override - String toString() => WhixpUtils.serialize(_xml) ?? ''; - - @override - bool operator ==(Object element) => - element is BaseElement && - element._name == _name && - element._namespace == _namespace && - element._xml == _xml && - element._parent == _parent; - - @override - int get hashCode => - _name.hashCode ^ _namespace.hashCode ^ _xml.hashCode ^ _parent.hashCode; -} diff --git a/xml/stanza.dart b/xml/stanza.dart deleted file mode 100644 index dae09fb..0000000 --- a/xml/stanza.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:whixp/src/jid/jid.dart'; - -import 'package:xml/xml.dart'; - -import 'base.dart'; -import 'iq.dart'; -import 'query.dart'; - -part '_registry.dart'; - -enum StanzaType { get, set, result, error, none } - -mixin class BaseElementFactory { - const BaseElementFactory(); - - static Stanza create(XmlElement xml) => - _createRegistry[E]!(xml); - static BaseElement parse(BaseElement element, ElementModel model) => - _parseRegistry[element.runtimeType]!(model); -} - -abstract class Stanza extends BaseElement implements BaseElementFactory { - Stanza(super.name, {super.namespace, super.plugins, super.xml, super.parent}); - - JabberID? get to { - final attribute = getAttribute('to'); - if (attribute.isEmpty) return null; - - return JabberID(attribute); - } - - set to(JabberID? jid) => setAttribute('to', jid?.toString()); - - JabberID? get from { - final attribute = getAttribute('from'); - if (attribute.isEmpty) return null; - - return JabberID(attribute); - } - - set from(JabberID? jid) => setAttribute('from', jid?.toString()); - - StanzaType get type { - final attribute = getAttribute('type'); - switch (attribute) { - case 'get': - return StanzaType.get; - case 'set': - return StanzaType.set; - case 'result': - return StanzaType.result; - case 'error': - return StanzaType.error; - default: - return StanzaType.none; - } - } - - set type(StanzaType type) => setAttribute('type', type.name); -} - -Stanza create(XmlElement xml) => - BaseElementFactory.create(xml); - -BaseElement parse(BaseElement element, ElementModel model) => - BaseElementFactory.parse(element, model); From 60817b7524cb1b050fd7d44e3569dc33eed2d386 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:03 +0400 Subject: [PATCH 12/81] refactor(xml): Remove unused tune-related code files --- lib/src/plugins/tune/stanza.dart | 44 ---------- lib/src/plugins/tune/tune.dart | 129 --------------------------- lib/src/plugins/vcard/vcard.dart | 145 ------------------------------- 3 files changed, 318 deletions(-) delete mode 100644 lib/src/plugins/tune/stanza.dart delete mode 100644 lib/src/plugins/tune/tune.dart delete mode 100644 lib/src/plugins/vcard/vcard.dart diff --git a/lib/src/plugins/tune/stanza.dart b/lib/src/plugins/tune/stanza.dart deleted file mode 100644 index 68a56b0..0000000 --- a/lib/src/plugins/tune/stanza.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of 'tune.dart'; - -class Tune extends XMLBase { - Tune({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'tune', - namespace: 'http://jabber.org/protocol/tune', - pluginAttribute: 'tune', - interfaces: { - 'artist', - 'length', - 'rating', - 'source', - 'title', - 'track', - 'uri', - }, - subInterfaces: { - 'artist', - 'length', - 'rating', - 'source', - 'title', - 'track', - 'uri', - }, - ); - - void setLength(String length) => setSubText('length', text: length); - - void setRating(String rating) => setSubText('rating', text: rating); - - @override - Tune copy({xml.XmlElement? element, XMLBase? parent}) => Tune( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} diff --git a/lib/src/plugins/tune/tune.dart b/lib/src/plugins/tune/tune.dart deleted file mode 100644 index 66d936a..0000000 --- a/lib/src/plugins/tune/tune.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:async'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -/// Information about tunes is provided by the user and propagated on the -/// network by the user's client. The information container for tune data is a -/// ____ element that is qualified by the -/// 'http://jabber.org/protocol/tune' namespace. -/// -/// see -class UserTune extends PluginBase { - /// Tune information SHOULD be communicated and transported by means of the - /// Publish-Subscribe (XEP-0060) subset specified in Personal Eventing - /// Protocol (XEP-0163). Because tune information is not pure presence - /// information and can change independently of the user's availability, it - /// SHOULD NOT be provided as an extension to ____. - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// Some Artist - /// 686 - /// 10 - /// cartcurtsongs - /// Heart of the Sunrise - /// 3 - /// - /// - /// - /// - /// - /// `` - UserTune() - : super( - 'tune', - description: 'XEP-0118: User Tune', - dependencies: {'PEP'}, - ); - - PEP? _pep; - - @override - void pluginInitialize() { - _pep = base.getPluginInstance('PEP'); - if (_pep == null) { - _pep = PEP(); - base.registerPlugin(_pep!); - } - } - - /// Publishes the user's current tune. - /// - /// [source] represents the album name, website, or other source of the song. - ///
[rating] is the user's rating of the song (from 1 to 10). - FutureOr publishTune( - JabberID jid, { - String? artist, - int? length, - int? rating, - String? source, - String? title, - String? track, - String? uri, - String? node, - String? id, - Form? options, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final tune = Tune(); - tune['artist'] = artist; - tune['length'] = length.toString(); - tune['rating'] = rating.toString(); - tune['source'] = source; - tune['title'] = title; - tune['track'] = track; - tune['uri'] = uri; - - return _pep!.publish( - jid, - tune, - node: node, - id: id, - options: options, - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - @override - void sessionBind(String? jid) { - final pep = base.getPluginInstance('PEP'); - if (pep != null) { - pep.registerPEP('tune', Tune()); - } - } - - @override - void pluginEnd() { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.removeFeature(Tune().namespace); - } - - final pep = base.getPluginInstance('PEP'); - if (pep != null) { - pep.removeInterest([Tune().namespace]); - } - } -} diff --git a/lib/src/plugins/vcard/vcard.dart b/lib/src/plugins/vcard/vcard.dart deleted file mode 100644 index d3bd2e3..0000000 --- a/lib/src/plugins/vcard/vcard.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; - -import 'package:whixp/src/exception.dart'; -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/disco/disco.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -/// vCards are an existing and widely-used standard for personal user -/// information storage, somewhat like an electronic business card. -/// -/// see -class VCardTemp extends PluginBase { - VCardTemp() - : super( - 'vcard-temp', - description: 'XEP-0054: vCard-Temp', - dependencies: {'disco', 'time'}, - ); - - @override - void pluginInitialize() { - _cache = {}; - - base.transport.registerHandler( - CallbackHandler( - 'VCardTemp', - (iq) => _handleGetVCard(iq as IQ), - matcher: StanzaPathMatcher('iq/vcard_temp'), - ), - ); - } - - late final Map _cache; - - /// Retrieves a vCard. - /// - /// [cache] indicates that method should only check cache for vCard. - FutureOr getVCard( - JabberID jid, { - JabberID? iqFrom, - bool cache = false, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - bool local = cache; - - if (base.isComponent) { - if (jid.domain == base.transport.boundJID.domain) { - local = true; - } - } else { - if (jid == base.transport.boundJID) { - local = true; - } - } - - if (local) { - final vCard = _cache[jid.bare]; - final iq = base.makeIQGet(); - if (vCard != null) { - iq.add(vCard); - } - return iq; - } - - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - iq.enable('vcard_temp'); - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Publishes a [vCard]. - FutureOr publish( - VCardTempStanza vCard, { - JabberID? jid, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - if (jid != null) _cache[jid.bare] = vCard; - - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - iq.add(vCard.element!.copy()); - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - void _handleGetVCard(IQ iq) { - final type = iq['type']; - if (type == 'result') { - _cache[iq.from.toString()] = iq['vcard_temp'] as VCardTempStanza; - return; - } else if (type == 'get' && base.isComponent) { - final vcard = _cache[iq.to!.bare]; - final reply = iq.replyIQ(); - reply.add(vcard); - reply.sendIQ(); - } else if (type == 'set') { - throw StanzaException.serviceUnavailable(iq); - } - } - - @override - void sessionBind(String? jid) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.addFeature('vcard-temp'); - } - } - - @override - void pluginEnd() { - base.transport.removeHandler('VCardTemp'); - final disco = base.getPluginInstance( - 'disco', - enableIfRegistered: false, - ); - if (disco != null) { - disco.removeFeature('vcard-temp'); - } - } -} From 7bd3a3cf0762e7b1c87b4ef23e2584529e748ddb Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:18 +0400 Subject: [PATCH 13/81] refactor(xml): Update test_base.dart to use Stanza class instead of XMLBase --- test/test_base.dart | 38 ++++++-------------------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/test/test_base.dart b/test/test_base.dart index 16098dc..9630c48 100644 --- a/test/test_base.dart +++ b/test/test_base.dart @@ -1,6 +1,6 @@ import 'package:test/test.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; @@ -86,45 +86,19 @@ xml.XmlElement parseXMLFromString(String xmlToParse) { /// add or remove XML elements. Only interfaces that map to XML attributes may /// be set using the defaults parameter. The supplied XML must take into account /// any extra elements that are included by default. -void check( - XMLBase stanza, - dynamic criteria, { - bool useValues = true, -}) { +void check(Stanza stanza, Stanza copiedStanza, dynamic criteria) { late xml.XmlElement eksemel; - if (criteria is! XMLBase) { + if (criteria is! Stanza) { eksemel = parseXMLFromString(criteria as String); } else { - eksemel = criteria.element!; - } - - final stanza1 = stanza.copy(element: eksemel); - - if (useValues) { - final values = Map.from(stanza.values); - final stanza2 = stanza1.copy(); - stanza2.values = values; - - // print('stanza: $stanza'); - // print('stanza1: $stanza1'); - // print('stanza2: $stanza2'); - - compare( - eksemel, - elements: [ - stanza.element!, - stanza1.element!, - stanza2.element!, - ], - ); - return; + eksemel = criteria.toXML(); } compare( eksemel, elements: [ - stanza.element!, - stanza1.element!, + stanza.toXML(), + copiedStanza.toXML(), ], ); } From 038a597ce84aeddd15241a15b49a38564fc031ec Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:27 +0400 Subject: [PATCH 14/81] refactor(xml): Remove unused `DataForms` and `DiscoItem` classes --- lib/src/plugins/disco/disco.dart | 408 ---------------------------- lib/src/plugins/disco/item.dart | 186 ------------- lib/src/plugins/disco/static.dart | 230 ---------------- lib/src/plugins/form/dataforms.dart | 82 ------ 4 files changed, 906 deletions(-) delete mode 100644 lib/src/plugins/disco/disco.dart delete mode 100644 lib/src/plugins/disco/item.dart delete mode 100644 lib/src/plugins/disco/static.dart delete mode 100644 lib/src/plugins/form/dataforms.dart diff --git a/lib/src/plugins/disco/disco.dart b/lib/src/plugins/disco/disco.dart deleted file mode 100644 index 4a95150..0000000 --- a/lib/src/plugins/disco/disco.dart +++ /dev/null @@ -1,408 +0,0 @@ -import 'dart:async'; - -import 'package:dartz/dartz.dart'; - -import 'package:whixp/src/exception.dart'; -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/rsm/rsm.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; -import 'package:whixp/src/utils/utils.dart'; -import 'package:whixp/src/whixp.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'info.dart'; -part 'item.dart'; -part 'static.dart'; - -/// ## Service Discovery -/// -/// In the context of XMPP, Disco, which stands for "Service Discovery", is a -/// protocol extension that enables entities within an XMPP network to discover -/// information about the caps (capabilities) and features of other entities. -/// -/// The primary goal of this extension is to provide a mechanism for -/// discovering available services, features, and identities on the network. -/// -/// __See also__: [XEP-0030](http://www.xmpp.org/extensions/xep-0030.html) -class ServiceDiscovery extends PluginBase { - /// A hirearchy of dynamic node handlers, ranging from global handlers to - /// specialized JID+node handlers, is used by this plugin to operate. - /// - /// The handlers by default function in a satic way, retaining their data in - /// memory. - /// - /// [wrapResults] ensures that results are wrapped in an [IQ] stanza. - ServiceDiscovery({bool useCache = true, bool wrapResults = false}) - : super('disco', description: 'Service Discovery') { - _useCache = useCache; - _wrapResults = wrapResults; - } - - late final IQ _iq; - late final bool _useCache; - late final bool _wrapResults; - late final _StaticDisco _static; - Iterator>? _iterator; - - @override - void pluginInitialize() { - _iq = IQ(transport: base.transport); - _static = _StaticDisco(base); - _iterator = null; - - base.transport.registerHandler( - CallbackHandler( - 'Disco Info', - (stanza) => _handleDiscoveryInformation(stanza as IQ), - matcher: StanzaPathMatcher('iq/disco_info'), - ), - ); - - base.transport.registerHandler( - CallbackHandler( - 'Disco Items', - (stanza) => _handleDiscoveryItems(stanza as IQ), - matcher: StanzaPathMatcher('iq/disco_items'), - ), - ); - } - - /// Retrieve the disco#info results from a given JID/node combination. - /// - /// The return type is in [Future] type. This is because, if method tries to - /// get discovery information from local, then it returns by the proper - /// [XMLBase]. When method goes remote discovery, it will return `null` at the - /// end. But the result can be used using provided [callback] method. If there - /// is an error or timeout occured, then [failureCallback] or - /// [timeoutCallback] can be used respectively. - Future getInformation({ - JabberID? jid, - String node = '', - JabberID? iqFrom, - bool local = false, - bool cached = false, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) async { - bool localTemp = local; - - if (!local) { - if (jid != null) { - if (base.isComponent) { - if (jid.domain == base.transport.boundJID.domain) { - localTemp = true; - } - } else { - if (jid == base.transport.boundJID) { - localTemp = true; - } - } - } else if (jid == null) { - localTemp = true; - } - } - - if (base.isComponent && iqFrom == null) { - iqFrom = base.transport.boundJID; - } - - if (localTemp) { - Log.instance - .debug('Looking up local disco#info data for $jid, node $node'); - - DiscoveryInformation? information = - _static.getInformation(jid: jid, node: node, iqFrom: iqFrom); - - information = _fixDefaultInformation(information); - return _wrap(iqTo: iqFrom, iqFrom: jid, payload: information); - } - - if (cached) { - Log.instance - .debug('Looking up cached disco#info data for $jid, node $node'); - final information = _static.getCachedInformation(); - - if (information != null) { - return _wrap(iqTo: iqFrom, iqFrom: jid, payload: information); - } - } - - _iq['from'] = iqFrom; - _iq['to'] = jid; - _iq['type'] = 'get'; - (_iq['disco_info'] as XMLBase)['node'] = node; - - await _iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - ); - - return null; - } - - /// Retrieves the disco#items results from a given [jid]/[node] combination. - /// - /// Items can be obtained from both local and remote agents; the [local] - /// parameter specifies whether executing the local node handlers will gather - /// the items or whether generating and sending "disco#items" stanza is - /// required. - /// - /// If [iterator] is `true`, loads [RSM] and returns a result set iterator - /// using the [RSM] plugin. - Future getItems({ - JabberID? jid, - String? node, - JabberID? iqFrom, - bool local = false, - bool iterator = false, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) async { - if (local && (jid == null)) { - final items = _static.getItems(jid: jid, node: node, iqFrom: iqFrom); - return Future.value(_wrap(iqTo: iqFrom, iqFrom: jid, payload: items)); - } - - final iq = IQ(transport: base.transport); - iq['from'] = iqFrom != null ? iqFrom.toString() : ''; - iq['to'] = jid; - iq['type'] = 'get'; - (iq['disco_items'] as DiscoveryItems)['node'] = node ?? ''; - - final rsm = base.getPluginInstance('RSM'); - - if (iterator && rsm != null) { - _iterator ??= rsm.iterate(iq, 'disco_items').iterator; - - if (_iterator != null) { - final current = await _iterator!.current; - _iterator!.moveNext(); - return current; - } - } - - final response = Completer(); - - await iq.sendIQ(callback: (stanza) => response.complete(stanza)); - return response.future; - } - - /// Adds a new item to the given JID/node combination. - /// - /// Each item is required to have a Jabber ID, but may also specify a node - /// value to reference non-addressable entities. - /// - /// * [node] is the node to modify. - /// * [subnode] is optional node for the item. - void addItem({ - String? jid, - JabberID? itemJid, - String? name, - String? node, - String? subnode, - }) { - jid ??= base.transport.boundJID.full; - - _static.addItem( - jid: itemJid, - node: node, - data: {'itemJID': jid, 'name': name, 'node': subnode}, - ); - } - - /// Sets or replaces all items for the specified JID/node combination. - /// - /// The given items must be in a [SingleDiscoveryItem]s [Set]. - void setItems({ - JabberID? jid, - String? node, - JabberID? iqFrom, - required Set items, - }) => - _static.setItems(jid: jid, node: node, iqFrom: iqFrom, items: items); - - /// Adds a new identity to the given JID/node combination. - /// - /// Each identity must be unique in terms of all four identity components: - /// [category], [type], [name], and [language]. - void addIdentity({ - /// The identity's category - String category = '', - - /// The identity's type - String type = '', - - /// Optional name for identity - String name = '', - - /// The node to modify - String? node, - - /// Optional two-letter language code - String? language, - - /// The Jabber ID to modify - JabberID? jid, - }) { - return _static.addIdentity( - jid: jid, - node: node, - data: { - 'category': category, - 'type': type, - 'name': name, - 'language': language, - }, - ); - } - - /// Ensures that results are wrapped in an [IQ] stanza if [_wrapResults] has - /// been set to `true`. - XMLBase? _wrap({ - JabberID? iqTo, - JabberID? iqFrom, - XMLBase? payload, - bool force = false, - }) { - if ((force || _wrapResults) && payload is! IQ) { - final iq = IQ(); - - iq['to'] = - iqTo != null ? iqTo.toString() : base.transport.boundJID.toString(); - iq['from'] = iqFrom ?? base.transport.boundJID.toString(); - iq['type'] = 'result'; - iq.add(payload); - return iq; - } - - return payload; - } - - /// At least one identity and feature must be included in the "disco#info" - /// results for a [JabberID]. In the event that no additional identity is - /// supplied, [Whixp] will automatically utilize the bot client identity or - /// the generic component. - /// - /// At the standart "disco#info" feature will also be added if no features - /// are provided. - DiscoveryInformation _fixDefaultInformation( - DiscoveryInformation info, - ) { - if (info['node'] == null) { - if (info['identities'] == null) { - if (base.isComponent) { - Log.instance.debug( - 'No identity found for this entity, using default component entity', - ); - } - } - if (info['features'] == null) {} - } - - return info; - } - - /// Processes an incoming "disco#info" stanza. If it is a get request, find - /// and return the appropriate identities and features. - /// - /// If it is an items result, fire the "discoveryInformation" event. - void _handleDiscoveryInformation(IQ iq) { - if (iq['type'] == 'get') { - Log.instance.debug( - 'Received disco information query from ${iq['from']} to ${iq['to']}', - ); - DiscoveryInformation information = _static.getInformation( - jid: JabberID(iq['to'] as String), - node: (iq['disco_info'] as XMLBase)['node'] as String, - ); - - final node = (iq['disco_info'] as XMLBase)['node'] as String; - - final reply = iq.replyIQ(); - reply.transport = base.transport; - - information = _fixDefaultInformation(information); - information['node'] = node; - reply.setPayload([information.element!]); - reply.sendIQ(); - } else if (iq['type'] == 'result') { - late String? iqTo; - Log.instance.debug( - 'Received disco information result from ${iq['from']} to ${iq['to']}', - ); - - if (_useCache) { - Log.instance.debug( - 'Caching disco information result from ${iq['from']} to ${iq['to']}', - ); - if (base.isComponent) { - iqTo = JabberID(iq['to'] as String).full; - } else { - iqTo = null; - } - - _static.cacheInformation( - jid: JabberID(iq['from'] as String), - node: (iq['disco_info'] as XMLBase)['node'] as String, - iqFrom: iqTo, - stanza: iq, - ); - } - - base.transport.emit( - 'discoveryInformation', - data: iq['disco_info'] as DiscoveryInformation, - ); - } - } - - /// Adds a [feature] to a [jid]/[node] combination. - /// - /// [node] and [jid] are node and jid to modify respectively. - void addFeature(String feature, {JabberID? jid, String? node}) => - _static.addFeature(feature, jid: jid, node: node); - - /// Removes a [feature] from [jid]/[node] combination. - /// - /// [node] and [jid] are node and jid to modify respectively. - void removeFeature(String feature, {JabberID? jid, String? node}) => - _static.removeFeature(feature, jid: jid, node: node); - - /// Processes an incoming "disco#items" stanza. If it is a get request, find - /// and return the appropriate items. If it is an items result, fire the - /// "discoItems" event. - void _handleDiscoveryItems(IQ iq) { - if (iq['type'] == 'get') { - Log.instance.debug( - 'Received disco items query from ${iq['from']} to ${iq['to']}', - ); - } else if (iq['type'] == 'result') { - Log.instance.debug( - 'Received disco items result from ${iq['from']} to ${iq['to']}', - ); - base.transport.emit( - 'discoveryItems', - data: iq['disco_items'] as DiscoveryItems, - ); - } - } - - @override - void pluginEnd() => addFeature(WhixpUtils.getNamespace('DISCO_INFO')); - - @override - void sessionBind(String? jid) => - removeFeature(WhixpUtils.getNamespace('DISCO_INFO')); -} diff --git a/lib/src/plugins/disco/item.dart b/lib/src/plugins/disco/item.dart deleted file mode 100644 index 266bbdb..0000000 --- a/lib/src/plugins/disco/item.dart +++ /dev/null @@ -1,186 +0,0 @@ -part of 'disco.dart'; - -class DiscoItem extends XMLBase { - DiscoItem({super.includeNamespace = false, super.element, super.parent}) - : super( - name: 'item', - namespace: WhixpUtils.getNamespace('DISCO_ITEMS'), - pluginAttribute: 'item', - interfaces: {'jid', 'node', 'name'}, - getters: { - const Symbol('node'): (args, base) => base.getAttribute('node'), - const Symbol('name'): (args, base) => base.getAttribute('name'), - }, - ); - - @override - DiscoItem copy({xml.XmlElement? element, XMLBase? parent}) => DiscoItem( - includeNamespace: includeNamespace, - element: element, - parent: parent, - ); -} - -/// Represents an item used in the context of discovery. It is designed to hold -/// information related to a discovery item, including a [jid] in a [String] -/// format, [node], and a [name]. -class SingleDiscoveryItem { - /// Constructs an item with the provided [name], [node], and [name]. - const SingleDiscoveryItem(this.jid, {this.node, this.name}); - - /// The Jabber identifier with the discovery item. - final String jid; - - /// The node information associated with the discovery item. - final String? node; - - /// The name associated with the discovery item. - final String? name; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SingleDiscoveryItem && - runtimeType == other.runtimeType && - jid == other.jid && - node == other.node && - name == other.name; - - @override - int get hashCode => jid.hashCode ^ node.hashCode ^ name.hashCode; - - @override - String toString() => - 'Service Discovery Item (jid: $jid, node: $node, name: $name)'; -} - -class DiscoveryItems extends XMLBase { - DiscoveryItems({ - super.pluginAttributeMapping, - super.pluginTagMapping, - super.pluginIterables, - super.getters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'query', - namespace: WhixpUtils.getNamespace('DISCO_ITEMS'), - includeNamespace: true, - pluginAttribute: 'disco_items', - interfaces: {'node', 'items'}, - ) { - addGetters( - { - /// Returns all items. - const Symbol('items'): (args, base) => items, - }, - ); - - addSetters( - { - const Symbol('items'): (value, args, base) => - setItems(value as Set), - }, - ); - - addDeleters( - { - /// Returns all items. - const Symbol('items'): (args, base) => removeItems(), - }, - ); - - registerPlugin(DiscoItem(), iterable: true); - registerPlugin(RSMStanza()); - } - - final _items = >{}; - - /// Returns all items. - Set get items { - final items = {}; - for (final item in this['substanzas'] as List) { - if (item is DiscoItem) { - items.add( - SingleDiscoveryItem( - item['jid'] as String, - node: item['node'] as String?, - name: item['name'] as String?, - ), - ); - } - } - return items; - } - - /// Removes all items. - void removeItems() { - final items = {}; - for (final item in iterables) { - if (item is DiscoItem) { - items.add(item); - } - } - - for (final item in items) { - element!.children.remove(item.element); - iterables.remove(item); - } - } - - /// Sets or replaces all items. The given [items] ust in a [Set] where each - /// item is a [DiscoveryItem] form. - void setItems(Set items) { - removeItems(); - for (final item in items) { - addItem(item.jid, node: item.node, name: item.name); - } - } - - /// Adds a new item element. Each item is required to have Jabber ID, but may - /// also specify a [node] value to reference non-addressable entities. - bool addItem(String jid, {String? node, String? name}) { - if (!_items.contains(Tuple2(jid, node))) { - _items.add(Tuple2(jid, node)); - final item = DiscoItem(parent: this); - item['jid'] = jid; - item['node'] = node; - item['name'] = name; - iterables.add(item); - return true; - } - return false; - } - - /// Removes a single item. - bool removeItem(String jid, {String? node}) { - if (_items.contains(Tuple2(jid, node))) { - for (final itemElement - in element!.findAllElements('item', namespace: namespace)) { - final item = Tuple2( - itemElement.getAttribute('jid'), - itemElement.getAttribute('node'), - ); - if (item == Tuple2(jid, node)) { - element!.children.remove(itemElement); - return true; - } - } - } - - return false; - } - - @override - DiscoveryItems copy({xml.XmlElement? element, XMLBase? parent}) => - DiscoveryItems( - pluginAttributeMapping: pluginAttributeMapping, - pluginTagMapping: pluginTagMapping, - pluginIterables: pluginIterables, - getters: getters, - deleters: deleters, - element: element, - parent: parent, - ); -} diff --git a/lib/src/plugins/disco/static.dart b/lib/src/plugins/disco/static.dart deleted file mode 100644 index fda20f5..0000000 --- a/lib/src/plugins/disco/static.dart +++ /dev/null @@ -1,230 +0,0 @@ -part of 'disco.dart'; - -/// Most clients and basic bots just need to manage a few disco nodes that will -/// remain essentially static, but components will probably need fully dynamic -/// processing of service discovery information. -/// -/// A collection of node handlers that [_StaticDisco] offers will keep static -/// sets of discovery data and objects in memory. -class _StaticDisco { - /// Creates static discovery interface. Every possible combination of JID and - /// node is stored by sets of "disco#info" and "disco#items" stanzas. - /// - /// Without any further processing, discovery data is kept in mermoy for later - /// use in these stanzas. - _StaticDisco(this.whixp); - - /// The [WhixpBase] instance. Mostly used to access the current transport - /// instance. - final WhixpBase whixp; - final nodes = , Map>{}; - - Map addNode({JabberID? jid, String? node, String? iqFrom}) { - late JabberID nodeJID; - late String nodeIQFrom; - - if (jid == null) { - nodeJID = whixp.transport.boundJID; - } else { - nodeJID = jid; - } - if (iqFrom == null) { - nodeIQFrom = ''; - } else { - nodeIQFrom = iqFrom; - } - - node ??= ''; - - if (!nodes.containsKey(Tuple3(nodeJID, node, nodeIQFrom))) { - final info = DiscoveryInformation(); - final items = DiscoveryItems(); - - info['node'] = node; - items['node'] = node; - - nodes[Tuple3(nodeJID, node, nodeIQFrom)] = { - 'information': info, - 'items': items, - }; - } - - return nodes[Tuple3(nodeJID, node, nodeIQFrom)]!; - } - - Map getNode({JabberID? jid, String? node, String? iqFrom}) { - late JabberID nodeJID; - late String nodeIQFrom; - - if (jid == null) { - nodeJID = whixp.transport.boundJID; - } else { - nodeJID = jid; - } - - node ??= ''; - - if (iqFrom == null) { - nodeIQFrom = ''; - } else { - nodeIQFrom = iqFrom; - } - - if (!nodes.containsKey(Tuple3(nodeJID, node, nodeIQFrom))) { - addNode(jid: nodeJID, node: node, iqFrom: nodeIQFrom); - } - - return nodes[Tuple3(nodeJID, node, nodeIQFrom)]!; - } - - DiscoveryInformation getInformation({ - JabberID? jid, - String? node, - JabberID? iqFrom, - }) { - if (!nodeExists(jid: jid, node: node)) { - if (node == null || node.isEmpty) { - return DiscoveryInformation(); - } else { - throw StanzaException( - 'Missing item exception occured on disco information retrieval', - condition: 'item-not-found', - ); - } - } else { - return getNode(jid: jid, node: node)['information']! - as DiscoveryInformation; - } - } - - DiscoveryItems? getItems({ - JabberID? jid, - String? node, - JabberID? iqFrom, - }) { - if (!nodeExists(jid: jid, node: node)) { - if (node == null || node.isEmpty) { - return DiscoveryItems(); - } else { - throw StanzaException( - 'Missing item exception occured on disco information retrieval', - condition: 'item-not-found', - ); - } - } else { - return getNode(jid: jid, node: node)['items'] as DiscoveryItems?; - } - } - - /// Replaces the stored items data for a JID/node combination. - void setItems({ - JabberID? jid, - String? node, - JabberID? iqFrom, - required Set items, - }) { - final newNode = addNode(jid: jid, node: node); - (newNode['items']! as DiscoveryItems).setItems(items); - } - - /// Caches discovery information for an external jabber ID. - void cacheInformation({ - JabberID? jid, - String? node, - String? iqFrom, - XMLBase? stanza, - }) { - XMLBase? information; - if (stanza is IQ) { - information = stanza['disco_info'] as XMLBase; - } else { - information = stanza; - } - - final newNode = addNode(jid: jid, node: node, iqFrom: iqFrom); - newNode['information'] = information; - } - - /// Retrieves cached discovery information data. - DiscoveryInformation? getCachedInformation({ - JabberID? jid, - String? node, - String? iqFrom, - }) { - if (!nodeExists(jid: jid, node: node, iqFrom: iqFrom)) { - return null; - } - - return nodes[Tuple3(jid, node, iqFrom)]!['information']! - as DiscoveryInformation; - } - - bool nodeExists({ - JabberID? jid, - String? node, - String? iqFrom, - }) { - late JabberID nodeJID; - late String nodeIQFrom; - if (jid == null) { - nodeJID = whixp.transport.boundJID; - } else { - nodeJID = jid; - } - node ??= ''; - if (iqFrom == null) { - nodeIQFrom = ''; - } else { - nodeIQFrom = iqFrom; - } - - return nodes.containsKey(Tuple3(nodeJID, node, nodeIQFrom)); - } - - /// Adds a feature to a JID/node combination. - void addFeature(String feature, {JabberID? jid, String? node}) { - final newNode = addNode(jid: jid, node: node); - if (newNode['information'] != null) { - (newNode['information']! as DiscoveryInformation).addFeature(feature); - } - } - - /// Removes a feature from a JID/node combination. - void removeFeature(String feature, {JabberID? jid, String? node}) { - if (nodeExists(jid: jid, node: node)) { - if (getNode(jid: jid, node: node)['information'] != null) { - (getNode(jid: jid, node: node)['information']! as DiscoveryInformation) - .deleteFeature(feature); - } - } - } - - /// Adds an item to a JID/node combination. - void addItem({ - required Map data, - JabberID? jid, - String? node, - }) { - final newNode = addNode(jid: jid, node: node); - (newNode['items']! as DiscoveryItems).addItem( - data['itemJID']!, - name: data['name'] ?? '', - node: data['node'] ?? '', - ); - } - - /// Adds a new identity to the JID/node combination. - void addIdentity({ - required Map data, - JabberID? jid, - String? node, - }) { - final newNode = addNode(jid: jid, node: node); - (newNode['information']! as DiscoveryInformation).addIdentity( - data['category'] ?? '', - data['type'] ?? '', - name: data['name'], - language: data['language'], - ); - } -} diff --git a/lib/src/plugins/form/dataforms.dart b/lib/src/plugins/form/dataforms.dart deleted file mode 100644 index aa0ac6e..0000000 --- a/lib/src/plugins/form/dataforms.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/disco/disco.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'field.dart'; -part 'form.dart'; - -/// DataForms - XMPP plugin for XEP-0004: Data Forms -/// -/// This class is a plugin support for Data Forms (XEP-0004), which defines a -/// protocol for exchanging structured data through forms. This plugin extends -/// the functionality of [PluginBase] and includes methods for handling data -/// forms in the XMPP communication. -/// -/// ### Example: -/// ```dart -/// final whixp = Whixp(); -/// final form = DataForms(); -/// -/// whixp.registerPlugin(form); /// registered the [DataForms] plugin in the client -/// -/// final createdForm = form.createForm(); -/// ``` -class DataForms extends PluginBase { - /// Initializes the [DataForms] instance. It sets the plugin name to `forms` - /// and provides a description for the plugin. It is dependent to the plugin. - DataForms() - : super( - 'forms', - description: 'XEP-0004: Data Forms', - dependencies: {'disco'}, - ); - - @override - void pluginInitialize() { - base.transport.registerHandler( - CallbackHandler( - 'Data Form', - (stanza) => base.transport - .emit('messageForm', data: stanza['form'] as Form), - matcher: StanzaPathMatcher('message/form'), - ), - ); - } - - /// Creates a new Data Form. - Form createForm({ - String formType = 'form', - String title = '', - String instructions = '', - }) => - Form() - ..setType(formType) - ..['title'] = title - ..['instructions'] = instructions; - - @override - void sessionBind(String? jid) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco.addFeature(Form().namespace); - } - } - - @override - void pluginEnd() { - final disco = base.getPluginInstance( - 'disco', - enableIfRegistered: false, - ); - if (disco != null) { - disco.removeFeature(Form().namespace); - } - base.transport.removeHandler('Data Form'); - } -} From 081540db69c6a676d2dff4380ef7a655a8031d5b Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:35 +0400 Subject: [PATCH 15/81] refactor(xml): Add stream management stanza test cases --- test/sm_test.dart | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/sm_test.dart diff --git a/test/sm_test.dart b/test/sm_test.dart new file mode 100644 index 0000000..ecb9fdd --- /dev/null +++ b/test/sm_test.dart @@ -0,0 +1,39 @@ +import 'package:test/test.dart'; + +import 'package:whixp/src/plugins/sm/feature.dart'; + +import 'package:xml/xml.dart' as xml; + +void main() { + group('stream management stanza test cases', () { + test('answer stanza', () { + const answer = SMAnswer(h: 1); + expect(answer.toXMLString(), equals('')); + }); + + test('resumed stanza', () { + const resumedXML = + ''; + final resumed = + SMResumed.fromXML(xml.XmlDocument.parse(resumedXML).rootElement); + + expect(resumed.toXMLString(), equals(resumedXML)); + expect(resumed.h, equals(2)); + expect(resumed.previd, isNotNull); + expect(resumed.previd, equals('some-id')); + }); + + test('failed stanza', () { + const failedXML = + 'Item not found'; + final root = xml.XmlDocument.parse(failedXML).rootElement; + final failed = SMFailed.fromXML(root); + + expect(failed.name, equals('sm:failed')); + expect(failed.toXMLString(), equals(failedXML)); + expect(failed.cause, isNotNull); + expect(failed.cause!.content, equals('Item not found')); + expect(failed.cause!.name, equals('item-not-found')); + }); + }); +} From 5d186a64a951bbc124c7cadcdded0d43dbad2e75 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:40 +0400 Subject: [PATCH 16/81] refactor(scram): Update Whixp import in scram_test.dart --- test/scram_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/scram_test.dart b/test/scram_test.dart index a0a4e3e..bbe69de 100644 --- a/test/scram_test.dart +++ b/test/scram_test.dart @@ -1,11 +1,11 @@ import 'package:convert/convert.dart' as convert; import 'package:test/test.dart'; +import 'package:whixp/src/client.dart'; import 'package:whixp/src/exception.dart'; import 'package:whixp/src/sasl/scram.dart'; import 'package:whixp/src/utils/utils.dart'; -import 'package:whixp/src/whixp.dart'; void main() { group('hmacIteration method test', () { @@ -24,7 +24,7 @@ void main() { group('deriveKeys method test', () { test('must return correct client key value in the output', () { - final whixp = Whixp('vsevex@localhost', '', provideHivePath: true); + final whixp = Whixp(); final result = Scram(whixp).deriveKeys( password: 'pencil', salt: 'QSXCR+Q6sek8bf92', From 2e9c631ea1d80d614626c10e95e306c13e209ae1 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:46 +0400 Subject: [PATCH 17/81] refactor(xml): Remove unused code files --- test/rsm_test.dart | 146 ++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/test/rsm_test.dart b/test/rsm_test.dart index a9f0c84..d72034d 100644 --- a/test/rsm_test.dart +++ b/test/rsm_test.dart @@ -1,98 +1,98 @@ -import 'package:test/test.dart'; +// import 'package:test/test.dart'; -import 'package:whixp/src/plugins/rsm/rsm.dart'; +// import 'package:whixp/src/plugins/rsm/rsm.dart'; -import 'package:xml/xml.dart' as xml; +// import 'package:xml/xml.dart' as xml; -import 'test_base.dart'; +// import 'test_base.dart'; -void main() { - late RSMStanza rsm; +// void main() { +// late RSMStanza rsm; - setUp(() => rsm = RSMStanza()); +// setUp(() => rsm = RSMStanza()); - group('Result Set Management plugin stanza test cases', () { - test('must properly set first index', () { - rsm['first'] = 'id'; - rsm.setFirstIndex('10'); +// group('Result Set Management plugin stanza test cases', () { +// test('must properly set first index', () { +// rsm['first'] = 'id'; +// rsm.setFirstIndex('10'); - check( - rsm, - 'id', - ); - }); +// check( +// rsm, +// 'id', +// ); +// }); - test('must properly get first index', () { - const elementString = - 'id'; +// test('must properly get first index', () { +// const elementString = +// 'id'; - final stanza = - RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) - .firstIndex; - expect(stanza, equals('10')); - }); +// final stanza = +// RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) +// .firstIndex; +// expect(stanza, equals('10')); +// }); - test('must properly delete first index', () { - const elementString = - 'id'; +// test('must properly delete first index', () { +// const elementString = +// 'id'; - final stanza = - RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) - ..deleteFirstIndex(); +// final stanza = +// RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) +// ..deleteFirstIndex(); - check( - stanza, - 'id', - ); - }); +// check( +// stanza, +// 'id', +// ); +// }); - test('must properly set before interface', () { - rsm.setBefore(true); +// test('must properly set before interface', () { +// rsm.setBefore(true); - check(rsm, ''); - }); +// check(rsm, ''); +// }); - test('must return true if there is not any text associated', () { - const elementString = - ''; +// test('must return true if there is not any text associated', () { +// const elementString = +// ''; - final stanza = - RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement); +// final stanza = +// RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement); - expect(stanza.before, isTrue); - }); +// expect(stanza.before, isTrue); +// }); - test('remove before interface', () { - const elementString = - ''; +// test('remove before interface', () { +// const elementString = +// ''; - final stanza = - RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) - ..delete('before'); +// final stanza = +// RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement) +// ..delete('before'); - check( - stanza, - '', - ); - }); +// check( +// stanza, +// '', +// ); +// }); - test('must properly set before interface with value', () { - rsm['before'] = 'value'; +// test('must properly set before interface with value', () { +// rsm['before'] = 'value'; - check( - rsm, - 'value', - ); - }); +// check( +// rsm, +// 'value', +// ); +// }); - test('must return proper text associated', () { - const elementString = - 'value'; +// test('must return proper text associated', () { +// const elementString = +// 'value'; - final stanza = - RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement); +// final stanza = +// RSMStanza(element: xml.XmlDocument.parse(elementString).rootElement); - expect(stanza['before'], equals('value')); - }); - }); -} +// expect(stanza['before'], equals('value')); +// }); +// }); +// } From cdc4b96a9d291355b0ffc4041ad502a09a5ed9b1 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:57:53 +0400 Subject: [PATCH 18/81] refactor(xml): Remove unused code files and update Whixp import in scram_test.dart --- test/pubsub_test.dart | 472 +++++------------------------------------- 1 file changed, 57 insertions(+), 415 deletions(-) diff --git a/test/pubsub_test.dart b/test/pubsub_test.dart index 70f6a70..c988734 100644 --- a/test/pubsub_test.dart +++ b/test/pubsub_test.dart @@ -1,437 +1,79 @@ import 'package:test/test.dart'; -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/message.dart'; +import 'package:whixp/src/plugins/pubsub/pubsub.dart'; import 'package:xml/xml.dart' as xml; -import 'test_base.dart'; - void main() { - late IQ iq; - late Message message; - - setUp(() { - iq = IQ(generateID: false); - message = Message(); - }); - - group( - 'pubsub owner stanza set cases', - () { - test( - 'default stanza for pubsub owner test cases', - () { - final owner = iq['pubsub_owner'] as PubSubOwnerStanza; - final def = owner['default'] as PubSubOwnerDefaultConfig; - def['node'] = 'testnode'; - (def['form'] as Form).addField( - variable: 'pubsub#title', - formType: 'text-single', - value: 'bang-bang', - ); - - check( - iq, - 'bang-bang', - useValues: false, - ); - }, - ); - - test( - 'must properly set iq/pubsub_owner/delete stanza', - () { - final owner = iq['pubsub_owner'] as PubSubOwnerStanza; - (owner['delete'] as PubSubOwnerDelete)['node'] = 'cartNode'; - check( - iq, - '', - ); - }, - ); - }, - ); - - group('pubsub stanza test cases', () { - test( - 'must set properly nested iq/pubsub/affiliations/affiliation', - () { - final aff1 = PubSubAffiliation(); - aff1['node'] = 'testnodefirst'; - aff1['affiliation'] = 'owner'; - final aff2 = PubSubAffiliation(); - aff2['node'] = 'testnodesecond'; - aff2['affiliation'] = 'publisher'; - ((iq['pubsub'] as PubSubStanza)['affiliations'] as PubSubAffiliations) - ..add(aff1) - ..add(aff2); - - check( - iq, - '', - ); - }, - ); - - test( - 'must set properly nested iq/pubsub/subsriptions/subscription', - () { - final sub1 = PubSubSubscription(); - sub1['node'] = 'testnodefirst'; - sub1['jid'] = JabberID('alyosha@localhost/mobile'); - final sub2 = PubSubSubscription(); - sub2['node'] = 'testnodesecond'; - sub2['jid'] = JabberID('vsevex@example.com/desktop'); - sub2['subscription'] = 'subscribed'; - ((iq['pubsub'] as PubSubStanza)['affiliations'] as PubSubAffiliations) - ..add(sub1) - ..add(sub2); - - check( - iq, - '', - ); - }, - ); - - test( - 'must set properly nested iq/pubsub/subscription/subscribe-options', - () { - final subscription = (iq['pubsub'] as PubSubStanza)['subscription'] - as PubSubSubscription; - (subscription['suboptions'] as PubSubSubscribeOptions)['required'] = - true; - subscription['node'] = 'testnode'; - subscription['jid'] = JabberID('vsevex@example.com/desktop'); - subscription['subscription'] = 'unconfigured'; - check( - iq, - '', - useValues: false, - ); - }, - ); - - test( - 'must set properly nested iq/pubsub/items stanza', - () { - final pubsub = iq['pubsub'] as PubSubStanza; - (pubsub['items'] as PubSubItems)['node'] = 'cart'; - final payload = xml.XmlDocument.parse( - '', - ).rootElement; - final item = PubSubItem(); - item['id'] = 'id1'; - item['payload'] = payload; - final itemSecond = PubSubItem(); - itemSecond['id'] = 'id2'; - (pubsub['items'] as PubSubItems) - ..add(item) - ..add(itemSecond); - check( - iq, - '', - ); - }, - ); - - test( - 'must set properly nested iq/pubsub/create&configure stanza', - () { - final pubsub = iq['pubsub'] as PubSubStanza; - (pubsub['create'] as PubSubCreate)['node'] = 'testnode'; - ((pubsub['configure'] as PubSubConfigure)['form'] as Form).addField( - variable: 'pubsub#title', - formType: 'text-single', - value: 'cartu desu', - ); - - check( - iq, - 'cartu desu', - ); - }, - ); - - test( - 'must set properly nested iq/pubsub/subscribe stanza', - () { - final pubsub = iq['pubsub'] as PubSubStanza; - final subscribe = pubsub['subscribe'] as PubSubSubscribe; - subscribe['jid'] = JabberID('alyosha@example.com/desktop'); - subscribe['node'] = 'firstnode'; - final options = subscribe['options'] as PubSubOptions; - options['node'] = 'optionsnode'; - options['jid'] = JabberID('vsevex@localhost/embedded'); - final form = Form(); - form['type'] = 'submit'; - form.addField( - variable: 'pubsub#title', - formType: 'text-single', - value: 'bang-bang', - ); - options['options'] = form; - - check( - iq, - 'bang-bang', - useValues: false, - ); - }, - ); - - test( - 'must properly get config from full create', - () { - iq['to'] = 'alyosha@example.com'; - iq['from'] = 'vsevex@localhost/desktop'; - iq['type'] = 'set'; - iq['id'] = 'someId'; + group('pep tune test cases', () { + test('must properly parse the given tune', () { + const stanzaString = + 'Yes6868YessongsHeart of the Sunrise3http://www.yesworld.com/lyrics/Fragile.html#9'; + final stanza = xml.XmlDocument.parse(stanzaString).rootElement; + + final tune = Tune.fromXML(stanza); + + expect(tune.artist, isNotNull); + expect(tune.artist, equals('Yes')); + expect(tune.length, isNotNull); + expect(tune.length, equals(686)); + expect(tune.rating, isNotNull); + expect(tune.rating, equals(8)); + expect(tune.source, isNotNull); + expect(tune.source, equals('Yessongs')); + expect(tune.title, isNotNull); + expect(tune.title, equals('Heart of the Sunrise')); + expect(tune.track, isNotNull); + expect(tune.track, equals('3')); + expect(tune.uri, isNotNull); + expect(tune.uri, equals('http://www.yesworld.com/lyrics/Fragile.html#9')); + }); - final pub = iq['pubsub'] as PubSubStanza; - final configure = pub['configure'] as PubSubConfigure; - (pub['create'] as PubSubCreate)['node'] = 'testnode'; - final form = configure['form'] as Form; - form.setType('submit'); - form.setFields(>{ - 'FORM_TYPE': { - 'type': 'hidden', - 'value': 'http://jabber.org/protocol/pubsub#node_config', - }, - 'pubsub#node_type': { - 'type': 'list-single', - 'label': 'Select the node type', - 'value': 'leaf', - }, - 'pubsub#title': { - 'type': 'text-single', - 'label': 'Name for the node', - }, - 'pubsub#deliver_notifications': { - 'type': 'boolean', - 'label': 'Event notifications', - 'value': true, - }, - 'pubsub#deliver_payloads': { - 'type': 'boolean', - 'label': 'Deliveer payloads with event notifications', - 'value': true, - }, - 'pubsub#notify_config': { - 'type': 'boolean', - 'label': 'Notify subscribers when the node configuration changes', - }, - 'pubsub#notify_delete': { - 'type': 'boolean', - 'label': 'Notify subscribers when the node is deleted', - }, - 'pubsub#notify_retract': { - 'type': 'boolean', - 'label': 'Notify subscribers when items are removed from the node', - 'value': true, - }, - 'pubsub#publish_model': { - 'type': 'list-single', - 'label': 'Specify the publisher model', - 'value': 'publishers', - }, - }); + test('must properly convert the given xml to the tune stanza', () { + const stanzaString = + 'Yes6868YessongsHeart of the Sunrise3http://www.yesworld.com/lyrics/Fragile.html#9'; + final stanza = xml.XmlDocument.parse(stanzaString).rootElement; - check( - iq, - 'http://jabber.org/protocol/pubsub#node_configleaf111publishers', - ); - }, - ); + final parsed = Tune.fromXML(stanza); + final fromParsed = parsed.toXML(); - test( - 'must properly set message/pubsub_event/items/item stanza', - () { - final item = PubSubEventItem(); - final element = xml.XmlElement( - xml.XmlName('test'), - [ - xml.XmlAttribute(xml.XmlName('failed'), '3'), - xml.XmlAttribute(xml.XmlName('passed'), '24'), - ], - )..setAttribute('xmlns', 'http://cartcurt.org/protocol/test'); - item['payload'] = element; - item['id'] = 'someID'; - final items = (message['pubsub_event'] as PubSubEvent)['items'] - as PubSubEventItems; - items.add(item); - items['node'] = 'foo'; - message['type'] = 'normal'; + expect(fromParsed.toXmlString(), equals(stanzaString)); + }); - check( - message, - '', - ); - }, - ); + test('must properly create stanza from constructor', () { + const tune = + Tune(artist: 'vsevex', length: 543, rating: 10, title: 'Without you'); - test('multiple message/pubsub_event/items/item stanza', () { - final item = PubSubEventItem(); - final itemSecond = PubSubEventItem(); - final payload = xml.XmlElement( - xml.XmlName('test'), - [ - xml.XmlAttribute(xml.XmlName('failed'), '3'), - xml.XmlAttribute(xml.XmlName('passed'), '24'), - ], - )..setAttribute('xmlns', 'http://cartcurt.org/protocol/test'); - final payloadSecond = xml.XmlElement( - xml.XmlName('test'), - [ - xml.XmlAttribute(xml.XmlName('total'), '10'), - xml.XmlAttribute(xml.XmlName('passed'), '27'), - ], - )..setAttribute('xmlns', 'http://cartcurt.org/protocol/test-other'); - itemSecond['payload'] = payloadSecond; - item['payload'] = payload; - item['id'] = 'firstID'; - itemSecond['id'] = 'secondID'; - final items = - (message['pubsub_event'] as PubSubEvent)['items'] as PubSubEventItems; - items - ..add(item) - ..add(itemSecond); - items['node'] = 'foo'; + final toString = tune.toXMLString(); - check( - message, - '', + expect( + toString, + 'vsevex54310Without you', ); }); + }); - test( - 'must properly set message/pubsub_event/items/item && retract mix', - () { - final item = PubSubEventItem(); - final itemSecond = PubSubEventItem(); - final payload = xml.XmlElement( - xml.XmlName('test'), - [ - xml.XmlAttribute(xml.XmlName('failed'), '3'), - xml.XmlAttribute(xml.XmlName('passed'), '24'), - ], - )..setAttribute('xmlns', 'http://cartcurt.org/protocol/test'); - final payloadSecond = xml.XmlElement( - xml.XmlName('test'), - [ - xml.XmlAttribute(xml.XmlName('total'), '10'), - xml.XmlAttribute(xml.XmlName('passed'), '27'), - ], - )..setAttribute('xmlns', 'http://cartcurt.org/protocol/test-other'); - itemSecond['payload'] = payloadSecond; - final retract = PubSubEventRetract(); - retract['id'] = 'retractID'; - item['payload'] = payload; - item['id'] = 'firstItemID'; - itemSecond['id'] = 'secondItemID'; - final items = (message['pubsub_event'] as PubSubEvent)['items'] - as PubSubEventItems; - items - ..add(item) - ..add(retract) - ..add(itemSecond); - items['node'] = 'bar'; - message.normal(); - - check( - message, - '', - ); - }, - ); - - test( - 'must properly set message/pubsub_event/collection/associate stanza', - () { - final collection = (message['pubsub_event'] - as PubSubEvent)['collection'] as PubSubEventCollection; - (collection['associate'] as PubSubEventAssociate)['node'] = 'foo'; - collection['node'] = 'node'; - message['type'] = 'headline'; - - check( - message, - '', - ); - }, - ); - - test( - 'must properly set message/pubsub_event/collection/disassociate stanza', - () { - final collection = (message['pubsub_event'] - as PubSubEvent)['collection'] as PubSubEventCollection; - (collection['disassociate'] as PubSubEventDisassociate)['node'] = 'foo'; - collection['node'] = 'node'; - message['type'] = 'headline'; - - check( - message, - '', - ); - }, - ); + group('pep mood test cases', () { + test('must properly parse from the given xml string', () { + const stanzaString = + 'Yay, the mood spec has been approved!'; - test( - 'must properly set message/pubsub_event/configuration/config stanza', - () { - final configuration = (message['pubsub_event'] - as PubSubEvent)['configuration'] as PubSubEventConfiguration; - configuration['node'] = 'someNode'; - (configuration['form'] as Form).addField( - variable: 'pubsub#title', - formType: 'text-single', - value: 'Some Value', - ); - message['type'] = 'headline'; + final mood = + Mood.fromXML(xml.XmlDocument.parse(stanzaString).rootElement); - check( - message, - 'Some Value', - ); - }, - ); + expect(mood.text, equals('Yay, the mood spec has been approved!')); + expect(mood.value, equals('happy')); + }); - test( - 'must properly set message/pubsub_event/purge stanza', - () { - final purge = (message['pubsub_event'] as PubSubEvent)['purge'] - as PubSubEventPurge; - purge['node'] = 'someNode'; - message['type'] = 'headline'; + test('must properly convert the given xml to the mood stanza', () { + const stanzaString = + 'Yay, the mood spec has been approved!'; + final stanza = xml.XmlDocument.parse(stanzaString).rootElement; - check( - message, - '', - ); - }, - ); + final parsed = Mood.fromXML(stanza); + final fromParsed = parsed.toXML(); - test( - 'must properly set message/pubsub_event/subscription stanza', - () { - final subscription = (message['pubsub_event'] - as PubSubEvent)['subscription'] as PubSubEventSubscription; - subscription['node'] = 'someNode'; - subscription['jid'] = JabberID('vsevex@exmaple.com/mobile'); - subscription['subid'] = 'someID'; - subscription['subscription'] = 'subscribed'; - subscription['expiry'] = 'presence'; - message['type'] = 'headline'; - check( - message, - '', - useValues: false, - ); - }, - ); + expect(fromParsed.toXmlString(), equals(stanzaString)); + }); }); } From 1d5e94a8051f39cda2f888d4fde0b4e64c858a97 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:00 +0400 Subject: [PATCH 19/81] refactor(xml): Add message stanza test cases and update imports in message_test.dart --- test/message_test.dart | 45 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/test/message_test.dart b/test/message_test.dart index c67a37d..6f58ef9 100644 --- a/test/message_test.dart +++ b/test/message_test.dart @@ -1,19 +1,46 @@ import 'package:test/test.dart'; +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/jid/jid.dart'; +import 'package:whixp/src/stanza/error.dart'; import 'package:whixp/src/stanza/message.dart'; void main() { group('message stanza test cases', () { - test('groupchat regression reply stanza should barejid', () { - final message = Message(); + test('generate message stanza', () { + final message = Message(body: 'salam', subject: 'greeting') + ..type = messageTypeChat + ..to = JabberID('vsevex@localhost') + ..from = JabberID('alyosha@localhost') + ..id = '5'; - message['to'] = 'vsevex@localhost'; - message['from'] = 'hall@service.localhost/alyosha'; - message['type'] = 'groupchat'; - message['body'] = 'salam'; - - final newMessage = message.replyMessage(); - expect(newMessage['to'], equals('hall@service.localhost')); + final xml = message.toXML(); + final parsed = Message.fromXML(xml); + expect(parsed.toXMLString(), message.toXMLString()); + expect(parsed.subject, isNotNull); + expect(parsed.subject, 'greeting'); + expect(parsed.body, isNotNull); + expect(parsed.body, 'salam'); + expect(parsed.type, isNotNull); + expect(parsed.type, 'chat'); }); + + test( + 'must decode message when there is error in message stanza', + () { + const String message = + ''; + + final error = ErrorStanza.fromString( + '', + ); + + final parsed = Message.fromString(message); + expect(parsed.error, isNotNull); + expect(parsed.error, error); + expect(parsed.error!.type, isNotNull); + expect(parsed.error!.type, errorCancel); + }, + ); }); } From fb7d79f4d6de39f4042f726a5f753751ee5cfeea Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:06 +0400 Subject: [PATCH 20/81] refactor(presence): Update presence_test.dart with improved test cases and imports --- test/presence_test.dart | 79 ++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/test/presence_test.dart b/test/presence_test.dart index 97b8d84..4b67fa0 100644 --- a/test/presence_test.dart +++ b/test/presence_test.dart @@ -1,31 +1,70 @@ import 'package:test/test.dart'; +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/jid/jid.dart'; import 'package:whixp/src/stanza/presence.dart'; -import 'test_base.dart'; - void main() { - group('Presence stanza test cases', () { - test('regression check presence["type"] = "dnd" show value working', () { + group('presence stanza test cases', () { + test('must generate presence and attach attributes', () { final presence = Presence(); - presence['type'] = 'dnd'; - check(presence, 'dnd'); + + presence + ..type = presenceShowChat + ..from = JabberID('vsevex@localhost') + ..to = JabberID('alyosha@localhost') + ..id = 'someID'; + + final xml = presence.toXML(); + expect( + xml.toString(), + '', + ); + + final fromString = Presence.fromString( + '', + ); + expect(fromString.to, isNotNull); + expect(fromString.to, equals(JabberID('alyosha@localhost'))); + expect(fromString.from, isNotNull); + expect(fromString.from, equals(JabberID('vsevex@localhost'))); + expect(fromString.id, isNotNull); + expect(fromString.id, equals('someID')); }); - test('properly manipulate presence type', () { - final presence = Presence(); - presence['type'] = 'available'; - check(presence, ''); - expect(presence['type'], 'available'); - - for (final showtype in {'away', 'chat', 'dnd', 'xa'}) { - presence['type'] = showtype; - check(presence, '$showtype'); - expect(presence['type'], showtype); - } - - presence.delete('type'); - check(presence, ''); + test('presence sub elements test case', () { + final presence = Presence( + show: presenceShowAway, + status: 'Sleeping', + priority: 10, + ); + + presence + ..type = presenceShowChat + ..from = JabberID('vsevex@localhost') + ..to = JabberID('alyosha@localhost') + ..id = 'someID'; + + final xml = presence.toXML(); + final fromXML = Presence.fromXML(xml); + + expect(fromXML.show, isNotNull); + expect(fromXML.show, equals(presence.show)); + expect(fromXML.status, isNotNull); + expect(fromXML.status, equals(presence.status)); + expect(fromXML.priority, isNotNull); + expect(fromXML.priority, equals(presence.priority)); + }); + + test('presence with error substanza', () { + final presence = Presence.fromString( + '', + ); + final fromXML = Presence.fromXML(presence.toXML()); + + expect(fromXML.error, isNotNull); + expect(fromXML.error!.code, 404); + expect(fromXML.error!.type, errorCancel); }); }); } From 559fdd4d92c93e2df319ec58edfba047c39eaf27 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:12 +0400 Subject: [PATCH 21/81] refactor(jid): Update jid_test.dart with necessary imports --- test/jid_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jid_test.dart b/test/jid_test.dart index bfb214c..87f1094 100644 --- a/test/jid_test.dart +++ b/test/jid_test.dart @@ -1,4 +1,5 @@ import 'package:test/test.dart'; + import 'package:whixp/src/jid/jid.dart'; void main() { From 0997373fe7c53b003054aa7a9504f9cf6216ab2d Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:21 +0400 Subject: [PATCH 22/81] refactor(xml): Update static.dart with XMPP stanza constants --- lib/src/_static.dart | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/src/_static.dart diff --git a/lib/src/_static.dart b/lib/src/_static.dart new file mode 100644 index 0000000..7572c86 --- /dev/null +++ b/lib/src/_static.dart @@ -0,0 +1,61 @@ +import 'package:whixp/src/utils/utils.dart'; + +const String $errorStanza = 'error'; +const String $presence = 'presence'; +const String $bind = 'bind'; +const String $message = 'message'; +const String $handshake = 'handshake'; +const String $rosterQuery = 'query'; +const String $rosterItem = 'rosterItem'; + +const String iqTypeError = "error"; +const String iqTypeGet = "get"; +const String iqTypeResult = "result"; +const String iqTypeSet = "set"; + +const String messageTypeChat = "chat"; +const String messageTypeError = "error"; +const String messageTypeGroupchat = "groupchat"; +const String messageTypeHeadline = "headline"; +const String messageTypeNormal = "normal"; + +const String presenceTypeError = "error"; +const String presenceTypeProbe = "probe"; +const String presenceTypeSubscribe = "subscribe"; +const String presenceTypeSubscribed = "subscribed"; +const String presenceTypeUnavailable = "unavailable"; +const String presenceTypeUnsubscribe = "unsubscribe"; +const String presenceTypeUnsubscribed = "unsubscribed"; + +const String presenceShowChat = 'chat'; +const String presenceShowAway = 'away'; +const String presenceShowDnd = 'dnd'; +const String presenceShowXa = 'xa'; + +const String errorAuth = 'auth'; +const String errorCancel = 'cancel'; +const String errorContinue = 'continue'; +const String errorModify = 'modify'; +const String errorWait = 'wait'; + +String get discoInformationTag => + '{${WhixpUtils.getNamespace('DISCO_INFO')}}query'; +String get discoItemsTag => '{${WhixpUtils.getNamespace('DISCO_ITEMS')}}query'; +String get bindTag => '{${WhixpUtils.getNamespace('BIND')}}bind'; +String get versionTag => '{jabber:iq:version}query'; +String get formsTag => '{${WhixpUtils.getNamespace('FORMS')}}x'; +String get tuneTag => '{http://jabber.org/protocol/tune}tune'; +String get moodTag => '{http://jabber.org/protocol/mood}mood'; +String get pubsubTag => '{${WhixpUtils.getNamespace('PUBSUB')}}pubsub'; +String get pubsubOwnerTag => + '{${WhixpUtils.getNamespace('PUBSUB')}#owner}pubsub'; +String get pubsubEventTag => + '{${WhixpUtils.getNamespace('PUBSUB')}#event}event'; +String get vCard4Tag => '{urn:ietf:params:xml:ns:vcard-4.0}vcard'; +String get vCardTag => '{vcard-temp}vCard'; +String get adhocCommandTag => '{http://jabber.org/protocol/commands}command'; +String get enableTag => '{urn:xmpp:push:0}enable'; +String get disableTag => '{urn:xmpp:push:0}disable'; +String get delayTag => '{urn:xmpp:delay}delay'; +String get stanzaIDTag => '{urn:xmpp:sid:0}stanza-id'; +String get originIDTag => '{urn:xmpp:sid:0}origin-id'; From af0b8896de3c52896d632e0ad312f301d6a5d1b4 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:29 +0400 Subject: [PATCH 23/81] refactor(extensions): Add extension methods for WhixpBase class --- lib/src/_extensions.dart | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/src/_extensions.dart diff --git a/lib/src/_extensions.dart b/lib/src/_extensions.dart new file mode 100644 index 0000000..200feac --- /dev/null +++ b/lib/src/_extensions.dart @@ -0,0 +1,51 @@ +part of 'whixp.dart'; + +extension SASLExtension on WhixpBase { + /// Requested [JabberID] from the passed jabber ID. + JabberID get requestedJID => _requestedJID; + + /// The sasl data keeper. Works with [SASL] class and keeps various data(s) + /// that can be used accross package. + Map get saslData => _saslData; + + Transport get transport => _transport; + + /// [Session] getter. + Session? get session => _session; + + /// Stream namespace. + String get streamNamespace => _streamNamespace; + + /// Default namespace. + String get defaultNamespace => _defaultNamespace; + + /// [Session] setter. + set session(Session? session) => _session = session; + + List> get streamFeatureOrder => _streamFeatureOrder; + + Map Function(Packet features), bool>> + get streamFeatureHandlers => _streamFeatureHandlers; + + /// Registers a stream feature handler. + void registerFeature( + String name, + FutureOr Function(Packet features) handler, { + bool restart = false, + int order = 5000, + }) { + /// Check beforehand if the corresponding feature is not in the list. + if (_streamFeatureOrder + .where((feature) => feature.secondValue == name) + .isEmpty) { + _registerFeature(name, handler, restart: restart, order: order); + } + } + + /// Map holder for the given user properties for the connection. + Map get credentials => _credentials; + + /// Setter for _credentials. + set credentials(Map credentials) => + _credentials = credentials; +} From 2bce1bd01c99a1625cc2bf6e88298e543d72f540 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:34 +0400 Subject: [PATCH 24/81] refactor(xml): Add XMLParser class for decoding XMPP stanzas --- lib/src/parser.dart | 97 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 lib/src/parser.dart diff --git a/lib/src/parser.dart b/lib/src/parser.dart new file mode 100644 index 0000000..f4d824c --- /dev/null +++ b/lib/src/parser.dart @@ -0,0 +1,97 @@ +import "package:whixp/src/exception.dart"; +import "package:whixp/src/plugins/features.dart"; +import "package:whixp/src/stanza/error.dart"; +import "package:whixp/src/stanza/iq.dart"; +import "package:whixp/src/stanza/message.dart"; +import "package:whixp/src/stanza/mixins.dart"; +import "package:whixp/src/stanza/presence.dart"; +import "package:whixp/src/utils/utils.dart"; + +import "package:xml/xml.dart" as xml; + +// ignore: avoid_classes_with_only_static_members +/// A utility class for parsing XML elements and decoding them into +/// corresponding XMPP stanzas. +class XMLParser { + /// Constructs an instance of [XMLParser]. + const XMLParser(); + + /// Decodes the next XMPP packet based on the provided XML element and + /// namespace. + /// + /// If [namespace] is not provided, the default namespace for XMPP client is + /// used. Throws a [WhixpInternalException] if the namespace is unknown. + static Packet nextPacket(xml.XmlElement node, {String? namespace}) { + final ns = + node.namespaceUri ?? namespace ?? WhixpUtils.getNamespace('CLIENT'); + switch (ns) { + case 'jabber:client': + return _decodeClient(node); + case 'http://etherx.jabber.org/streams': + return _decodeStream(node); + case 'urn:ietf:params:xml:ns:xmpp-sasl': + return _decodeSASL(node); + case 'urn:xmpp:sm:3': + return StreamManagement.parse(node); + default: + throw WhixpInternalException.unknownNamespace(ns); + } + } + + /// Decodes an XMPP packet from the 'urn:ietf:params:xml:ns:xmpp-streams' + /// namespace. + /// + /// Throws a [WhixpInternalException] if the XML element's local name does + /// not match expected packets. + static Packet _decodeStream(xml.XmlElement node) { + switch (node.localName) { + case 'error': + return ErrorStanza.fromXML(node); + case 'features': + return StreamFeatures.fromXML(node); + default: + throw WhixpInternalException.unexpectedPacket( + node.getAttribute('xmlns'), + node.localName, + ); + } + } + + /// Decodes an XMPP packet from the 'jabber:client' namespace. + /// + /// Throws a [WhixpInternalException] if the XML element's local name does + /// not match expected packets. + static Packet _decodeClient(xml.XmlElement node) { + switch (node.localName) { + case 'message': + return Message.fromXML(node); + case 'presence': + return Presence.fromXML(node); + case 'iq': + return IQ.fromXML(node); + default: + throw WhixpInternalException.unexpectedPacket( + node.namespaceUri, + node.localName, + ); + } + } + + static Packet _decodeSASL(xml.XmlElement node) { + switch (node.localName) { + case 'challenge': + return SASLChallenge.fromXML(node); + case 'response': + return SASLResponse.fromXML(node); + case 'success': + return SASLSuccess.fromXML(node); + case 'failure': + return SASLFailure.fromXML(node); + default: + throw WhixpInternalException.unexpectedPacket( + node.namespaceUri, + node.localName, + ); + } + } +} From 3216d634a8f1b7fcf77b35e96aa6e31a299bd2a7 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:39 +0400 Subject: [PATCH 25/81] refactor(reconnection): Add RandomBackoffReconnectionPolicy class --- lib/src/reconnection.dart | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 lib/src/reconnection.dart diff --git a/lib/src/reconnection.dart b/lib/src/reconnection.dart new file mode 100644 index 0000000..ed59cb3 --- /dev/null +++ b/lib/src/reconnection.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:synchronized/synchronized.dart'; + +import 'package:whixp/src/log/log.dart'; + +typedef PerformReconnectFunction = Future Function(); + +abstract class ReconnectionPolicy { + /// Function provided by XmppConnection that allows the policy + /// to perform a reconnection. + PerformReconnectFunction? performReconnect; + + final Lock _lock = Lock(); + + /// Indicate if a reconnection attempt is currently running. + bool _isReconnecting = false; + + /// Indicate if should try to reconnect. + bool _shouldAttemptReconnection = false; + + Future canTryReconnecting() async => + _lock.synchronized(() => !_isReconnecting); + + Future getIsReconnecting() async => + _lock.synchronized(() => _isReconnecting); + + Future _resetIsReconnecting() => + _lock.synchronized(() => _isReconnecting = false); + + /// In case the policy depends on some internal state, this state must be reset + /// to an initial state when reset is called. In case timers run, they must be + /// terminated. + Future reset() => _resetIsReconnecting(); + + Future canTriggerFailure() => _lock.synchronized(() { + if (_shouldAttemptReconnection && !_isReconnecting) { + _isReconnecting = true; + return true; + } + + return false; + }); + + /// Called by the XmppConnection when the reconnection failed. + Future onFailure() async {} + + /// Caled by the XmppConnection when the reconnection was successful. + Future onSuccess(); + + Future getShouldReconnect() async { + return _lock.synchronized(() => _shouldAttemptReconnection); + } + + /// Set whether a reconnection attempt should be made. + Future setShouldReconnect(bool value) => + _lock.synchronized(() => _shouldAttemptReconnection = value); +} + +/// A simple reconnection strategy: Make the reconnection delays exponentially longer +/// for every failed attempt. +/// NOTE: This ReconnectionPolicy may be broken +class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { + RandomBackoffReconnectionPolicy( + this._minBackoffTime, + this._maxBackoffTime, + ) : assert( + _minBackoffTime < _maxBackoffTime, + '_minBackoffTime must be smaller than _maxBackoffTime', + ), + super(); + + /// The maximum time in seconds that a backoff should be. + final int _maxBackoffTime; + + /// The minimum time in seconds that a backoff should be. + final int _minBackoffTime; + + /// Backoff timer. + Timer? _timer; + + /// Logger. + final Lock _timerLock = Lock(); + + /// Called when the backoff expired + Future onTimerElapsed() async { + Log.instance.info('Timer elapsed. Waiting for lock...'); + final shouldContinue = await _timerLock.synchronized(() async { + if (_timer == null) { + Log.instance + .warning('The timer is already set to null. Doing nothing.'); + return false; + } + + if (!(await getIsReconnecting())) return false; + + if (!(await getShouldReconnect())) return false; + + _timer?.cancel(); + _timer = null; + return true; + }); + + if (!shouldContinue) return; + + Log.instance.info('Reconnecting...'); + await performReconnect!(); + } + + @override + Future reset() async { + Log.instance.info('Resetting reconnection policy'); + await _timerLock.synchronized(() { + _timer?.cancel(); + _timer = null; + }); + await super.reset(); + } + + @override + Future onFailure() async { + final seconds = math.Random().nextInt(_maxBackoffTime - _minBackoffTime) + + _minBackoffTime; + Log.instance + .info('Failure occured. Starting random backoff with ${seconds}s'); + + await _timerLock.synchronized(() { + _timer?.cancel(); + _timer = Timer(Duration(seconds: seconds), onTimerElapsed); + }); + } + + @override + Future onSuccess() async { + await reset(); + } + + bool isTimerRunning() => _timer != null; +} From 90dc178abf74ee9376c2544e272359a605115db0 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:45 +0400 Subject: [PATCH 26/81] refactor(database): Add HiveController class for managing interaction with Hive storage --- lib/src/database/controller.dart | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lib/src/database/controller.dart diff --git a/lib/src/database/controller.dart b/lib/src/database/controller.dart new file mode 100644 index 0000000..027296a --- /dev/null +++ b/lib/src/database/controller.dart @@ -0,0 +1,71 @@ +import 'package:hive/hive.dart'; + +import 'package:whixp/src/log/log.dart'; +import 'package:whixp/src/session.dart'; +import 'package:whixp/src/stanza/stanza.dart'; + +// ignore: avoid_classes_with_only_static_members +/// Manages interaction with a Hive storage box for storing key-value pairs. +class HiveController { + /// Constructs a [HiveController]. + HiveController(); + + /// The static storage box for key-value pairs. + static Box>? _smBox; + + /// Static storage box for unacked stanza records. + static Box? _unackeds; + + /// Initializes the storage box. + /// + /// If [path] is provided, the storage box will be opened at the specified + /// path. + static Future initialize([String? path]) async { + if (_smBox != null) { + Log.instance.warning( + 'Tried to reinitialize sm box, but it is already initialized', + ); + } + if (_unackeds != null) { + Log.instance.warning( + 'Tried to reinitialize unacked stanzas box, but it is already initialized', + ); + } + _smBox = await Hive.openBox>('sm', path: path); + _unackeds = await Hive.openBox('unacked', path: path); + return; + } + + /// Writes a key-value pair to the storage box. + /// + /// [jid] is the key associated with the provided [state]. + static Future writeToSMBox(String jid, Map state) => + _smBox!.put(jid, state); + + /// Writes a key-value pair to the storage box. + /// + /// [sequence] is the key associated with the provided unacked [stanza]. + static Future writeUnackeds(int sequence, Stanza stanza) => + _unackeds!.put(sequence, stanza.toXMLString()); + + /// All available key-value pairs for lcaolly saved unacked stanzas. + static Map get unackeds => _unackeds!.toMap(); + + /// Pops from the unacked stanzas list. + static String? popUnacked() { + final unacked = _unackeds?.getAt(0); + _unackeds?.deleteAt(0); + return unacked; + } + + /// Reads a key-value pair from the storage box. + /// + /// [jid] is the key associated with the value to be read. + /// Returns a [Future] that completes with the [SMState] associated with [jid], + /// or `null` if [jid] does not exist in the storage box. + static Future readFromSMBox(String jid) async { + final data = _smBox?.get(jid); + if (data == null) return null; + return SMState.fromJson(Map.from(_smBox!.get(jid)!)); + } +} From 79b70e569cece9555f693d92d5303c34991f8368 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:49 +0400 Subject: [PATCH 27/81] refactor(matcher): Add Matcher classes for packet filtering --- lib/src/handler/matcher.dart | 142 +++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 lib/src/handler/matcher.dart diff --git a/lib/src/handler/matcher.dart b/lib/src/handler/matcher.dart new file mode 100644 index 0000000..329d136 --- /dev/null +++ b/lib/src/handler/matcher.dart @@ -0,0 +1,142 @@ +import 'package:whixp/src/stanza/iq.dart'; +import 'package:whixp/src/stanza/message.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/presence.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; +import 'package:xml/xpath.dart'; + +/// An abstract class representing a matcher for packet filtering. +abstract class Matcher { + /// Constructs a matcher with an optional [name]. + const Matcher(this.name); + + /// The name of the matcher. + final String? name; + + /// Checks if the given [packet] matches the criteria defined by the matcher. + bool match(Packet packet); +} + +class IQIDMatcher extends Matcher { + // Constructor that initializes the id and calls the superclass constructor + IQIDMatcher(this.id) : super('id-matcher'); + + // Instance variable to store the ID + final String id; + + // Override the match method to implement custom matching logic + @override + bool match(Packet packet) { + // Check if the packet is not an IQ packet, return false if it's not + if (packet is! IQ) return false; + // Check if the packet's ID matches the provided ID + return packet.id == id; + } +} + +/// A matcher that matches packets based on their name. +class NameMatcher extends Matcher { + /// Constructs a name matcher with the specified [name]. + NameMatcher(super.name); + + @override + bool match(Packet packet) => name == packet.name; +} + +class SuccessAndFailureMatcher extends Matcher { + SuccessAndFailureMatcher(this.sf) : super('success-failure-matcher'); + + final Tuple2 sf; + + @override + bool match(Packet packet) => + packet.name == sf.firstValue || packet.name == sf.secondValue; +} + +/// Type of [Matcher] designed to match packets based on their XML structure. +/// +/// It extends the functionality of the [Matcher] class by providing a mechanism +/// to check if a packet's XML representation has a specific depth of nested +/// elements. +class DescendantMatcher extends Matcher { + /// Creates an instance of DescendantMatcher with the specified nesting + /// [names]. + DescendantMatcher(this.names) : super('descendant-matcher'); + + /// Names of nesting (e.g. "message/event/items") to check for in the packet's + /// XML structure. + final String names; + + @override + bool match(Packet packet) { + final descendants = names.split('/'); + late int level = 1; + + if (packet.name != descendants.first) return false; + + try { + for (final name in descendants.sublist(1)) { + final element = xml.XmlDocument.parse( + packet.toXML().xpath('/*' * (level + 1)).first.toXmlString(), + ).rootElement; + + if (element.localName == name) { + level++; + } + } + + if (level == descendants.length) return true; + + return false; + } catch (_) { + return false; + } + } +} + +/// A matcher that matches packets based on their stanza type and namespace. +class NamespaceTypeMatcher extends Matcher { + /// Constructs a namespace-type matcher with the specified [types]. + NamespaceTypeMatcher(this.types) : super(null); + + /// The list of stanza types to match. + final List types; + + @override + bool match(Packet packet) { + String? type; + + switch (packet.runtimeType) { + case IQ _: + type = (packet as IQ).type; + case Presence _: + type = (packet as Presence).type; + case Message _: + type = (packet as Message).type; + type = (type?.isEmpty ?? true) ? 'normal' : type; + default: + return false; + } + + if (type?.isEmpty ?? true) return false; + return types.contains(type); + } +} + +/// A matcher that matches IQ packets based on their namespaces. +class NamespaceIQMatcher extends Matcher { + /// Constructs a namespace IQ matcher with the specified [types]. + NamespaceIQMatcher(this.types) : super(null); + + /// The list of IQ packet namespaces to match. + final List types; + + @override + bool match(Packet packet) { + if (packet is! IQStanza) return false; + return types.contains(packet.namespace); + } +} From ba23d8d6a041958ca9c41d823e4aec36a1b2d75f Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:58:53 +0400 Subject: [PATCH 28/81] refactor(router): Add Router class for handling incoming packets --- lib/src/handler/router.dart | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/src/handler/router.dart diff --git a/lib/src/handler/router.dart b/lib/src/handler/router.dart new file mode 100644 index 0000000..5f49dcf --- /dev/null +++ b/lib/src/handler/router.dart @@ -0,0 +1,63 @@ +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/handler/handler.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/transport.dart'; +import 'package:whixp/whixp.dart'; + +// ignore: avoid_classes_with_only_static_members +/// A router for handling incoming packets by matching them with registered +/// handlers. +class Router { + const Router(); + + /// List of registered handlers for routing packets. + static final _handlers = []; + + /// Routes the incoming [packet] by matching it with registered handlers. + /// + /// If a matching handler is found, the packet is processed accordingly. + /// + /// If no matching handler is found, it checks if the packet is an IQ stanza + /// with 'get' or 'set' type, and responds with a `feature-not-implemented` + /// error if necessary. + static void route(Packet packet) { + if (_match(packet)) { + return; + } + + if (packet is IQ && [iqTypeGet, iqTypeSet].contains(packet.type)) { + return _notImplemented(packet); + } + } + + /// Adds a [handler] to the list of registered handlers. + static void addHandler(Handler handler) => _handlers.add(handler); + + /// Removes a [handler] from the registered handlers list. + static void removeHandler(String name) => + _handlers.removeWhere((handler) => handler.name == name); + + /// Clears all registerd route handlers. + static void clearHandlers() => _handlers.clear(); + + /// Matches the incoming [packet] with registered handlers. + /// + /// Returns `true` if a matching handler is found, otherwise `false`. + static bool _match(Packet packet) { + for (final handler in _handlers) { + final match = handler.match(packet); + if (match) return true; + } + return false; + } + + /// Sends a feature-not-implemented error response for the unhandled [iq]. + static void _notImplemented(IQ iq) { + final error = ErrorStanza(); + error.code = 501; + error.type = errorCancel; + error.reason = 'feature-not-implemented'; + iq.makeError(error); + Transport.instance().send(iq); + } +} From 2b5e50f0a78cc69cc385b8308c822f6cfa2d107e Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:15 +0400 Subject: [PATCH 29/81] refactor(iq): Add IQ stanza test cases and update imports in iq_test.dart --- test/iq_test.dart | 109 ++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/test/iq_test.dart b/test/iq_test.dart index 218cff6..66fb28a 100644 --- a/test/iq_test.dart +++ b/test/iq_test.dart @@ -1,62 +1,95 @@ import 'package:test/test.dart'; +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/jid/jid.dart'; +import 'package:whixp/src/plugins/disco/info.dart'; +import 'package:whixp/src/plugins/disco/items.dart'; import 'package:whixp/src/stanza/error.dart'; import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; -import 'package:xml/xml.dart' as xml; - -import 'test_base.dart'; - void main() { group('IQ stanza test cases', () { - test('initializing default IQ values', () { - final iq = IQ(); - check(iq, ''); - }); + test('unmarshalling IQs', () { + const iq = ''; + final stanza = IQ() + ..type = 'set' + ..to = JabberID('vsevex@localhost') + ..id = '5'; - test('setting iq stanza payload must work properly', () { - final iq = IQ(); - iq.setPayload( - [WhixpUtils.xmlElement('test', namespace: 'tester')], - ); - check(iq, ''); + final parsed = IQ.fromString(iq); + + expect(parsed, equals(stanza)); }); - test('test behavior for unhandled method', () { - final iq = IQ(); - final error = StanzaError(); - registerStanzaPlugin(iq, error); + test('must properly generate IQ stanza', () { + final iq = IQ() + ..type = iqTypeResult + ..from = JabberID('alyosha@localhost') + ..to = JabberID('vsevex@localhost') + ..id = '21'; + + final payload = DiscoInformation(); + payload.addIdentity('Test Gateway', 'gateway', type: 'mqtt'); + payload.addFeature([ + WhixpUtils.getNamespace('disco_info'), + WhixpUtils.getNamespace('disco_items'), + ]); - iq['id'] = 'test'; - (iq['error'] as XMLBase)['condition'] = 'feature-not-implemented'; - (iq['error'] as XMLBase)['text'] = 'No handlers registered'; + iq.payload = payload; + + final xml = iq.toXML(); + final parsedIQ = IQ.fromXML(xml); + expect(iq.toXMLString(), parsedIQ.toXMLString()); }); - test('must properly modify query element of IQ stanzas', () { - final iq = IQ(); + test('error tag', () { + final error = ErrorStanza(); + error.code = 503; + error.type = errorCancel; + error.reason = 'service-unavailable'; + error.text = 'User session not found'; - iq['query'] = 'namespace'; - check(iq, ''); + final xml = error.toXML(); + final parsed = ErrorStanza.fromXML(xml); + expect(error, parsed); + }); - iq['query'] = 'namespace2'; - check(iq, ''); + test('disco items case', () { + final iq = IQ() + ..type = iqTypeGet + ..from = JabberID('vsevex@localhost') + ..to = JabberID('alyosha@localhost') + ..id = '4'; - expect(iq['query'], equals('namespace2')); + final payload = DiscoItems(node: 'music'); + iq.payload = payload; - iq.delete('query'); - check(iq, ''); - }); + final xml = iq.toXMLString(); + expect( + xml, + equals( + '', + ), + ); - test('must properly set "result" in reply stanza', () { - final iq = IQ(); - iq['to'] = 'vsevex@localhost'; - iq['type'] = 'get'; + final parsed = IQ.fromXML(iq.toXML()); + expect(parsed.toXMLString(), iq.toXMLString()); + }); - final newIQ = iq.replyIQ(); + test('unmarshalling payload', () { + const String query = + ''; + final iq = IQ.fromString(query); + expect(iq.payload, isNotNull); - check(newIQ, ''); + final namespace = (iq.payload! as IQStanza).namespace; + expect(namespace, 'jabber:iq:version'); + expect( + iq.payload!.toXMLString(), + equals(''), + ); }); }); } From 2193fa1b6582d145434ada41993e241f59d46987 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:21 +0400 Subject: [PATCH 30/81] refactor(error): Add ErrorStanza test cases and update imports in error_test.dart --- test/error_test.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/error_test.dart diff --git a/test/error_test.dart b/test/error_test.dart new file mode 100644 index 0000000..24e34cf --- /dev/null +++ b/test/error_test.dart @@ -0,0 +1,34 @@ +import 'package:test/test.dart'; + +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/stanza/error.dart'; + +void main() { + group('error stanza test cases', () { + test('must generate presence and attach attributes', () { + final error = ErrorStanza(); + + error + ..code = 404 + ..type = errorCancel + ..reason = "item-not-found" + ..text = "Item not found"; + + final xml = error.toXML(); + + expect( + xml.toString(), + equals( + 'item-not-foundItem not found', + ), + ); + final fromXML = ErrorStanza.fromXML(xml); + expect(fromXML.code, isNotNull); + expect(fromXML.code, 404); + expect(fromXML.reason, isNotNull); + expect(fromXML.reason, 'item-not-found'); + expect(fromXML.type, isNotNull); + expect(fromXML.type, errorCancel); + }); + }); +} From 7501be5107a7a14abd2b4f81e214996b72805019 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:26 +0400 Subject: [PATCH 31/81] refactor(tests): Remove commented out test cases in echotils_test.dart --- test/echotils_test.dart | 93 ----------------------------------------- 1 file changed, 93 deletions(-) diff --git a/test/echotils_test.dart b/test/echotils_test.dart index b3a9520..775e7c3 100644 --- a/test/echotils_test.dart +++ b/test/echotils_test.dart @@ -185,97 +185,4 @@ void main() { expect(result, expected); }); }); - - // group('hasAttr method test cases', () { - // final testClass = PropertyTestClass(); - - // test( - // 'object must have the specified property', - // () => expect(WhixpUtils.hasAttr(testClass, 'firstProperty'), isTrue), - // ); - - // test( - // 'object must not have the specified property', - // () => expect( - // WhixpUtils.hasAttr(testClass, 'nonexistingproperty'), - // isFalse, - // ), - // ); - - // test('object is null', () { - // const Object? object = null; - // expect(WhixpUtils.hasAttr(object, 'someProperty'), isFalse); - // }); - - // test( - // 'property name is an empty string', - // () => expect(WhixpUtils.hasAttr(testClass, ''), isFalse), - // ); - - // test( - // 'must return true when object has nullable property', - // () => expect(WhixpUtils.hasAttr(testClass, 'nullProperty'), isTrue), - // ); - - // test( - // 'must return true when class contains specified method', - // () => expect(WhixpUtils.hasAttr(testClass, 'intMethod'), isTrue), - // ); - // }); - - // group('getAttr method test cases', () { - // final testClass = PropertyTestClass(); - - // test('must get property value', () { - // final property = WhixpUtils.getAttr(testClass, 'firstProperty'); - // expect(property, equals(42)); - // }); - - // test('must get method result', () { - // final method = WhixpUtils.getAttr(testClass, 'intMethod'); - // expect(method, isA()); - - // final result = (method as Function()).call(); - // expect(result, equals(0)); - // }); - - // test( - // 'attribute does not exist', - // () => - // expect(WhixpUtils.getAttr(testClass, 'nonexistentproperty'), isNull), - // ); - - // test( - // 'object is null', - // () => expect(WhixpUtils.getAttr(null, 'nonexistentproperty'), isNull), - // ); - // }); - - // group('setAttr method test cases', () { - // final testClass = PropertyTestClass(); - - // test('sets property value', () { - // expect(WhixpUtils.getAttr(testClass, 'firstProperty'), equals(42)); - - // WhixpUtils.setAttr(testClass, 'firstProperty', 50); - - // expect(WhixpUtils.getAttr(testClass, 'firstProperty'), equals(50)); - // }); - - // test( - // 'throws error for non-existent property', - // () => expect( - // () => WhixpUtils.setAttr(testClass, 'nonExistent', 'value'), - // throwsArgumentError, - // ), - // ); - - // test( - // 'throws error for null object', - // () => expect( - // () => WhixpUtils.setAttr(null, 'firstProperty', 40), - // throwsArgumentError, - // ), - // ); - // }); } From 027305b887fcfce98a5d15ca02bab248985cb9d5 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:31 +0400 Subject: [PATCH 32/81] refactor: Add Router, Matcher, HiveController, RandomBackoffReconnectionPolicy, XMLParser, and extension classes --- test/disco_test.dart | 354 +++---------------------------------------- 1 file changed, 18 insertions(+), 336 deletions(-) diff --git a/test/disco_test.dart b/test/disco_test.dart index 523b8dd..fe5ca81 100644 --- a/test/disco_test.dart +++ b/test/disco_test.dart @@ -1,6 +1,6 @@ import 'package:test/test.dart'; -import 'package:whixp/src/plugins/disco/disco.dart'; +import 'package:whixp/src/plugins/plugins.dart'; import 'package:whixp/src/stanza/iq.dart'; import 'test_base.dart'; @@ -8,374 +8,56 @@ import 'test_base.dart'; void main() { late IQ iq; - setUp(() => iq = IQ(generateID: false)); + setUp(() => iq = IQ()); group('disco extension stanza creating and manipulating test cases', () { test('disco#info query without node', () { - (iq['disco_info'] as DiscoveryInformation)['node'] = ''; + iq.payload = DiscoInformation(node: ''); check( iq, + IQ.fromXML(iq.toXML()), '', ); }); test('disco#info query with a node', () { - (iq['disco_info'] as DiscoveryInformation)['node'] = 'cart'; + iq.payload = DiscoInformation(node: 'info'); check( iq, - '', + IQ.fromXML(iq.toXML()), + '', ); }); test('must properly add identity to disco#info', () { - (iq['disco_info'] as DiscoveryInformation) - .addIdentity('conference', 'text', name: 'room', language: 'en'); + final info = DiscoInformation(); + info.addIdentity('conference', 'room', type: 'text'); + iq.payload = info; check( iq, - '', + IQ.fromXML(iq.toXML()), + '', ); }); test( 'must keep first identity when adding multiple copies of the same category and type combination', () { - (iq['disco_info'] as DiscoveryInformation) - .addIdentity('conference', 'text', name: 'MUC'); - (iq['disco_info'] as DiscoveryInformation) - .addIdentity('conference', 'text', name: 'room'); + final info = DiscoInformation(); + info + ..addIdentity('conference', 'MUC', type: 'text') + ..addIdentity('conference', 'room', type: 'text'); + iq.payload = info; check( iq, + IQ.fromXML(iq.toXML()), '', ); }, ); - - test('previous test, but language property added', () { - (iq['disco_info'] as DiscoveryInformation) - .addIdentity('conference', 'text', name: 'MUC', language: 'en'); - (iq['disco_info'] as DiscoveryInformation) - .addIdentity('conference', 'text', name: 'room', language: 'en'); - - check( - iq, - '', - ); - }); - - test('remove identites from a disco#info stanza', () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc') - ..addIdentity('client', 'bot') - ..deleteIdentity('client', 'bot'); - - check( - iq, - '', - ); - }); - - test('remove identities from a disco#info stanza with language', () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc') - ..addIdentity('client', 'bot', language: 'az') - ..deleteIdentity('client', 'bot'); - - check( - iq, - '', - ); - }); - - test('remove all identities from a disco#info stanza', () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc', name: 'PC') - ..addIdentity('client', 'pc', language: 'az') - ..addIdentity('client', 'bot'); - - (iq['disco_info'] as DiscoveryInformation).delete('identities'); - check( - iq, - '', - ); - }); - - test('remove all identities with provided language from disco#info stanza', - () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc', name: 'PC') - ..addIdentity('client', 'pc', language: 'az') - ..addIdentity('client', 'bot'); - - (iq['disco_info'] as DiscoveryInformation) - .deleteIdentities(language: 'az'); - }); - - test('must add multiple identities at once', () { - const identities = [ - DiscoveryIdentity('client', 'pc', name: 'PC', language: 'az'), - DiscoveryIdentity('client', 'bot', name: 'Bot'), - ]; - - (iq['disco_info'] as DiscoveryInformation).setIdentities(identities); - - check( - iq, - '', - ); - }); - - test('selectively replace identities based on language', () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc', language: 'en') - ..addIdentity('client', 'pc', language: 'az') - ..addIdentity('client', 'bot', language: 'ru'); - - const identities = [ - DiscoveryIdentity('client', 'pc', name: 'Bot', language: 'ru'), - DiscoveryIdentity('client', 'bot', name: 'Bot', language: 'en'), - ]; - - (iq['disco_info'] as DiscoveryInformation) - .setIdentities(identities, language: 'ru'); - - check( - iq, - '', - ); - }); - - test('getting all identities from a disco#info stanza', () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc') - ..addIdentity('client', 'pc', language: 'az') - ..addIdentity('client', 'pc', language: 'ru') - ..addIdentity('client', 'pc', language: 'en'); - - expect( - (iq['disco_info'] as DiscoveryInformation).getIdentities(), - equals({ - const DiscoveryIdentity('client', 'pc', language: 'en'), - const DiscoveryIdentity('client', 'pc', language: 'ru'), - const DiscoveryIdentity('client', 'pc', language: 'az'), - const DiscoveryIdentity('client', 'pc'), - }), - ); - }); - - test('getting all identities of a given language from a disco#info stanza', - () { - (iq['disco_info'] as DiscoveryInformation) - ..addIdentity('client', 'pc') - ..addIdentity('client', 'pc', language: 'az') - ..addIdentity('client', 'pc', language: 'ru') - ..addIdentity('client', 'pc', language: 'en'); - - expect( - (iq['disco_info'] as DiscoveryInformation) - .getIdentities(language: 'en'), - equals({const DiscoveryIdentity('client', 'pc', language: 'en')}), - ); - }); - - test('must correctly add feature to disco#info', () { - (iq['disco_info'] as DiscoveryInformation) - ..addFeature('foo') - ..addFeature('bar'); - - check( - iq, - '', - ); - }); - - test('must correctly handle adding duplicate feature to disco#info', () { - (iq['disco_info'] as DiscoveryInformation) - ..addFeature('foo') - ..addFeature('bar') - ..addFeature('foo'); - - check( - iq, - '', - ); - }); - - test('must properly remove feature from disco', () { - (iq['disco_info'] as DiscoveryInformation) - ..addFeature('foo') - ..addFeature('bar') - ..addFeature('foo'); - - (iq['disco_info'] as DiscoveryInformation).deleteFeature('foo'); - - check( - iq, - '', - ); - }); - - test('get all features from disco#info', () { - (iq['disco_info'] as DiscoveryInformation) - ..addFeature('foo') - ..addFeature('bar') - ..addFeature('foo'); - - final features = (iq['disco_info'] as DiscoveryInformation)['features']; - expect(features, equals({'foo', 'bar'})); - }); - - test('must properly remove all features from a disco#info', () { - (iq['disco_info'] as DiscoveryInformation) - ..addFeature('foo') - ..addFeature('bar') - ..addFeature('baz'); - - (iq['disco_info'] as DiscoveryInformation).delete('features'); - check( - iq, - '', - ); - }); - - test('add multiple features at once', () { - final features = {'foo', 'bar', 'baz'}; - - (iq['disco_info'] as DiscoveryInformation)['features'] = features; - - check( - iq, - '', - ); - }); - }); - - group('discovery items test cases', () { - test('must properly add features to disco#info', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com') - ..addItem('vsevex@example.com', node: 'foo') - ..addItem('vsevex@example.com', node: 'bar', name: 'cart'); - - check( - iq, - '', - useValues: false, - ); - }); - - test('add items with the same JID without any nodes', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com', name: 'cart') - ..addItem('vsevex@example.com', name: 'hert'); - - check( - iq, - '', - useValues: false, - ); - }); - - test('add items with the same JID nodes', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com', name: 'cart', node: 'foo') - ..addItem('vsevex@example.com', name: 'hert', node: 'foo'); - - check( - iq, - '', - useValues: false, - ); - }); - - test('remove items without nodes from stanza', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com') - ..addItem('vsevex@example.com', node: 'foo') - ..addItem('alyosha@example.com') - ..removeItem('vsevex@example.com'); - - check( - iq, - '', - useValues: false, - ); - }); - - test('remove items with nodes from stanza', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com') - ..addItem('vsevex@example.com', node: 'foo') - ..addItem('alyosha@example.com') - ..removeItem('vsevex@example.com', node: 'foo'); - - check( - iq, - '', - useValues: false, - ); - }); - - test('must properly get all items', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com') - ..addItem('vsevex@example.com', node: 'foo') - ..addItem('alyosha@example.com', node: 'bar', name: 'cart'); - - expect( - ((iq['disco_items']) as DiscoveryItems)['items'], - equals({ - const SingleDiscoveryItem('vsevex@example.com', node: '', name: ''), - const SingleDiscoveryItem( - 'vsevex@example.com', - node: 'foo', - name: '', - ), - const SingleDiscoveryItem( - 'alyosha@example.com', - node: 'bar', - name: 'cart', - ), - }), - ); - }); - - test('must properly remove all items', () { - (iq['disco_items'] as DiscoveryItems) - ..addItem('vsevex@example.com') - ..addItem('vsevex@example.com', node: 'foo') - ..addItem('alyosha@example.com', node: 'bar', name: 'cart'); - - (iq['disco_items'] as DiscoveryItems).removeItems(); - - check( - iq, - '', - useValues: false, - ); - }); - - test('must properly set all items', () { - final items = { - const SingleDiscoveryItem('vsevex@example.com'), - const SingleDiscoveryItem('vsevex@example.com', node: 'foo'), - const SingleDiscoveryItem( - 'alyosha@example.com', - node: 'bar', - name: 'cart', - ), - }; - - (iq['disco_items'] as DiscoveryItems)['items'] = items; - - check( - iq, - '', - useValues: false, - ); - }); }); } From a2148785fbfa423de853b0917840c0fcf4f54ba0 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:48 +0400 Subject: [PATCH 33/81] refactor: Add Router, Matcher, HiveController, RandomBackoffReconnectionPolicy, and XMLParser classes --- test/dataforms_test.dart | 264 ++++++++++++++------------------------- 1 file changed, 91 insertions(+), 173 deletions(-) diff --git a/test/dataforms_test.dart b/test/dataforms_test.dart index 906d80a..d1362ea 100644 --- a/test/dataforms_test.dart +++ b/test/dataforms_test.dart @@ -1,6 +1,6 @@ import 'package:test/test.dart'; +import 'package:whixp/src/plugins/form/form.dart'; -import 'package:whixp/src/plugins/form/dataforms.dart'; import 'package:whixp/src/stanza/message.dart'; import 'test_base.dart'; @@ -12,212 +12,130 @@ void main() { group('data forms extension stanza manipulation test cases', () { test( - 'must properly set multiple instructions elements in a data form', + 'must properly set instructions element in a data form', () { - (message['form'] as Form)['instructions'] = - 'Instructions\nSecond batch'; + message.addPayload(Form(instructions: 'Instructions')); check( message, - 'InstructionsSecond batch', + Message.fromXML(message.toXML()), + 'Instructions', ); }, ); - test('add field to a data form', () { - final form = message['form'] as Form; - form.addField( - variable: 'aa', - formType: 'text-single', - label: 'cart', - description: 'A cart field', - required: true, - value: 'inner value', - ); + test( + 'add field to a data form', + () { + final field = Field( + variable: 'a', + type: FieldType.textSingle, + label: 'label', + description: 'some description', + required: true, + values: ['inner value'], + ); - check( - message, - 'inner valueA cart field', - ); + final form = Form()..addFields([field]); - final fields = >{}; - fields['v1'] = { - 'type': 'text-single', - 'label': 'Username', - 'required': true, - }; + message.addPayload(form); - form.setFields(fields); + check( + message, + Message.fromXML(message.toXML()), + 'inner valuesome description', + ); - check( - message, - 'inner valueA cart field', - ); + final secondField = Field( + variable: 'aa', + type: FieldType.textSingle, + label: 'Username', + required: true, + ); - fields.clear(); - fields['v2'] = { - 'type': 'text-private', - 'label': 'Password', - 'required': true, - }; - fields['v3'] = { - 'type': 'text-multi', - 'label': 'Message', - 'value': 'Enter message.\nCartu desu', - }; - fields['v4'] = { - 'type': 'list-single', - 'label': 'Message Type', - 'options': >[ - {'label': 'gup', 'value': 'salam'}, - ], - }; - - form.setFields(fields); + form.addFields([secondField]); - check( - message, - 'inner valueA cart fieldEnter message.Cartu desu', - ); - }); + check( + message, + Message.fromXML(secondField.toXML()), + 'inner valuesome description', + ); + + form.clearFields(); + + check( + message, + Message.fromXML(message.toXML()), + '', + ); + }, + ); test('must properly set form values', () { - final form = message['form'] as Form; + final form = Form(); + final firstField = Field(variable: 'foo', type: FieldType.textSingle); + firstField.values.add('salam'); + final secondField = Field(variable: 'bar', type: FieldType.listMulti); + secondField.values.addAll(['a', 'b', 'c']); - form - ..addField(variable: 'foo', formType: 'text-single') - ..addField(variable: 'bar', formType: 'list-multi') - ..setValues({ - 'foo': 'salam', - 'bar': ['a', 'b', 'c'], - }); + form.addFields([firstField, secondField]); + + message.addPayload(form); check( message, + Message.fromXML(message.toXML()), 'salamabc', ); }); test('setting type to "submit" must clear extra details', () { - final form = message['form'] as Form; - final fields = >{}; - fields['v1'] = { - 'type': 'text-single', - 'label': 'Username', - 'required': true, - }; - fields['v2'] = { - 'type': 'text-private', - 'label': 'Password', - 'required': true, - }; - fields['v3'] = { - 'type': 'text-multi', - 'label': 'Message', - 'value': 'Enter message.\nA long one even.', - }; - fields['v4'] = { - 'type': 'list-single', - 'label': 'Message Type', - 'options': [ - { - 'label': 'gup!', - 'value': 'cart', - }, - {'label': 'heh', 'value': 'lerko'}, - ], - }; - - form - ..setFields(fields) - ..setType('submit') - ..setValues({ - 'v1': 'username', - 'v2': 'passwd', - 'v3': 'Message\nagain', - 'v4': 'helemi', - }); - - check( - message, - 'usernamepasswdMessageagainhelemi', + final form = Form(type: FormType.submit); + final first = Field( + type: FieldType.textSingle, + variable: 'v1', + label: 'Username', + required: true, ); - }); - - test('cancel type test', () { - final form = message['form'] as Form; - final fields = >{}; - fields['v1'] = { - 'type': 'text-single', - 'label': 'Username', - 'required': true, - }; - fields['v2'] = { - 'type': 'text-private', - 'label': 'Password', - 'required': true, - }; - fields['v3'] = { - 'type': 'text-multi', - 'label': 'Message', - 'value': 'Enter message.\nA long one even.', - }; - fields['v4'] = { - 'type': 'list-single', - 'label': 'Message Type', - 'options': [ - { - 'label': 'gup!', - 'value': 'cart', - }, - {'label': 'heh', 'value': 'lerko'}, - ], - }; - - form.setFields(fields); - form.setType('cancel'); + final second = Field( + variable: 'v2', + type: FieldType.textPrivate, + label: 'Password', + required: true, + ); + final third = Field( + variable: 'v3', + type: FieldType.textMulti, + label: 'Message', + ); + third.values.add('Enter message'); + final fourth = Field( + variable: 'v4', + type: FieldType.listSingle, + label: 'Message Type', + ); + fourth.options.add(const FieldOption('option1', 'value')); + form.addFields([first, second, third, fourth]); + message.addPayload(form); - check( - message, - '', + expect( + message.toXMLString(), + equals(Message.fromXML(message.toXML()).toXMLString()), ); }); test('must properly assign ', () { - final form = message['form'] as Form; - form - ..setType('result') - ..addReported( - 'a1', - formType: 'text-single', - label: 'Username', - ) - ..addItem({'a1': 'vsevex@example.com'}); - - check( - message, - 'vsevex@example.com', - useValues: false, + final form = Form( + type: FormType.result, + reported: + Field(variable: 'a', type: FieldType.textSingle, label: 'Username'), ); - }); - - test('must properly set all the defined reported', () { - final form = message['form'] as Form; - form.setType('result'); - final reported = >{ - 'v1': { - 'var': 'v1', - 'type': 'text-single', - 'label': 'Username', - }, - }; + message.addPayload(form); - form.setReported(reported); - - check( - message, - '', + expect( + message.toXMLString(), + Message.fromXML(message.toXML()).toXMLString(), ); }); }); From 592b66b6a63e55040e1c273d3c1ebde41654c21c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sat, 17 Aug 2024 23:59:54 +0400 Subject: [PATCH 34/81] refactor: Add bind stanza test cases and update imports in bind_test.dart --- test/bind_test.dart | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/bind_test.dart diff --git a/test/bind_test.dart b/test/bind_test.dart new file mode 100644 index 0000000..7bbc8c2 --- /dev/null +++ b/test/bind_test.dart @@ -0,0 +1,33 @@ +import 'package:test/test.dart'; + +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/stanza/iq.dart'; + +void main() { + group('bind stanza test cases', () { + test('must generate iq stanza correctly from string', () { + const stanza = + 'resource'; + final iq = IQ.fromString(stanza); + + expect(iq.payload, isNotNull); + expect(iq.payload, isA()); + final payload = iq.payload! as Bind; + expect(payload.resource, equals('resource')); + }); + + test( + 'must build bind stanza correctly depending on the given params', + () { + const bind = Bind(resource: 'resource', jid: 'vsevex@localhost'); + + expect( + bind.toXMLString(), + equals( + 'resourcevsevex@localhost', + ), + ); + }, + ); + }); +} From 32de4c71d4462014f784137dd2248d39632c004c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:00:09 +0400 Subject: [PATCH 35/81] refactor: Update WhixpUtils class with new methods and improve code organization --- lib/src/utils/src/utils.dart | 141 +++++++++++++++-------------------- 1 file changed, 60 insertions(+), 81 deletions(-) diff --git a/lib/src/utils/src/utils.dart b/lib/src/utils/src/utils.dart index 5d01c1d..051ca1d 100644 --- a/lib/src/utils/src/utils.dart +++ b/lib/src/utils/src/utils.dart @@ -41,7 +41,7 @@ class WhixpUtils { /// By creating a single instance of [xml.XmlBuilder] and reusing it, the code /// can avoid the overhead of creating new [xml.XmlBuilder] instances for each /// XML document it generates. - static xml.XmlBuilder _makeGenerator() => _xmlGenerator; + static xml.XmlBuilder makeGenerator() => _xmlGenerator; /// Generates an XML element with optional attributes and nested text. /// @@ -103,7 +103,7 @@ class WhixpUtils { /// Finally, it returns the resulting XML node. If the `name` argument is /// empty or contains only whitespace, or if the `attributes` argument is /// not a valid type, the method returns `null`. - final builder = _makeGenerator(); + final builder = makeGenerator(); builder.element( name, nest: () { @@ -166,6 +166,22 @@ class WhixpUtils { /// For more information refer to [xmlUnescape] method in [Escaper] class. static String xmlUnescape(String text) => Escaper().xmlUnescape(text); + /// Generates a namespaced element string. + /// + /// This method takes an XML element and returns a string in the format + /// `{namespace}localName`, where `namespace` is the value of the `xmlns` + /// attribute of the element and `localName` is the local name of the element. + /// + /// - [element]: The XML element from which to extract the namespace and + /// local name. + /// + /// Returns a string representing the namespaced element. + static String generateNamespacedElement(xml.XmlElement element) { + final namespace = element.getAttribute('xmlns'); + final localName = element.localName; + return '{$namespace}$localName'; + } + /// This method takes an XML [element] and seralizes it into string /// representation of the XML. It uses the `serialize` function to recusively /// iterate through all child elements of the input [element] and construct @@ -213,7 +229,7 @@ class WhixpUtils { /// This method takes a [String] argument [value] and returns a [String] /// in UTF-8. It works by iterating through each character in the input string /// and converting each character to its UTF-8 equivalent. - static String utf16to8(String value) { + String utf16to8(String value) { String out = ''; final length = value.length; @@ -259,10 +275,10 @@ class WhixpUtils { /// the counter is used as the unique id. /// /// Returns the generated ID. - static String getUniqueId([dynamic suffix]) { + static String generateUniqueID([dynamic suffix]) { /// It follows the format specified by the UUID version 4 standart. - final uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - .replaceAllMapped(RegExp('[xy]'), (match) { + final uuid = + 'xxxxx-2113-yxxx-xxxxxxxx'.replaceAllMapped(RegExp('[xy]'), (match) { final r = math.Random.secure().nextInt(16); final v = match.group(0) == 'x' ? r : (r & 0x3 | 0x8); return v.toRadixString(16); @@ -424,80 +440,43 @@ class WhixpUtils { /// WhixpUtils.addNamespace('CLIENT', 'jabber:client'); /// ``` static void addNamespace(String name, String key) => _namespace[name] = key; +} - /// Checks if an object has a specified property using reflection. - /// - /// Uses Dart's reflection capabilities to inspect the structure of an - /// [object] at runtime and determine whether it has a property with the given - /// [property]. - /// - /// ### Example: - /// ```dart - /// final object = SomeClass(); - /// if (WhixpUtils.hasAttr(object, 'property')) { - /// log('property exists!'); - /// } else { - /// /// ...otherwise do something - /// } - /// ``` - /// **Warning:** - /// * Reflection can be affected by certain build configurations, and the - /// effectiveness of this function may vary in those cases. - // static bool hasAttr(Object? object, String property) { - // final instanceMirror = mirrors.reflect(object); - // return instanceMirror.type.instanceMembers.containsKey(Symbol(property)); - // } - - /// Gets the value of an attribute from an object using reflection. - /// - /// This function uses Dart's reflection capabilities to inspect the structure - /// of an object at runtime and retrieves the value of an attribute with the - /// specified name. - /// - /// ### Example: - /// ```dart - /// final exampleObject = Example(); - /// final name = WhixpUtils.getAttr(exampleObject, 'name'); - /// log(name); /// outputs name - /// ``` - /// - /// **Warning:** - /// * Reflection can be affected by certain build configurations, and the - /// effectiveness of this function may vary in those cases. - // static dynamic getAttr(Object? object, String attribute) { - // final instanceMirror = mirrors.reflect(object); - - // try { - // final value = instanceMirror.getField(Symbol(attribute)).reflectee; - // return value; - // } catch (error) { - // return null; - // } - // } - - /// Sets the value of an attribute on an object using reflection. - /// - /// Uses Dart's reflection capabilities to inspect the structure of an object - /// at runtime and sets the value of an attribute with the specified name. - /// - /// ### Example: - /// ```dart - /// final exampleObject = Example(); - /// final name = WhixpUtils.setAttr(exampleObject, 'name', 'hert'); - /// ``` - /// **Warning:** - /// * Reflection can be affected by certain build configurations, and the - /// effectiveness of this function may vary in those cases. - // static void setAttr(Object? object, String attribute, dynamic value) { - // if (value is Function) { - // throw ArgumentError("Setting methods dynamically is not supported."); - // } - // final instanceMirror = mirrors.reflect(object); - // try { - // instanceMirror.setField(Symbol(attribute), value); - // } catch (error) { - // /// Handle cases where the attribute does not exist - // throw ArgumentError("Attribute '$attribute' not found"); - // } - // } +class Tuple2 { + const Tuple2(this.firstValue, this.secondValue); + + final F firstValue; + final S secondValue; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Tuple2 && + other.firstValue == firstValue && + other.secondValue == secondValue; + } + + @override + int get hashCode => firstValue.hashCode ^ secondValue.hashCode; +} + +class Tuple3 { + const Tuple3(this.firstValue, this.secondValue, this.thirdValue); + + final F firstValue; + final S secondValue; + final T thirdValue; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Tuple3 && + other.firstValue == firstValue && + other.secondValue == secondValue && + other.thirdValue == thirdValue; + } + + @override + int get hashCode => + firstValue.hashCode ^ secondValue.hashCode ^ thirdValue.hashCode; } From ccebb7dcd7fa6503b9813cd462ba16d43115a20c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:00:17 +0400 Subject: [PATCH 36/81] refactor: Add StandaloneStringPreparation class for standalone string preparation --- lib/src/stringprep/stringprep.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/stringprep/stringprep.dart b/lib/src/stringprep/stringprep.dart index 84280d8..c525ea7 100644 --- a/lib/src/stringprep/stringprep.dart +++ b/lib/src/stringprep/stringprep.dart @@ -86,6 +86,7 @@ class StringPreparation { ); } +// ignore: avoid_classes_with_only_static_members class StandaloneStringPreparation { const StandaloneStringPreparation(); static const String _rtlChars = r'\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; From cbdbe16c38d7409fe68ec904ab64978d52b89e6c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:00:40 +0400 Subject: [PATCH 37/81] refactor: Add Stanza and its subclasses for handling different types of stanzas --- lib/src/stanza/stanza.dart | 101 +++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 lib/src/stanza/stanza.dart diff --git a/lib/src/stanza/stanza.dart b/lib/src/stanza/stanza.dart new file mode 100644 index 0000000..f5c9a4b --- /dev/null +++ b/lib/src/stanza/stanza.dart @@ -0,0 +1,101 @@ +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/plugins/plugins.dart'; +import 'package:whixp/src/plugins/version.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +/// This class is the base for all stanza types in `Whixp`. +/// Stanza objects carry the core XML packet structure and metadata. +abstract class Stanza with Packet { + /// Default constructor for [Stanza] class. + const Stanza(); + + /// Constructs a specific [Stanza] object from an XML element. + factory Stanza.payloadFromXML(String tag, xml.XmlElement node) { + if (tag == bindTag) { + return Bind.fromXML(node); + } else if (tag == discoInformationTag) { + return DiscoInformation.fromXML(node); + } else if (tag == discoItemsTag) { + return DiscoItems.fromXML(node); + } else if (tag == versionTag) { + return Version.fromXML(node); + } else if (tag == formsTag) { + return Form.fromXML(node); + } else if (tag == tuneTag) { + return Tune.fromXML(node); + } else if (tag == moodTag) { + return Mood.fromXML(node); + } else if (tag == pubsubTag) { + return PubSubStanza.fromXML(node); + } else if (tag == pubsubOwnerTag) { + return PubSubStanza.fromXML(node); + } else if (tag == pubsubEventTag) { + return PubSubEvent.fromXML(node); + } else if (tag == stanzaIDTag) { + return StanzaID.fromXML(node); + } else if (tag == originIDTag) { + return OriginID.fromXML(node); + } else if (tag == vCard4Tag) { + return VCard4.fromXML(node); + } else if (tag == adhocCommandTag) { + return Command.fromXML(node); + } else if (tag == enableTag) { + return Command.fromXML(node); + } else if (tag == disableTag) { + return Disable.fromXML(node); + } else if (tag == delayTag) { + return DelayStanza.fromXML(node); + } else { + throw WhixpInternalException.stanzaNotFound( + node.localName, + WhixpUtils.generateNamespacedElement(node), + ); + } + } +} + +/// This class is the base for all message stanza types. +abstract class MessageStanza extends Stanza { + const MessageStanza(); + + /// Returns the XML tag associated with the [IQStanza] object. + /// + /// This tag represents the name of the XML element for the IQ stanza. + /// + /// Saves the tag in the given format "{namespace}name". + String get tag; +} + +/// This class is the base for all presence stanza types. +abstract class PresenceStanza extends Stanza { + /// Returns the XML tag associated with the [IQStanza] object. + /// + /// This tag represents the name of the XML element for the IQ stanza. + /// + /// Saves the tag in the given format "{namespace}name". + String get tag; +} + +/// This class is the base for all IQ stanza types. +/// IQ stanzas are used to request, query, or command data from a server. +abstract class IQStanza extends Stanza { + const IQStanza(); + + /// Returns the namespace associated with the [IQStanza] object. + /// + /// The namespace is a unique identifier for the XML schema used in the + /// stanza. + String get namespace; + + /// Returns the XML tag associated with the [IQStanza] object. + /// + /// This tag represents the name of the XML element for the IQ stanza. + /// + /// Saves the tag in the given format "{namespace}name". + String get tag; +} From 0ab33807b599f48e11f73dfd411e31fb33ff1328 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:01:00 +0400 Subject: [PATCH 38/81] refactor: Add SMRequest, SMFailed, SMAnswer, SMResume, and SMResumed classes for Stream Management feature --- lib/src/plugins/sm/feature.dart | 70 +++++++++++++++++++++ lib/src/plugins/sm/stanza/_answer.dart | 39 ++++++++++++ lib/src/plugins/sm/stanza/_enabled.dart | 81 +++++++++++++++++++++++++ lib/src/plugins/sm/stanza/_failed.dart | 40 ++++++++++++ lib/src/plugins/sm/stanza/_request.dart | 11 ++++ lib/src/plugins/sm/stanza/_resumed.dart | 75 +++++++++++++++++++++++ lib/src/plugins/time/_methods.dart | 17 ------ 7 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 lib/src/plugins/sm/feature.dart create mode 100644 lib/src/plugins/sm/stanza/_answer.dart create mode 100644 lib/src/plugins/sm/stanza/_enabled.dart create mode 100644 lib/src/plugins/sm/stanza/_failed.dart create mode 100644 lib/src/plugins/sm/stanza/_request.dart create mode 100644 lib/src/plugins/sm/stanza/_resumed.dart diff --git a/lib/src/plugins/sm/feature.dart b/lib/src/plugins/sm/feature.dart new file mode 100644 index 0000000..4d69f70 --- /dev/null +++ b/lib/src/plugins/sm/feature.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:whixp/src/database/controller.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/session.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +part 'stanza/_answer.dart'; +part 'stanza/_enabled.dart'; +part 'stanza/_failed.dart'; +part 'stanza/_request.dart'; +part 'stanza/_resumed.dart'; + +const _namespace = 'urn:xmpp:sm:3'; +const _answer = 'sm:answer'; +const _enable = 'sm:enable'; +const _enabled = 'sm:enabled'; +const _failed = 'sm:failed'; +const _request = 'sm:request'; +const _resume = 'sm:resume'; +const _resumed = 'sm:resumed'; + +class StreamManagement { + StreamManagement(); + + xml.XmlElement toXML() => WhixpUtils.xmlElement('sm', namespace: _namespace); + + static Future saveSMStateToLocal(String jid, SMState state) async { + return HiveController.writeToSMBox(jid, state.toJson()); + } + + static Future getSMStateFromLocal(String? jid) async { + if (jid?.isEmpty ?? true) return null; + return await HiveController.readFromSMBox(jid!); + } + + static Future saveUnackedToLocal(int sequence, Stanza stanza) { + return HiveController.writeUnackeds(sequence, stanza); + } + + static Map get unackeds => HiveController.unackeds; + + static String? popFromUnackeds() => HiveController.popUnacked(); + + static Packet parse(xml.XmlElement node) { + switch (node.localName) { + case 'enabled': + return SMEnabled.fromXML(node); + case 'resumed': + return SMResumed.fromXML(node); + case 'r': + return const SMRequest(); + case 'a': + return SMAnswer.fromXML(node); + case 'failed': + return SMFailed.fromXML(node); + default: + throw WhixpInternalException.unexpectedPacket( + _namespace, + node.localName, + ); + } + } +} diff --git a/lib/src/plugins/sm/stanza/_answer.dart b/lib/src/plugins/sm/stanza/_answer.dart new file mode 100644 index 0000000..3fc9010 --- /dev/null +++ b/lib/src/plugins/sm/stanza/_answer.dart @@ -0,0 +1,39 @@ +part of '../feature.dart'; + +/// Represents an answer in the state machine, incorporating packet +/// functionality. +class SMAnswer extends Stanza { + /// Initializes a new instance of [SMAnswer]. + const SMAnswer({this.h}); + + /// The value of 'h' attribute. + final int? h; + + /// Creates an [SMAnswer] instance from an XML element. + /// + /// [node] The XML element representing the SMAnswer. + factory SMAnswer.fromXML(xml.XmlElement node) { + int? h; + for (final attribute in node.attributes) { + if (attribute.localName == 'h') h = int.parse(attribute.value); + } + + return SMAnswer(h: h); + } + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = {}; + dictionary['xmlns'] = _namespace; + + if (h != null) dictionary['h'] = h.toString(); + + builder.element('a', attributes: dictionary); + + return builder.buildDocument().rootElement; + } + + @override + String get name => _answer; +} diff --git a/lib/src/plugins/sm/stanza/_enabled.dart b/lib/src/plugins/sm/stanza/_enabled.dart new file mode 100644 index 0000000..196623d --- /dev/null +++ b/lib/src/plugins/sm/stanza/_enabled.dart @@ -0,0 +1,81 @@ +part of '../feature.dart'; + +// Define SMEnable class extending [Stanza]. +class SMEnable extends Stanza { + // Default constructor + const SMEnable({this.resume = false}); + + // Instance variable to track resume status + final bool resume; + + // Override the toXML method to generate XML representation of the object + @override + xml.XmlElement toXML() { + // Create a dictionary to hold XML attributes + final dictionary = HashMap(); + + dictionary['xmlns'] = _namespace; // Add xmlns attribute. + dictionary['resume'] = resume.toString(); // Add resume attribute + + // Create the XML element with the 'enable' tag and attributes from the dictionary + final element = WhixpUtils.xmlElement('enable', attributes: dictionary); + + return element; // Return the constructed XML element + } + + @override + String get name => _enable; +} + +class SMEnabled extends Stanza { + /// Initializes a new instance of [SMEnabled]. + SMEnabled(); + + /// The value of 'id' attribute. + String? id; + + /// The value of 'location' attribute. + String? location; + + /// The value of 'resume' attribute. + String? resume; + + /// The value of 'max' attribute. + int? max; + + /// Creates an [SMEnabled] instance from an XML element. + /// + /// [node] The XML element representing the [SMEnabled]. + factory SMEnabled.fromXML(xml.XmlElement node) { + final enabled = SMEnabled(); + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'id': + enabled.id = attribute.value; + case 'location': + enabled.location = attribute.value; + case 'resume': + enabled.resume = attribute.value; + case 'max': + enabled.max = int.parse(attribute.value); + } + } + + return enabled; + } + + @override + xml.XmlElement toXML() { + final dictionary = HashMap(); + dictionary['xmlns'] = _namespace; + if (id != null) dictionary['id'] = id; + if (location != null) dictionary['location'] = location; + if (resume != null) dictionary['resume'] = resume; + if (max != null && max != 0) dictionary['max'] = max; + + return WhixpUtils.xmlElement('enabled', attributes: dictionary); + } + + @override + String get name => _enabled; +} diff --git a/lib/src/plugins/sm/stanza/_failed.dart b/lib/src/plugins/sm/stanza/_failed.dart new file mode 100644 index 0000000..5b1fde6 --- /dev/null +++ b/lib/src/plugins/sm/stanza/_failed.dart @@ -0,0 +1,40 @@ +part of '../feature.dart'; + +class SMFailed extends Stanza { + /// Initializes a new instance of [SMFailed]. + const SMFailed({this.cause}); + + /// The cause of the failure. + final Node? cause; + + /// Creates an [SMFailed] instance from an XML element. + /// + /// [node] The XML element representing the SMFailed. + factory SMFailed.fromXML(xml.XmlElement node) { + Node? cause; + + for (final child in node.children.whereType()) { + cause = Node.fromXML(child); + } + + return SMFailed(cause: cause); + } + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = HashMap(); + dictionary['xmlns'] = _namespace; + + builder.element('failed', attributes: dictionary); + + final root = builder.buildDocument().rootElement; + + if (cause != null) root.children.add(cause!.toXML().copy()); + + return root; + } + + @override + String get name => _failed; +} diff --git a/lib/src/plugins/sm/stanza/_request.dart b/lib/src/plugins/sm/stanza/_request.dart new file mode 100644 index 0000000..cacd0c8 --- /dev/null +++ b/lib/src/plugins/sm/stanza/_request.dart @@ -0,0 +1,11 @@ +part of '../feature.dart'; + +class SMRequest extends Stanza { + const SMRequest(); + + @override + xml.XmlElement toXML() => WhixpUtils.xmlElement('r', namespace: _namespace); + + @override + String get name => _request; +} diff --git a/lib/src/plugins/sm/stanza/_resumed.dart b/lib/src/plugins/sm/stanza/_resumed.dart new file mode 100644 index 0000000..b76e838 --- /dev/null +++ b/lib/src/plugins/sm/stanza/_resumed.dart @@ -0,0 +1,75 @@ +part of '../feature.dart'; + +class SMResume extends Stanza { + // Default constructor + const SMResume({this.h, this.previd}); + + final int? h; + final String? previd; + + // Override the toXML method to generate XML representation of the object + @override + xml.XmlElement toXML() { + // Create a dictionary to hold XML attributes + final dictionary = HashMap(); + + dictionary['xmlns'] = _namespace; + if (h != null) dictionary['h'] = h.toString(); + if (previd != null) dictionary['previd'] = previd.toString(); + + // Create the XML element with the 'resume' tag and attributes from the + // dictionary + final element = WhixpUtils.xmlElement('resume', attributes: dictionary); + + return element; + } + + @override + String get name => _resume; +} + +class SMResumed extends Stanza { + /// Initializes a new instance of [SMResumed]. + SMResumed({this.previd, this.h}); + + /// The value of 'previd' attribute. + final String? previd; + + /// The value of 'h' attribute. + final int? h; + + /// Creates an [SMResumed] instance from an XML element. + /// + /// [node] The XML element representing the SMResumed. + factory SMResumed.fromXML(xml.XmlElement node) { + String? previd; + int? h; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'previd': + previd = attribute.value; + case 'h': + h = int.parse(attribute.value); + } + } + + return SMResumed(previd: previd, h: h); + } + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = {}; + dictionary['xmlns'] = _namespace; + if (h != null) dictionary['h'] = h.toString(); + if (previd != null) dictionary['previd'] = previd!; + + builder.element('resumed', attributes: dictionary); + + return builder.buildDocument().rootElement; + } + + @override + String get name => _resumed; +} diff --git a/lib/src/plugins/time/_methods.dart b/lib/src/plugins/time/_methods.dart index b744090..4a39718 100644 --- a/lib/src/plugins/time/_methods.dart +++ b/lib/src/plugins/time/_methods.dart @@ -32,20 +32,3 @@ dynamic _date({int? year, int? month, int? day, bool asString = true}) { } return value.toUtc(); } - -@visibleForTesting -DateTime parse(String time) => _parse(time); - -@visibleForTesting -String format(DateTime time) => _format(time); - -@visibleForTesting -String formatDate(DateTime time) => _formatDate(time); - -@visibleForTesting -String formatTime(DateTime time, {bool useZulu = false}) => - _formatTime(time, useZulu: useZulu); - -@visibleForTesting -dynamic date({int? year, int? month, int? day, bool asString = true}) => - _date(year: year, month: month, day: day, asString: asString); From 27149c833ba57bf98cda8de38859d2cb7dbc4ff3 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:01:40 +0400 Subject: [PATCH 39/81] refactor(bind): Add Bind class for handling Bind IQ stanzas --- lib/src/plugins/bind.dart | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 lib/src/plugins/bind.dart diff --git a/lib/src/plugins/bind.dart b/lib/src/plugins/bind.dart new file mode 100644 index 0000000..bc5e61e --- /dev/null +++ b/lib/src/plugins/bind.dart @@ -0,0 +1,67 @@ +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +final _namespace = WhixpUtils.getNamespace('BIND'); + +/// Represents a Bind IQ stanza used in XMPP communication. +class Bind extends IQStanza { + /// The name of the Bind stanza. + static const String _name = 'bind'; + + /// Constructor for creating a Bind instance. + const Bind({this.resource, this.jid}); + + /// The resource associated with the Bind stanza. + final String? resource; + + /// The JID (Jabber Identifier) associated with the Bind stanza. + final String? jid; + + /// Factory constructor to create a Bind instance from an XML element. + factory Bind.fromXML(xml.XmlElement node) { + String? resource; + String? jid; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'resource': + resource = child.innerText; + case 'jid': + jid = child.innerText; + } + } + + return Bind(resource: resource, jid: jid); + } + + @override + xml.XmlElement toXML() { + final element = WhixpUtils.makeGenerator(); + element.element( + name, + attributes: {'xmlns': namespace}, + nest: () { + if (resource?.isNotEmpty ?? false) { + element.element('resource', nest: () => element.text(resource!)); + } + if (jid?.isNotEmpty ?? false) { + element.element('jid', nest: () => element.text(jid!)); + } + }, + ); + + return element.buildDocument().rootElement; + } + + @override + String get name => _name; + + @override + String get namespace => _namespace; + + @override + String get tag => bindTag; +} From fdfb5575496447e9a9603f281dd4e18d4f9965c2 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:01:45 +0400 Subject: [PATCH 40/81] refactor(idna): Add IDNA class for handling Internationalized Domain Names --- lib/src/idna/idna.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/idna/idna.dart b/lib/src/idna/idna.dart index 3143a59..9cbe7c2 100644 --- a/lib/src/idna/idna.dart +++ b/lib/src/idna/idna.dart @@ -6,6 +6,7 @@ import 'package:whixp/src/exception.dart'; import 'package:whixp/src/idna/punycode.dart'; import 'package:whixp/src/stringprep/stringprep.dart'; +// ignore: avoid_classes_with_only_static_members class IDNA { const IDNA(); From e48b89fe347c22d24c8a75e336c638bf58d9dabd Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:01:52 +0400 Subject: [PATCH 41/81] refactor(jid): Update JabberID class with imports and code organization --- lib/src/jid/src/jid.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/jid/src/jid.dart b/lib/src/jid/src/jid.dart index 5040c26..baa5058 100644 --- a/lib/src/jid/src/jid.dart +++ b/lib/src/jid/src/jid.dart @@ -1,13 +1,13 @@ import 'dart:io'; -import 'package:dartz/dartz.dart'; - import 'package:memoize/memoize.dart'; import 'package:whixp/src/escaper/escaper.dart'; import 'package:whixp/src/jid/src/exception.dart'; import 'package:whixp/src/utils/src/stringprep.dart'; +import 'package:whixp/src/utils/utils.dart'; +// ignore: avoid_classes_with_only_static_members /// A private utility class for parsing, formatting, and validating Jabber IDs. class _Jabbered { const _Jabbered(); @@ -185,9 +185,9 @@ class JabberID { JabberID([String? jid]) { if (jid == null) return; final jabberID = _Jabbered._parse(jid); - _node = jabberID.value1; - _domain = jabberID.value2; - _resource = jabberID.value3; + _node = jabberID.firstValue; + _domain = jabberID.secondValue; + _resource = jabberID.thirdValue; _bare = ''; _full = ''; @@ -230,9 +230,9 @@ class JabberID { set bare(String bare) { final jid = _Jabbered._parse(bare); - assert(jid.value2.isNotEmpty); - _node = jid.value1; - _domain = jid.value2; + assert(jid.secondValue.isNotEmpty); + _node = jid.firstValue; + _domain = jid.secondValue; _updateBareFull(); } @@ -253,9 +253,9 @@ class JabberID { set full(String value) { final jid = _Jabbered._parse(value); - _node = jid.value1; - _domain = jid.value2; - _resource = jid.value3; + _node = jid.firstValue; + _domain = jid.secondValue; + _resource = jid.thirdValue; _updateBareFull(); } From d803353989e491568cc0f1093a8281b9aa51c51b Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:02:00 +0400 Subject: [PATCH 42/81] refactor: Update StreamFeatures class with imports and code organization --- lib/src/plugins/features.dart | 93 ++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/lib/src/plugins/features.dart b/lib/src/plugins/features.dart index c331f08..3d96da6 100644 --- a/lib/src/plugins/features.dart +++ b/lib/src/plugins/features.dart @@ -1,6 +1,87 @@ -export 'package:whixp/src/plugins/bind/bind.dart'; -export 'package:whixp/src/plugins/mechanisms/feature.dart'; -export 'package:whixp/src/plugins/preapproval/preapproval.dart'; -export 'package:whixp/src/plugins/rosterver/rosterver.dart'; -export 'package:whixp/src/plugins/session/session.dart'; -export 'package:whixp/src/plugins/starttls/starttls.dart'; +import 'package:whixp/src/plugins/bind.dart'; +import 'package:whixp/src/plugins/mechanisms/mechanisms.dart'; +import 'package:whixp/src/plugins/sm/feature.dart'; +import 'package:whixp/src/plugins/starttls.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +export 'bind.dart'; +export 'mechanisms/feature.dart'; +export 'sm/feature.dart'; +export 'starttls.dart'; + +/// Represents stream features stanza. +/// +/// This stanza encapsulates features available in the stream negotiation phase. +class StreamFeatures extends Stanza { + /// Constructs a [StreamFeatures] stanza. + StreamFeatures(); + + static Set supported = {}; + + static Set list = {}; + + /// StartTLS feature. + StartTLS? startTLS; + + /// Bind feature. + Bind? bind; + + /// Available mechanisms. + SASLMechanisms? mechanisms; + + /// SM + StreamManagement? sm; + + /// Constructs a [StreamFeatures] stanza from XML. + factory StreamFeatures.fromXML(xml.XmlElement node) { + final features = StreamFeatures(); + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'starttls': + final startTLS = StartTLS.fromXML(child); + features.startTLS = startTLS; + list.add(startTLS.name); + case 'mechanisms': + final mechanisms = SASLMechanisms.fromXML(child); + features.mechanisms = mechanisms; + list.add('mechanisms'); + case 'bind': + final bind = Bind.fromXML(child); + features.bind = bind; + list.add(bind.name); + case 'sm': + final sm = StreamManagement(); + features.sm = sm; + list.add('sm'); + } + } + + return features; + } + + @override + xml.XmlElement toXML() { + final features = WhixpUtils.xmlElement(name); + if (startTLS != null) features.children.add(startTLS!.toXML().copy()); + if (mechanisms != null) features.children.add(mechanisms!.toXML().copy()); + if (bind != null) features.children.add(bind!.toXML().copy()); + + return features; + } + + /// Indicates if the stream supports StartTLS. + bool get doesStartTLS => startTLS != null; + + /// Indicates if TLS is required. + bool get tlsRequired => startTLS?.required ?? false; + + /// Whether stream management enabled by the server or not. + bool get doesStreamManagement => sm != null; + + @override + String get name => 'stream:features'; +} From 355fa7898027a8ec100168cb96ec965a14e92354 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:02:19 +0400 Subject: [PATCH 43/81] refactor(id): Add StanzaID and OriginID classes for handling stanza IDs --- lib/src/plugins/id/id.dart | 8 ++++ lib/src/plugins/id/stanza.dart | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 lib/src/plugins/id/id.dart create mode 100644 lib/src/plugins/id/stanza.dart diff --git a/lib/src/plugins/id/id.dart b/lib/src/plugins/id/id.dart new file mode 100644 index 0000000..595676b --- /dev/null +++ b/lib/src/plugins/id/id.dart @@ -0,0 +1,8 @@ +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/jid/jid.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +part 'stanza.dart'; diff --git a/lib/src/plugins/id/stanza.dart b/lib/src/plugins/id/stanza.dart new file mode 100644 index 0000000..dfe7a99 --- /dev/null +++ b/lib/src/plugins/id/stanza.dart @@ -0,0 +1,82 @@ +part of 'id.dart'; + +class StanzaID extends MessageStanza { + const StanzaID(this.id, {this.by}); + + final String id; + final JabberID? by; + + @override + xml.XmlElement toXML() { + final attributes = {}; + attributes['id'] = id; + if (by != null) attributes['by'] = by.toString(); + + return WhixpUtils.xmlElement( + name, + namespace: 'urn:xmpp:sid:0', + attributes: attributes, + ); + } + + /// Constructs a [SASLSuccess] packet from XML. + factory StanzaID.fromXML(xml.XmlElement node) { + late String id; + JabberID? by; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'id': + id = attribute.value; + case 'by': + by = JabberID(attribute.value); + } + } + + return StanzaID(id, by: by); + } + + @override + String get name => 'stanza-id'; + + @override + String get tag => stanzaIDTag; +} + +class OriginID extends MessageStanza { + const OriginID(this.id); + + final String id; + + @override + xml.XmlElement toXML() { + final attributes = {}; + attributes['id'] = id; + + return WhixpUtils.xmlElement( + 'success', + namespace: 'urn:xmpp:sid:0', + attributes: attributes, + ); + } + + /// Constructs a [SASLSuccess] packet from XML. + factory OriginID.fromXML(xml.XmlElement node) { + late String id; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'id': + id = attribute.value; + } + } + + return OriginID(id); + } + + @override + String get name => 'origin-id'; + + @override + String get tag => originIDTag; +} From c45f372fe68d726fcf9c3132a72b4e02d9180ae1 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:02:25 +0400 Subject: [PATCH 44/81] refactor(log): Update Log class with configurable logging levels and native colors --- lib/src/log/src/log.dart | 185 +++++++++++++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 37 deletions(-) diff --git a/lib/src/log/src/log.dart b/lib/src/log/src/log.dart index eeba44f..05ca66d 100644 --- a/lib/src/log/src/log.dart +++ b/lib/src/log/src/log.dart @@ -1,19 +1,23 @@ -import 'package:logger/logger.dart'; +enum Level { + wtf, + error, + warning, + info, + debug, + verbose, +} /// Sets up debugging for [Whixp] || [Transport] instance. /// /// Configures debugging by attaching event listeners to log various types of /// messages. Different types of messages (info, status, error) can be enabled /// or disabled. -/// -/// See also: -/// -/// - [Logger], the logger instance used for logging messages. -/// - [PrettyPrinter], a pretty printer configuration for the logger. class Log { static late Log _internal; - /// A singleton class that provides configurable logging levels. + /// [Log] instance. + static Log get instance => _internal; + factory Log({ /// Whether to enable debug messages. Defaults to `true` bool enableDebug = true, @@ -26,32 +30,35 @@ class Log { /// Whether to enable warning messages. Defaults to `false` bool enableWarning = false, + + /// Whether to use native colors assigned to level types or not. + bool nativeColors = true, + + /// Whether to add date and time indicator in the log message or not. + bool includeTimestamp = false, + + /// Whether to show date in the log output or not. + bool showDate = false, }) => _internal = Log._firstInstance( enableDebug, enableInfo, enableError, enableWarning, + nativeColors, + includeTimestamp, + showDate, ); + /// A singleton class that provides configurable logging levels. Log._firstInstance( this._enableDebug, this._enableInfo, this._enableError, this._enableWarning, - ); - - /// [Log] instance. - static Log get instance => _internal; - - /// The underlying logger instance used for logging. - late final _logger = Logger( - printer: PrettyPrinter( - lineLength: 250, - printTime: true, - methodCount: 0, - noBoxingByDefault: true, - ), + this._nativeColors, + this._includeTimestamp, + this._showDate, ); /// Whether to enable debugging message. Defaults to `true`. @@ -66,33 +73,137 @@ class Log { /// Whether to enable warning message debugging. Defaults to `false`. final bool _enableWarning; - /// Logs an informational message if info logging is enabled. - void info(String info) { + final bool _includeTimestamp; + + final bool _showDate; + + /// Override this function if you want to convert a stacktrace for some reason + /// for example to apply a source map in the browser. + static StackTrace? Function(StackTrace?) stackTraceConverter = (s) => s; + + final bool _nativeColors; + + final List outputEvents = []; + + void addLogEvent(LogEvent logEvent) { + if (logEvent.level == Level.error && _enableError || + logEvent.level == Level.warning && _enableWarning || + logEvent.level == Level.info && _enableInfo || + logEvent.level == Level.debug && _enableDebug) { + outputEvents.add(logEvent); + logEvent.printOut(); + } + } + + void error(String title, {Object? exception, StackTrace? stackTrace}) { + addLogEvent( + LogEvent( + title, + exception: exception, + stackTrace: stackTraceConverter(stackTrace), + level: Level.error, + ), + ); + } + + void warning(String title, [Object? exception, StackTrace? stackTrace]) { + if (_enableWarning) { + addLogEvent( + LogEvent( + title, + exception: exception, + stackTrace: stackTraceConverter(stackTrace), + level: Level.warning, + ), + ); + } + } + + void info(String title, [Object? exception, StackTrace? stackTrace]) { if (_enableInfo) { - _logger.i(info); + addLogEvent( + LogEvent( + title, + exception: exception, + stackTrace: stackTraceConverter(stackTrace), + level: Level.info, + ), + ); } } - /// Logs a debugging message if debug logging is enabled. - void debug(String debug) { + void debug(String title, [Object? exception, StackTrace? stackTrace]) { if (_enableDebug) { - _logger.d(debug); + addLogEvent( + LogEvent( + title, + exception: exception, + stackTrace: stackTraceConverter(stackTrace), + ), + ); } } +} - /// Logs an error message if error logging is enabled. - /// - /// Optionally, you can provide an [error] object and [stackTrace]. - void error(String message, {Object? error, StackTrace? stackTrace}) { - if (_enableError) { - _logger.e(message, error: error, stackTrace: stackTrace); +// ignore: avoid_print +class LogEvent { + final String title; + final Object? exception; + final StackTrace? stackTrace; + final Level level; + + LogEvent( + this.title, { + this.exception, + this.stackTrace, + this.level = Level.debug, + }); +} + +String _timestamp(bool showDate) { + final now = DateTime.now(); + + final formattedDate = 'dd/MM/yyyy' + .replaceAll('yyyy', now.year.toString().padLeft(4, '0')) + .replaceAll('MM', now.month.toString().padLeft(2, '0')) + .replaceAll('dd', now.day.toString().padLeft(2, '0')); + + final formattedTime = + '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.${now.millisecond.toString().padLeft(3, '0')}'; + return '${showDate ? '$formattedDate ' : ''}$formattedTime'; +} + +extension PrintLogs on LogEvent { + void printOut() { + final instance = Log.instance; + var logsStr = title; + if (exception != null) { + logsStr += ' - $exception'; } - } - /// Logs a warning message if warning logging is enabled. - void warning(String warning) { - if (_enableWarning) { - _logger.w(warning); + if (stackTrace != null) { + logsStr += '\n$stackTrace'; + } + if (instance._nativeColors) { + switch (level) { + case Level.wtf: + logsStr = '\x1B[31m!!!CRITICAL!!! $logsStr\x1B[0m'; + case Level.error: + logsStr = '\x1B[31m$logsStr\x1B[0m'; + case Level.warning: + logsStr = '\x1B[33m$logsStr\x1B[0m'; + case Level.info: + logsStr = '\x1B[32m$logsStr\x1B[0m'; + case Level.debug: + logsStr = '\x1B[34m$logsStr\x1B[0m'; + case Level.verbose: + break; + } } + + final timestamp = '\x1B[35;40m${_timestamp(instance._showDate)}\x1B[0m'; + + // ignore: avoid_print + print('[Whixp]${instance._includeTimestamp ? ' $timestamp' : ''} $logsStr'); } } From c10d4d9ee7fb230be6aea59a6e0eb392555cc2e1 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:02:35 +0400 Subject: [PATCH 45/81] refactor: Add mixins for Packet and Attributes classes --- lib/src/stanza/mixins.dart | 108 +++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 lib/src/stanza/mixins.dart diff --git a/lib/src/stanza/mixins.dart b/lib/src/stanza/mixins.dart new file mode 100644 index 0000000..a5453a5 --- /dev/null +++ b/lib/src/stanza/mixins.dart @@ -0,0 +1,108 @@ +import 'dart:collection'; + +import 'package:whixp/src/jid/jid.dart'; + +import 'package:xml/xml.dart' as xml; + +/// Represents the type of packet. +enum PacketType { presence, message, iq } + +/// Mixin class Packet that provides a common interface for all packet types. +/// This mixin is used to define the basic properties and methods for packets. +mixin Packet { + /// Property that returns the name of the packet. + String get name; + + /// Method that converts the packet to an XML element. + /// + /// This method is used to serialize the packet into an XML format. + xml.XmlElement toXML(); + + /// Converts the packet to an XML string. + /// + /// This method is used to serialize the packet into an XML string format. + /// It removes the XML declaration and trims the left side of the string. + String toXMLString() { + final document = toXML(); + + final xmlString = document.toXmlString(); + return xmlString.replaceFirst('', '').trimLeft(); + } + + /// Overrides of the `toString` method to return the XML string representation + /// of the packet. + @override + String toString() => toXMLString(); +} + +/// Provides a common interface for managing attributes in packets. +/// +/// This mixin is used to define the basic properties and methods for handling +/// attributes. +mixin Attributes { + /// Holds the `XML` namespace of the packet. + String? _xmlns; + + /// returns the type of the packet. The type is an attribute of the packet + /// that identifies its type. + String? type; + + /// The ID is an attribute of the packet that uniquely identifies it. + String? id; + + /// Returns the sender of the packet. The sender is an attribute of the packet + /// that identifies who sent it. + JabberID? from; + + /// Returns the recipient of the packet. The recipient is an attribute of the + /// packet that identifies who it is intended for. + JabberID? to; + + /// Returns the language of the packet. The language is an attribute of the + /// packet that identifies the language used in the packet. + String? language; + + /// Getter for the `XML` namespace of the packet. + String? get xmlns => _xmlns; + + /// Setter for the `XML` namespace of the packet. + set xmlns(String? xmlns) => _xmlns = xmlns; + + /// Loads the attributes of the packet from an `XML` node. + /// + /// This method is used to deserialize the packet from an XML format. + void loadAttributes(xml.XmlNode node) { + xmlns = node is xml.XmlElement ? node.getAttribute('xmlns') : ''; + for (final attribute in node.attributes) { + switch (attribute.name.toString()) { + case "type": + type = attribute.value; + case "from": + from = JabberID(attribute.value); + case "to": + to = JabberID(attribute.value); + case "id": + id = attribute.value; + case "lang": + language = attribute.value; + default: + break; + } + } + } + + /// Returns a hash map of the packet's attributes. Serializes the packet into + /// a hash map format. + HashMap get attributeHash { + final dictionary = HashMap(); + if (_xmlns?.isNotEmpty ?? false) dictionary["xmlns"] = _xmlns!; + if (id?.isNotEmpty ?? false) dictionary["id"] = id!; + if (type?.isNotEmpty ?? false) dictionary["type"] = type!; + if (from != null) dictionary["from"] = from.toString(); + if (to != null) dictionary["to"] = to.toString(); + final langNs = (xmlns?.isNotEmpty ?? false) ? "xml:lang" : "lang"; + if (language?.isNotEmpty ?? false) dictionary[langNs] = language!; + + return dictionary; + } +} From 7a7ccbfaaaa61bc7050d1aeba28362ce43c7b622 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:02:58 +0400 Subject: [PATCH 46/81] refactor: Add Session class for managing XMPP sessions --- lib/src/session.dart | 224 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 lib/src/session.dart diff --git a/lib/src/session.dart b/lib/src/session.dart new file mode 100644 index 0000000..3e7fc12 --- /dev/null +++ b/lib/src/session.dart @@ -0,0 +1,224 @@ +import 'dart:math' as math; + +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/jid/jid.dart'; +import 'package:whixp/src/log/log.dart'; +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/stanza/iq.dart'; +import 'package:whixp/src/stanza/message.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/presence.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/transport.dart'; + +final int _seq = math.pow(2, 32) as int; + +class Session { + Session(this.features); + + JabberID? bindJID; + SMState? state; + bool enabledOut = false; + final StreamFeatures features; + + Future bind() async { + final resource = Transport.instance().boundJID?.resource; + Bind bind = const Bind(); + if (resource?.isNotEmpty ?? false) bind = Bind(resource: resource); + + final iq = IQ(generateID: true); + iq.type = iqTypeSet; + iq.payload = bind; + + final result = await iq.send(); + if (result.payload != null && result.error == null) { + bindJID = JabberID((result.payload! as Bind).jid); + if (!features.doesStreamManagement) { + Transport.instance().emit('startSession'); + } + } else { + Log.instance.warning('IQ bind result is missing'); + } + return false; + } + + Future resume( + String? fullJID, { + required void Function() onResumeDone, + required Future Function() onResumeFailed, + }) async { + if (!features.doesStreamManagement) return false; + Transport.instance() + .callbacksBeforeStanzaSend + .add((stanza) async => _handleOutgoing(fullJID, stanza as Stanza)); + if (fullJID == null) return false; + state = await StreamManagement.getSMStateFromLocal(fullJID); + if (state == null) return onResumeFailed.call(); + + final resume = SMResume(h: state?.handled, previd: state?.id); + + final result = await Transport.instance().sendAwait( + 'SM Resume Handler', + resume, + 'sm:resumed', + failurePacket: 'sm:failed', + ); + + if (result != null) { + if (result is SMFailed) { + Log.instance.warning('SM failed: ${result.cause?.content}'); + } else { + onResumeDone.call(); + Transport.instance().emit('startSession'); + } + return false; + } else { + return await onResumeFailed.call(); + } + } + + Future enableStreamManagement( + Future Function(Packet packet) onEnabled, + ) async { + if (!features.doesStreamManagement) return false; + const enable = SMEnable(resume: true); + if (bindJID == null) await bind.call(); + final result = await Transport.instance().sendAwait( + 'SM Enable Handler', + enable, + 'sm:enabled', + failurePacket: 'sm:failed', + ); + + if (result != null) { + await onEnabled.call(result); + state = state?.copyWith(handled: 0); + + Transport.instance().emit('startSession'); + } + + return false; + } + + void sendAnswer() { + final answer = SMAnswer(h: state?.handled ?? 0); + return Transport.instance().send(answer); + } + + void request() { + Log.instance.warning('Requesting Ack'); + const request = SMRequest(); + return Transport.instance().send(request); + } + + Future handleAnswer(Packet packet, String full) async { + if (packet is! SMAnswer) return; + if (packet.h == state?.lastAck) return; + + int numAcked = ((packet.h ?? 0) - (state?.lastAck ?? 0)) % _seq; + final unackeds = StreamManagement.unackeds; + + Log.instance.warning( + 'Acked: ${packet.h}, Last acked: ${state?.lastAck}, Unacked: ${unackeds.length}, Num acked: $numAcked, Remaining: ${unackeds.length - numAcked}', + ); + + if ((numAcked > unackeds.length) || numAcked < 0) { + Log.instance.error( + 'Inconsistent sequence numbers from the server, ignoring and replacing ours with them.', + ); + numAcked = unackeds.length; + } + int sequence = state?.sequence ?? 1; + for (int i = 0; i < numAcked; i++) { + /// Pop and update sequence number + final unacked = StreamManagement.popFromUnackeds(); + if (unacked?.isEmpty ?? true) return; + sequence = sequence - 1; + + try { + final raw = unacked!; + late Stanza stanza; + if (raw.startsWith('('ackedRaw', data: raw); + } + + Transport.instance().emit('ackedStanza', data: stanza); + } catch (_) { + Transport.instance().emit('ackedRaw', data: unacked); + } + } + + state = state?.copyWith(lastAck: packet.h, sequence: sequence); + await saveSMState(full, state); + } + + Future _handleOutgoing(String? fullJID, Stanza stanza) async { + if (!enabledOut) return stanza; + if (fullJID?.isEmpty ?? true) return stanza; + + if (stanza is Message || stanza is IQ || stanza is Presence) { + final sequence = ((state?.sequence ?? 0) + 1) % _seq; + state = state?.copyWith(sequence: sequence); + await StreamManagement.saveUnackedToLocal(sequence, stanza); + await saveSMState(fullJID, state); + request(); + } + + return stanza; + } + + Future increaseInbound(String full) async { + if (full.isEmpty) return null; + final handled = ((state?.handled ?? 0) + 1) % _seq; + + await saveSMState(full, state?.copyWith(handled: handled)); + + return handled; + } + + Future saveSMState(String? fullJID, SMState? state) async { + if ((fullJID?.isEmpty ?? true) || state == null) return; + this.state = state; + await StreamManagement.saveSMStateToLocal(fullJID!, state); + } + + bool get isSessionOpen => bindJID != null; +} + +// Class holding Stream Management information. +class SMState { + final String id; + final int sequence; + final int handled; + final int lastAck; + + const SMState(this.id, this.sequence, this.handled, this.lastAck); + + Map toJson() => { + 'id': id, + 'sequence': sequence, + 'handled': handled, + 'last_ack': lastAck, + }; + + SMState copyWith({String? id, int? sequence, int? handled, int? lastAck}) => + SMState( + id ?? this.id, + sequence ?? this.sequence, + handled ?? this.handled, + lastAck ?? this.lastAck, + ); + + factory SMState.fromJson(Map json) => SMState( + json['id'] as String, + json['sequence'] as int, + json['handled'] as int, + json['last_ack'] as int, + ); +} From eccf968017a4c60559edcec8f9dc2c1085947a02 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:03:18 +0400 Subject: [PATCH 47/81] refactor(stream): Add StreamParser class for handling XML stream parsing --- lib/src/stream.dart | 175 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 lib/src/stream.dart diff --git a/lib/src/stream.dart b/lib/src/stream.dart new file mode 100644 index 0000000..a3d15a5 --- /dev/null +++ b/lib/src/stream.dart @@ -0,0 +1,175 @@ +import 'dart:async'; +import 'dart:convert'; + +// ignore: implementation_imports +import 'package:xml/src/xml_events/utils/conversion_sink.dart'; + +import 'package:xml/xml.dart' as xml; +import 'package:xml/xml_events.dart'; + +/// A result object for [XmlStreamBuffer]. +abstract class StreamObject {} + +/// A complete XML element returned by the stream buffer. +class StreamElement extends StreamObject { + StreamElement(this.element); + + /// The actual [xml.XmlNode]. + final xml.XmlElement element; +} + +/// The "Stream Header" of a new XML stream. +class StreamHeader extends StreamObject { + StreamHeader(this.attributes); + + /// Headers of the stream header. + final Map attributes; +} + +/// "" footer indicator object. +class StreamFooter extends StreamObject { + StreamFooter(); +} + +/// A wrapper around a [Converter]'s [Converter.startChunkedConversion] method. +class _ChunkedConversionBuffer { + /// Use the converter [converter]. + _ChunkedConversionBuffer(Converter> converter) { + _outputSink = ConversionSink>(_results.addAll); + _inputSink = converter.startChunkedConversion(_outputSink); + } + + /// The results of the converter. + final List _results = List.empty(growable: true); + + /// The sink that outputs to [_results]. + late ConversionSink> _outputSink; + + /// The sink that we use for input. + late Sink _inputSink; + + /// Close all opened sinks. + void close() { + _inputSink.close(); + _outputSink.close(); + } + + /// Turn the input [input] into a list of [T] according to the initial + /// converter. + List convert(S input) { + _results.clear(); + _inputSink.add(input); + return _results; + } +} + +/// A buffer to put between a socket's input and a full XML stream. +class StreamParser extends StreamTransformerBase> { + final StreamController> _streamController = + StreamController>(); + + /// Turns a String into a list of [XmlEvent]s in a chunked fashion. + _ChunkedConversionBuffer _eventBuffer = + _ChunkedConversionBuffer(XmlEventDecoder()); + + /// Turns a list of [XmlEvent]s into a list of [xml.XmlNode]s in a chunked fashion. + _ChunkedConversionBuffer, xml.XmlNode> _childBuffer = + _ChunkedConversionBuffer, xml.XmlNode>( + const XmlNodeDecoder(), + ); + + /// The selectors. + _ChunkedConversionBuffer, XmlEvent> _childSelector = + _ChunkedConversionBuffer, XmlEvent>( + XmlSubtreeSelector((event) => event.qualifiedName != 'stream:stream'), + ); + _ChunkedConversionBuffer, XmlEvent> _streamHeaderSelector = + _ChunkedConversionBuffer, XmlEvent>( + XmlSubtreeSelector((event) => event.qualifiedName == 'stream:stream'), + ); + + void reset() { + try { + _eventBuffer.close(); + } catch (_) { + /// Do nothing. A crash here may indicate that we end on invalid XML, + /// which is fine since we're not going to use the buffer's output anymore. + } + try { + _childBuffer.close(); + } catch (_) { + /// Do nothing. + } + try { + _childSelector.close(); + } catch (_) { + /// Do nothing. + } + try { + _streamHeaderSelector.close(); + } catch (_) { + /// Do nothing. + } + + /// Recreate the buffers. + _eventBuffer = + _ChunkedConversionBuffer(XmlEventDecoder()); + _childBuffer = _ChunkedConversionBuffer, xml.XmlNode>( + const XmlNodeDecoder(), + ); + _childSelector = _ChunkedConversionBuffer, XmlEvent>( + XmlSubtreeSelector((event) => event.qualifiedName != 'stream:stream'), + ); + _streamHeaderSelector = _ChunkedConversionBuffer, XmlEvent>( + XmlSubtreeSelector((event) => event.qualifiedName == 'stream:stream'), + ); + } + + @override + Stream> bind(Stream stream) { + /// We do not want to use xml's `toXmlEvents` and `toSubtreeEvents` methods + /// as they create streams we cannot close. We need to be able to destroy + /// and recreate an XML parser whenever we start a new connection. + stream.listen((input) { + final events = _eventBuffer.convert(input); + final streamHeaderEvents = _streamHeaderSelector.convert(events); + final objects = List.empty(growable: true); + + // Process the stream header separately. + for (final event in streamHeaderEvents) { + if (event is! XmlStartElementEvent) { + continue; + } + + if (event.name != 'stream:stream') { + continue; + } else { + if (event.attributes.isEmpty) objects.add(StreamFooter()); + } + + objects.add( + StreamHeader( + Map.fromEntries( + event.attributes.map( + (attributes) => MapEntry(attributes.name, attributes.value), + ), + ), + ), + ); + } + + // Process the children of the element. + final childEvents = _childSelector.convert(events); + final children = _childBuffer.convert(childEvents); + for (final node in children) { + if (node.nodeType == XmlNodeType.ELEMENT) { + objects.add(StreamElement(node as xml.XmlElement)); + } + } + + _streamController.add(objects); + }); + + return _streamController.stream; + } +} From de034590b8c8a1d04f4a6f0a862cdae7d099c3ee Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:03:48 +0400 Subject: [PATCH 48/81] refactor: Add DiscoItem, Feature, and Identity classes for XMPP disco functionality --- lib/src/plugins/disco/_feature.dart | 52 ++++ lib/src/plugins/disco/_identity.dart | 80 ++++++ lib/src/plugins/disco/_item.dart | 68 +++++ lib/src/plugins/disco/info.dart | 381 ++++++--------------------- lib/src/plugins/disco/items.dart | 88 +++++++ 5 files changed, 366 insertions(+), 303 deletions(-) create mode 100644 lib/src/plugins/disco/_feature.dart create mode 100644 lib/src/plugins/disco/_identity.dart create mode 100644 lib/src/plugins/disco/_item.dart create mode 100644 lib/src/plugins/disco/items.dart diff --git a/lib/src/plugins/disco/_feature.dart b/lib/src/plugins/disco/_feature.dart new file mode 100644 index 0000000..78ca2b8 --- /dev/null +++ b/lib/src/plugins/disco/_feature.dart @@ -0,0 +1,52 @@ +part of 'info.dart'; + +/// Represents an XMPP feature. +/// +/// Features are typically used to indicate capabilities or supported +/// functionalities by an XMPP entity. +/// +/// Example usage: +/// ```xml +/// +/// ``` +class Feature { + static const String _name = 'feature'; + + /// Constructs a `feature`. + Feature({this.variable}); + + /// The variable associated with the feature. + final String? variable; + + /// Constructs a feature from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory Feature.fromXML(xml.XmlElement node) { + if (_name != node.localName) { + throw WhixpInternalException.invalidNode(node.localName, _name); + } + + String? variable; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'var': + variable = attribute.value; + } + } + + return Feature(variable: variable); + } + + /// Converts the feature to its XML representation. + xml.XmlElement toXML() { + final dictionary = HashMap(); + + if (variable?.isNotEmpty ?? false) dictionary['var'] = variable!; + + final builder = WhixpUtils.makeGenerator() + ..element(_name, attributes: dictionary); + + return builder.buildDocument().rootElement; + } +} diff --git a/lib/src/plugins/disco/_identity.dart b/lib/src/plugins/disco/_identity.dart new file mode 100644 index 0000000..f2efc8d --- /dev/null +++ b/lib/src/plugins/disco/_identity.dart @@ -0,0 +1,80 @@ +part of 'info.dart'; + +/// Represents an XMPP identity. +/// +/// Identity is typically used to describe the identity of an XMPP entity, +/// providing information such as name, category, and type. +/// +/// Example usage: +/// ```xml +/// +/// ``` +class Identity { + static const _name = 'identity'; + + /// Constructs an identity. + Identity({this.name, this.category, this.type}); + + /// The name of the identity. + final String? name; + + /// The category of the identity. + final String? category; + + /// The type of the identity. + final String? type; + + /// Constructs an identity from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory Identity.fromXML(xml.XmlElement node) { + if (_name != node.localName) { + throw WhixpInternalException.invalidNode(node.localName, _name); + } + + String? name; + String? category; + String? type; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'name': + name = attribute.value; + case 'category': + category = attribute.value; + case 'type': + type = attribute.value; + } + } + return Identity(name: name, category: category, type: type); + } + + /// Converts the identity to its XML representation. + xml.XmlElement toXML() { + final dictionary = HashMap(); + if (name?.isNotEmpty ?? false) { + dictionary['name'] = name!; + } + if (category?.isNotEmpty ?? false) { + dictionary['category'] = category!; + } + if (type?.isNotEmpty ?? false) { + dictionary['type'] = type!; + } + final builder = WhixpUtils.makeGenerator() + ..element(_name, attributes: dictionary); + return builder.buildDocument().rootElement; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Identity && + runtimeType == other.runtimeType && + name == other.name && + category == other.category && + type == other.type; + + @override + int get hashCode => name.hashCode ^ category.hashCode ^ type.hashCode; +} diff --git a/lib/src/plugins/disco/_item.dart b/lib/src/plugins/disco/_item.dart new file mode 100644 index 0000000..23346bd --- /dev/null +++ b/lib/src/plugins/disco/_item.dart @@ -0,0 +1,68 @@ +part of 'items.dart'; + +/// Represents an XMPP disco item. +/// +/// Disco items are used in service discovery to represent items associated +/// with a particular node. +/// +/// Example usage: +/// ```xml +/// +/// ``` +class DiscoItem { + static const _name = 'item'; + + /// Constructs a disco item. + DiscoItem({this.jid, this.node, this.name}); + + /// The JID associated with the disco item. + final String? jid; + + /// The node associated with the disco item. + final String? node; + + /// The name of the disco item. + final String? name; + + /// Constructs a disco item from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory DiscoItem.fromXML(xml.XmlElement node) { + if (_name != node.localName) { + throw WhixpInternalException.invalidNode(node.localName, _name); + } + + String? jid; + String? nod; + String? name; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'jid': + jid = attribute.value; + case 'node': + nod = attribute.value; + case 'name': + name = attribute.value; + } + } + return DiscoItem(jid: jid, node: nod, name: name); + } + + /// Converts the disco item to its XML representation. + xml.XmlElement toXML() { + final dictionary = HashMap(); + final builder = WhixpUtils.makeGenerator(); + if (jid?.isNotEmpty ?? false) { + dictionary['jid'] = jid!; + } + if (node?.isNotEmpty ?? false) { + dictionary['node'] = node!; + } + if (name?.isNotEmpty ?? false) { + dictionary['name'] = name!; + } + builder.element(_name, attributes: dictionary); + return builder.buildDocument().rootElement; + } +} diff --git a/lib/src/plugins/disco/info.dart b/lib/src/plugins/disco/info.dart index 28bc5a8..9853c57 100644 --- a/lib/src/plugins/disco/info.dart +++ b/lib/src/plugins/disco/info.dart @@ -1,346 +1,121 @@ -part of 'disco.dart'; +import 'dart:collection'; -/// The ability to discover information about entities on the Jabber network is -/// extremely valuable. Such information might include features offered or -/// protocols supported by the entity, the entity's type or identity, and -/// additional entities that are associated with the original entity in some way -/// (often thought of as "children" of the "parent" entity). -/// -/// see https://xmpp.org/extensions/xep-0030.html#intro -class DiscoveryInformation extends XMLBase { - /// Allows for users and agents to find the identities and features supported - /// by other entities in the network (XMPP) through service discovery - /// ("disco"). - /// - /// In particular, the "disco#info" query type of __IQ__ stanzas is used to - /// request the list of identities and features offered by a Jabber ID. - /// - /// An identity is a combination of a __category__ and __type__, such as the - /// "client" category with a type "bot" to indicate the agent is not a human - /// operated client, or a category of "gateway" with a type of "aim" to - /// identify the agent as a gateway for the legacy AIM protocol. - /// - /// XMPP Registrar Disco Categories: - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// - /// - /// - /// ``` - DiscoveryInformation({ - super.element, - super.parent, - super.getters, - super.deleters, - }) : super( - name: 'query', - namespace: WhixpUtils.getNamespace('DISCO_INFO'), - includeNamespace: true, - pluginAttribute: 'disco_info', - interfaces: {'node', 'features', 'identities'}, - languageInterfaces: {'identities'}, - ) { - _features = {}; - _identities = {}; - - addGetters({ - const Symbol('features'): (args, base) => getFeatures(), - }); +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; - addSetters( - { - const Symbol('features'): (value, args, base) => - setFeatures(value as Iterable), - }, - ); +import 'package:xml/xml.dart' as xml; - addDeleters({ - const Symbol('identities'): (args, base) => deleteIdentities(), - const Symbol('features'): (args, base) => deleteFeatures(), - }); - } - - /// [Set] of features. - late final Set _features; +part '_feature.dart'; +part '_identity.dart'; - late final Set _identities; +final String _namespace = WhixpUtils.getNamespace('DISCO_INFO'); - /// Returns a [Set] or [List] of all identities in [DiscoveryIdentity]. - /// - /// If a [language] was specified, only return identities using that language. - /// If [duplicate] was set to true, then use [List] as it is allowed to - /// duplicate items. +/// Represents an XMPP disco information IQ stanza. +/// +/// Disco information IQ stanzas are used for querying and discovering +/// information about an XMPP entity's capabilities and features. +class DiscoInformation extends IQStanza { + static const String _name = 'query'; - Iterable getIdentities({ - String? language, - bool duplicate = false, - }) { - late final Iterable identities; - if (duplicate) { - identities = []; - } else { - identities = {}; - } + /// Constructs a disco information IQ stanza. + DiscoInformation({this.node}); - for (final idElement - in element!.findAllElements('identity', namespace: namespace)) { - final xmlLanguage = idElement.getAttribute('xml:lang'); - if (language == null || xmlLanguage == language) { - final identity = DiscoveryIdentity( - idElement.getAttribute('category')!, - idElement.getAttribute('type')!, - name: idElement.getAttribute('name'), - language: idElement.getAttribute('xml:lang'), - ); + /// The node associated with the disco information. + final String? node; - if (identities is Set) { - (identities as Set).add(identity); - } else { - (identities as List).add(identity); - } - } - } + /// List of identities associated with the disco information. + final identities = []; - return identities; - } + /// List of features associated with the disco information. + final features = []; - /// Adds a new identity element. Each identity must be unique in terms of all - /// four identity components. - /// - /// The XMPP Registrar maintains a registry of values for the [category] and - /// [type] attributes of the element in the - /// 'http://jabber.org/protocol/disco#info' namespace. + /// Constructs a disco information IQ stanza from an XML element node. /// - /// Multiple, identical [category]/[type] pairs allowed only if the xml:lang - /// values are different. Likewise, multiple [category]/[type]/xml:[language] - /// pairs are allowed so long as the [name]s are different. - /// - /// [category] and [type] are required. - /// - /// see: - bool addIdentity( - String category, - String type, { - String? name, - String? language, - }) { - final identity = DiscoveryIdentity(category, type, language: language); - if (!_identities.contains(identity)) { - _identities.add(identity); - final idElement = WhixpUtils.xmlElement('identity'); - idElement.setAttribute('category', category); - idElement.setAttribute('type', type); - if (language != null && language.isNotEmpty) { - idElement.setAttribute('xml:lang', language); - } - if (name != null && name.isNotEmpty) { - idElement.setAttribute('name', name); - } - element!.children.insert(0, idElement); - return true; + /// Throws a [WhixpInternalException] if the provided XML node is invalid. + factory DiscoInformation.fromXML(xml.XmlElement node) { + if (node.localName != _name) { + throw WhixpInternalException.invalidNode(node.localName, _name); } - return false; - } - - /// Adds or replaces all entities. The [identities] must be in a - /// [DiscoveryIdentity] form. - /// - /// If a [language] is specified, any [identities] using that language will be - /// removed to be replaced with the given [identities]. - void setIdentities( - Iterable identities, { - String? language, - }) { - deleteIdentities(language: language); - for (final identity in identities) { - addIdentity( - identity.category, - identity.type, - name: identity.name, - language: identity.language, - ); + String? nod; + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'node': + nod = attribute.value; + } } - } - /// Removes a given identity. - bool deleteIdentity( - String category, - String type, { - String? name, - String? language, - }) { - final identity = - DiscoveryIdentity(category, type, name: name, language: language); - if (_identities.contains(identity)) { - _identities.remove(identity); - for (final idElement - in element!.findAllElements('identity', namespace: namespace)) { - final id = DiscoveryIdentity( - idElement.getAttribute('category') ?? '', - idElement.getAttribute('type') ?? '', - language: idElement.getAttribute('xml:lang'), - ); + final info = DiscoInformation(node: nod); - if (id == identity) { - element!.children.remove(idElement); - return true; - } + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'identity': + info.identities.add(Identity.fromXML(child)); + case 'feature': + info.features.add(Feature.fromXML(child)); } } - return false; + return info; } - /// Removes all identities. If a [language] was specified, only remove - /// identities using that language. - void deleteIdentities({String? language}) { - for (final idElement - in element!.findAllElements('identity', namespace: namespace)) { - if (language == null || language.isEmpty) { - element!.children.remove(idElement); - } else if (idElement.getAttribute('xml:lang') == language) { - _identities.remove( - DiscoveryIdentity( - idElement.getAttribute('category') ?? '', - idElement.getAttribute('type') ?? '', - language: idElement.getAttribute('xml:lang'), - ), - ); - element!.children.remove(idElement); - } + /// Converts the disco information IQ stanza to its XML representation. + @override + xml.XmlElement toXML() { + final dictionary = HashMap(); + final builder = WhixpUtils.makeGenerator(); + dictionary['xmlns'] = namespace; + if (node?.isNotEmpty ?? false) { + dictionary['node'] = node!; } - } - /// Returns a [Set] or [List] of all features as so: - /// __(category, type, name, language)__ - /// - /// If [duplicate] was set to true, then use [List] as it is allowed to - /// duplicate items. - Iterable getFeatures({bool duplicate = false}) { - late final Iterable features; - if (duplicate) { - features = []; - } else { - features = {}; - } + builder.element(_name, attributes: dictionary); + final root = builder.buildDocument().rootElement; - for (final featureElement - in element!.findAllElements('feature', namespace: namespace)) { - if (features is Set) { - (features as Set).add(featureElement.getAttribute('var')); - } else { - (features as List).add(featureElement.getAttribute('var')); + if (identities.isNotEmpty) { + for (final child in identities) { + root.children.add(child.toXML().copy()); } } - return features; - } - - /// Adds a single feature. - /// - /// The XMPP Registrar maintains a registry of features for use as values of - /// the 'var' attribute of the element in the - /// 'http://jabber.org/protocol/disco#info' namespace; - /// - /// see - bool addFeature(String feature) { - if (!_features.contains(feature)) { - _features.add(feature); - final featureElement = WhixpUtils.xmlElement('feature'); - featureElement.setAttribute('var', feature); - element!.children.add(featureElement); - return true; + if (features.isNotEmpty) { + for (final feature in features) { + root.children.add(feature.toXML().copy()); + } } - return false; - } - /// Adds or replaces all supported [features]. The [features] must be in a - /// [Set] where each identity is a [String]. - void setFeatures(Iterable features) { - deleteFeatures(); - for (final feature in features) { - addFeature(feature); - } + return root; } - /// Deletes a single feature. - bool deleteFeature(String feature) { - if (_features.contains(feature)) { - _features.remove(feature); - for (final featureElement - in element!.findAllElements('feature', namespace: namespace)) { - element!.children.remove(featureElement); - return true; - } + /// Adds an identity to the disco information. + void addIdentity(String name, String category, {String? type}) { + int? index; + if (identities.where((identity) => identity.name == name).isNotEmpty) { + index = identities.indexWhere((identity) => identity.name == name); } - - return false; + final identity = Identity(name: name, category: category, type: type); + if (index != null) { + identities[index] = identity; + return; + } + identities.add(identity); } - /// Removes all features. - void deleteFeatures() { - for (final featureElement - in element!.findAllElements('feature', namespace: namespace)) { - element!.children.remove(featureElement); + /// Adds features to the disco information. + void addFeature(List namespaces) { + for (final namespace in namespaces) { + features.add(Feature(variable: namespace)); } } - /// Overrided [copy] method with `setters` and `getters` list copied. - @override - DiscoveryInformation copy({xml.XmlElement? element, XMLBase? parent}) => - DiscoveryInformation( - element: element, - parent: parent, - getters: getters, - deleters: deleters, - ); -} - -/// Represents an identity as defined in disco (service discovery) entities. -/// -/// It encapsulates information such as [category], [type], [name], and -/// [language] associated with the identity. -class DiscoveryIdentity { - /// Constructs an Identity instance with the specified [category] and [type]. - /// Optionally includes a [name] and [language] associated with the identity. - const DiscoveryIdentity(this.category, this.type, {this.name, this.language}); - - /// Gets the category of the identity. - final String category; - - /// Gets the type of the identity. - final String type; - - /// Gets the optional name associated with the identity. May be `null` if not - /// provided. - final String? name; - - /// Gets the optional language associated with the identity. May be null if - /// not provided. - final String? language; - @override - bool operator ==(Object other) => - identical(this, other) || - other is DiscoveryIdentity && - runtimeType == other.runtimeType && - category == other.category && - type == other.type && - name == other.name && - language == other.language; + String get name => _name; @override - int get hashCode => - category.hashCode ^ type.hashCode ^ name.hashCode ^ language.hashCode; + String get namespace => _namespace; @override - String toString() => - 'Discovery Identity (category: $category, type: $type, name: $name, language: $language)'; + String get tag => discoInformationTag; } diff --git a/lib/src/plugins/disco/items.dart b/lib/src/plugins/disco/items.dart new file mode 100644 index 0000000..563d27e --- /dev/null +++ b/lib/src/plugins/disco/items.dart @@ -0,0 +1,88 @@ +import 'dart:collection'; + +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +part '_item.dart'; + +final _namespace = WhixpUtils.getNamespace('DISCO_ITEMS'); + +/// Represents an XMPP disco items IQ stanza. +/// +/// Disco items IQ stanzas are used for querying and discovering items associated with a particular node. +class DiscoItems extends IQStanza { + static const _name = 'query'; + + /// Constructs a disco items IQ stanza. + DiscoItems({this.node}); + + /// The node associated with the disco items. + final String? node; + + /// List of disco items associated with the disco items. + final items = []; + + /// Constructs a disco items IQ stanza from an XML element node. + /// + /// Throws a [WhixpInternalException] if the provided XML node is invalid. + factory DiscoItems.fromXML(xml.XmlElement node) { + String? nod; + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'node': + nod = attribute.value; + } + } + + final items = DiscoItems(node: nod); + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'item': + items.items.add(DiscoItem.fromXML(child)); + } + } + + return items; + } + + /// Converts the disco items IQ stanza to its XML representation. + @override + xml.XmlElement toXML() { + final dictionary = HashMap(); + dictionary['xmlns'] = namespace; + if (node?.isNotEmpty ?? false) { + dictionary['node'] = node!; + } + + final element = WhixpUtils.xmlElement( + name, + attributes: dictionary, + namespace: namespace, + ); + for (final item in items) { + element.children.add(item.toXML().copy()); + } + + return element; + } + + /// Adds a disco item to the disco items. + void addItem(String jid, String node, String name) { + final item = DiscoItem(jid: jid, node: node, name: name); + items.add(item); + } + + @override + String get name => _name; + + @override + String get namespace => _namespace; + + @override + String get tag => discoItemsTag; +} From 0213ba27b2ecc0622107aa619243fdbef7d3fee6 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:03:56 +0400 Subject: [PATCH 49/81] refactor(handler): Update Handler class with matchers and callback function --- lib/src/handler/handler.dart | 99 ++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/lib/src/handler/handler.dart b/lib/src/handler/handler.dart index af7b672..cd5711e 100644 --- a/lib/src/handler/handler.dart +++ b/lib/src/handler/handler.dart @@ -1,52 +1,63 @@ import 'dart:async'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/base.dart'; -import 'package:whixp/src/transport.dart'; - -part 'callback.dart'; - -/// An abstract class representing a generic handler for processing XMPP -/// stanzas. -/// -/// Handlers are responsible for processing stanzas based on a defined -/// matching criteria. -/// -/// ### Example: -/// ```dart -/// class CustomHandler extends Handler{ -/// final matcher = Matcher(); -/// final handler = CustomHandler('customIQHandler', matcher: matcher); -/// } -/// -/// void main() { -/// final stanzaFromServer = StanzaBase(); -/// -/// handler.run(stanzaFromServer); -/// } -/// ``` -abstract class Handler { - /// Creates an instance of the [Handler] with the specified parameters. - Handler(this.name, {required this.matcher, this.transport}); - - /// The name of the [Handler]. +import 'package:whixp/src/handler/matcher.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/utils/utils.dart'; + +/// A handler for processing packets based on various matching criteria. +class Handler { + /// Constructs a handler with a [name] and a [callback] function. + Handler(this.name, this.callback); + + /// The name of the handler. final String name; - /// The matcher used to determine whether the handler should process the - /// given stanza. - final BaseMatcher matcher; + /// The callback function to be executed when a packet matches the handler's + /// criteria. + final FutureOr Function(Packet packet) callback; - /// This can be initialized through [Transport] class later, so cannot be - /// marked as final. - /// - /// This instance will help us to use the instance of [Transport] to send - /// over stanzas if mandatory. - Transport? transport; + /// List of matchers added to the handler. + final _matchers = []; - /// Determines whether the given [StanzaBase] object matches the criteria - /// defined by the handler's matcher. - bool match(StanzaBase stanza) => matcher.match(stanza); + /// Adds a matcher to the handler. + void addMatcher(Matcher matcher) => _matchers.add(matcher); - /// Executes the handler's logic to process the given [StanzaBase] payload. - FutureOr run(StanzaBase payload); + /// Matches the incoming packet against the registered matchers. + /// + /// If a match is found, the associated callback function is executed. Returns + /// `true` if a match is found, otherwise `false`. + bool match(Packet packet) { + for (final matcher in _matchers) { + if (matcher.match(packet)) { + callback.call(packet); + return true; + } + } + + return false; + } + + /// Adds a matcher that matches packets with a specific [name]. + void packet(String name) => addMatcher(NameMatcher(name)); + + /// Adds a matcher that contains both success and failure stanza name(s). + void sf(Tuple2 sf) => + addMatcher(SuccessAndFailureMatcher(sf)); + + /// Adds a matcher that matches packets with a specific IQ [id]. + void id(String id) => addMatcher(IQIDMatcher(id)); + + void descendant(String descendants) => + addMatcher(DescendantMatcher(descendants)); + + /// Adds a matcher that matches packets based on their stanza [types]. + void stanzaType(List types) => + addMatcher(NamespaceTypeMatcher(types)); + + /// Adds a matcher that matches IQ packets based on their [namespaces]. + void iqNamespaces(List namespaces) => addMatcher( + NamespaceIQMatcher( + namespaces.map((namespace) => namespace.toLowerCase()).toList(), + ), + ); } From 40bc8d8ee9e016b03de2da5efbd28776539efdb7 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:04:01 +0400 Subject: [PATCH 50/81] refactor(enums): Add TransportState enum for managing transport states --- lib/src/enums.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/src/enums.dart diff --git a/lib/src/enums.dart b/lib/src/enums.dart new file mode 100644 index 0000000..1c1c3fa --- /dev/null +++ b/lib/src/enums.dart @@ -0,0 +1,10 @@ +enum TransportState { + connecting, + connectionFailure, + reconnecting, + tlsSuccess, + connected, + disconnected, + killed, + terminated +} From 606d04f480b1d896b5aa5e278c78a5cf6997af9e Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:04:08 +0400 Subject: [PATCH 51/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/transport.dart | 1050 ++++++++++++++++------------------------ 1 file changed, 408 insertions(+), 642 deletions(-) diff --git a/lib/src/transport.dart b/lib/src/transport.dart index 5a82cba..cde8b7e 100644 --- a/lib/src/transport.dart +++ b/lib/src/transport.dart @@ -1,54 +1,37 @@ import 'dart:async' as async; import 'dart:io' as io; -import 'dart:math' as math; import 'package:connecta/connecta.dart'; -import 'package:dartz/dartz.dart'; import 'package:dnsolve/dnsolve.dart'; -import 'package:memoize/memoize.dart'; -import 'package:meta/meta.dart'; +import 'package:synchronized/extension.dart'; +import 'package:whixp/src/enums.dart'; +import 'package:whixp/src/exception.dart'; import 'package:whixp/src/handler/eventius.dart'; import 'package:whixp/src/handler/handler.dart'; +import 'package:whixp/src/handler/router.dart'; import 'package:whixp/src/jid/jid.dart'; import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/stanza/handshake.dart'; -import 'package:whixp/src/stanza/root.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/parser.dart'; +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/reconnection.dart'; +import 'package:whixp/src/stanza/iq.dart'; +import 'package:whixp/src/stanza/message.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/presence.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/stream.dart'; import 'package:whixp/src/utils/utils.dart'; -import 'package:whixp/src/whixp.dart'; import 'package:xml/xml.dart' as xml; -import 'package:xml/xml_events.dart' as parser; - -/// Filterization types of incoming and outgoing stanzas. -enum FilterMode { iN, out, outSync } - -/// A synchronous filter function for processing [StanzaBase] objects. -/// -/// This filter is a typedef representing a function that takes a [StanzaBase] -/// object as input, processes it synchronously, and returns a modified or -/// processed [StanzaBase]. -/// -/// It is typically used for applying synchronous transformations or filters to -/// stanza objects. -@internal -typedef SyncFilter = StanzaBase Function(StanzaBase stanza); - -/// An asynchronous filter function for processing [StanzaBase] objects. -/// -/// An AsyncFilter is a typedef representing a function that takes a -/// [StanzaBase] object as input, processes it asynchronously, and returns a -/// modified or processed [StanzaBase] wrapped in [Future]. -@internal -typedef AsyncFilter = Future Function(StanzaBase stanza); /// Designed to simplify the complexities associated with establishing a -/// connection to as server, as well as sending and receiving XML "stanzas". +/// connection to a server, as well as sending and receiving XML stanzas. /// -/// The class manages two streams, each responsible for communication in a -/// specific direction over the same socket. +/// Establishes a socket connection, accepts and sends data over this socket. class Transport { + static Transport? _instance; + /// Typically, stanzas are first processed by a [Transport] event handler /// which will then trigger ustom events to continue further processing, /// especially since custom event handlers may run in individual threads. @@ -63,26 +46,23 @@ class Transport { /// ### Example: /// ```dart /// final transport = Transport('xmpp.is', port: 5223, useTLS: true); - /// transport.connect(); /// this will connect to the "xmpp.is" on port 5223 over DirectTLS + /// /// it will connect to the "xmpp.is" on port 5223 over DirectTLS + /// transport.connect(); /// ``` - Transport( - /// The hostname that the client needs to establish a connection with - this._host, { + factory Transport( + /// Hostname that the client needs to establish a connection with + String host, { /// Defaults to 5222 int port = 5222, /// The JabberID (JID) used by this connection, as set after session /// binding. This may even be a different bare JID than what was requested - required this.boundJID, + JabberID? boundJID, /// The service name to check with DNS SRV records. For example, setting this /// to "xmpp-client" would query the "_xmpp-clilent._tcp" service. String? dnsService, - /// The distinction between clients and components can be important, primarily - /// for choosing how to handle the `to` and `from` [JabberID]s of stanzas - this.isComponent = false, - /// If set to `true`, attempt to use IPv6 bool useIPv6 = false, @@ -101,16 +81,11 @@ class Transport { /// If `true`, periodically send a whitespace character over the wire to /// keep the connection alive - this.whitespaceKeepAlive = true, + bool pingKeepAlive = true, - /// The default interval between keepalive signals when [whitespaceKeepAlive] - /// is enabled. Represents in seconds. Defaults to `300` - this.whitespaceKeepAliveInterval = 300, - - /// The maximum number of reconnection attempts that the [Transport] will - /// make in case the connection with the server is lost or cannot be - /// established initially. Defaults to 3 - this.maxReconnectionAttempt = 3, + /// The default interval between keepalive signals when [pingKeepAlive] is + /// enabled. Represents in seconds. Defaults to `180` + int pingKeepAliveInterval = 180, /// Optional [io.SecurityContext] which is going to be used in socket /// connections @@ -123,10 +98,47 @@ class Transport { bool Function(io.X509Certificate)? onBadCertificateCallback, /// Represents the duration in milliseconds for which the system will wait - /// for a connection to be established before raising a [TimeoutException]. + /// for a connection to be established before raising a + /// [async.TimeoutException]. /// - /// Defaults to 2000 milliseconds - this.connectionTimeout = 2000, + /// Defaults to `4000` milliseconds + int connectionTimeout = 4000, + required ReconnectionPolicy reconnectionPolicy, + }) { + if (_instance != null) return _instance!; + return _instance = Transport._internal( + host, + boundJID: boundJID, + port: port, + dnsService: dnsService, + useIPv6: useIPv6, + useTLS: useTLS, + disableStartTLS: disableStartTLS, + pingKeepAlive: pingKeepAlive, + pingKeepAliveInterval: pingKeepAliveInterval, + context: context, + onBadCertificateCallback: onBadCertificateCallback, + connectionTimeout: connectionTimeout, + reconnectionPolicy: reconnectionPolicy, + ); + } + + factory Transport.instance() => _instance!; + + Transport._internal( + this.host, { + required int port, + required this.boundJID, + required String? dnsService, + required bool useIPv6, + required bool useTLS, + required bool disableStartTLS, + required this.pingKeepAlive, + required this.pingKeepAliveInterval, + required io.SecurityContext? context, + required bool Function(io.X509Certificate)? onBadCertificateCallback, + required this.connectionTimeout, + required ReconnectionPolicy reconnectionPolicy, }) { _port = port; @@ -135,7 +147,7 @@ class Transport { _useIPv6 = useIPv6; _dnsService = dnsService; - endSessionOnDisconnect = false; + endSessionOnDisconnect = true; streamHeader = ''; streamFooter = ''; @@ -144,11 +156,13 @@ class Transport { _onBadCertificateCallback = onBadCertificateCallback; + _reconnectionPolicy = reconnectionPolicy; + _setup(); } /// The host that [Whixp] has to connect to. - final String _host; + final String host; /// The port that [Whixp] has to connect to. late int _port; @@ -159,16 +173,12 @@ class Transport { /// The JabberID (JID) used by this connection, as set after session binding. /// /// This may even be a different bare JID than what was requested. - late JabberID boundJID; + JabberID? boundJID; /// Will hold host that [Whixp] is connected to and will work in the /// association of [SASL]. late String serviceName; - /// The distinction between clients and components can be important, primarily - /// for choosing how to handle the `to` and `from` [JabberID]s of stanzas. - bool isComponent; - /// [Connecta] instance that will be declared when there is a connection /// attempt to the server. Connecta? _connecta; @@ -192,27 +202,24 @@ class Transport { /// a TLS connection on the client side. late bool _disableStartTLS; - /// Flag that indicates to if the session has been started or not. - late bool sessionStarted; - /// If set to `true`, attempt to use IPv6. late bool _useIPv6; /// If `true`, periodically send a whitespace character over the wire to keep /// the connection alive. - final bool whitespaceKeepAlive; + final bool pingKeepAlive; + + /// The default interval between keepalive signals when [pingKeepAliveInterval] + /// is enabled. + final int pingKeepAliveInterval; /// Controls if the session can be considered ended if the connection is /// terminated. late bool endSessionOnDisconnect; - /// The default interval between keepalive signals when [whitespaceKeepAlive] - /// is enabled. - final int whitespaceKeepAliveInterval; - - /// Default [Timer] for [whitespaceKeepAliveInterval]. This will be assigned - /// when there is a [Timer] attached after connection established. - async.Timer? _whitespaceKeepAliveTimer; + /// Default [Timer] for [pingKeepAliveInterval]. This will be assigned when + /// there is a [Timer] attached after connection established. + async.Timer? _keepAliveTimer; /// The service name to check with DNS SRV records. For example, setting this /// to "xmpp-client" would query the "_xmpp-clilent._tcp" service. @@ -222,34 +229,28 @@ class Transport { /// for a connection to be established before raising a [TimeoutException]. final int connectionTimeout; - /// The maximum number of reconnection attempts that the [Transport] will - /// make in case the connection with the server is lost or cannot be - /// established initially. Defaults to 3. - late int maxReconnectionAttempt; - /// Optional [io.SecurityContext] which is going to be used in socket /// connections. late io.SecurityContext? _context; /// [StreamController] for [_waitingQueue]. - final _waitingQueueController = - async.StreamController>.broadcast(); - final _queuedStanzas = >[]; - - late async.Completer? _runOutFilters; + late async.StreamController _waitingQueueController; + final _queuedStanzas = []; /// [Completer] of current connection attempt. After the connection, this /// [Completer] should be equal to null. async.Completer? _currentConnectionAttempt; - /// Current connection attempt count. It works with [maxReconnectionAttempt]. - int _currentConnectionAttemptCount = 0; + /// Actual [StreamParser] instance to manipulate incoming data from the + /// socket. + late StreamParser _parser; + + /// [StreamParser] parser should add list of [StreamObject] to this + /// controller. + late async.StreamController _streamController; - /// This variable is used to keep track of stream header and footer. If stream - /// is opened without closing that, it will keep its state at "1". When there - /// is a closing tag, that means it should be equal to zero and close the - /// connection. - late int _xmlDepth; + /// [StreamParser] parser will communicate with this [async.Stream]; + late async.Stream> _stream; /// [Iterator] of DNS results that have not yet been tried. Iterator>? _dnsAnswers; @@ -260,20 +261,13 @@ class Transport { /// The default closing tag for the stream element. late String streamFooter; - /// [Task] [List] to keep track of [Task]s that are going to be sent slowly. - final _slowTasks = []; - /// [io.ConnectionTask] keeps current connection task and can be used to /// cancel if there is a need. io.ConnectionTask? _connectionTask; - /// The backoff of the connection attempt (increases exponentially after each - /// failure). Represented in milliseconds; - late int _connectFutureWait; - /// The event to trigger when the [_connect] succeeds. It can be "connected" /// or "tlsSuccess" depending on the step we are at. - late String _eventWhenConnected; + late TransportState _eventWhenConnected; /// The domain to try when querying DNS records. String _defaultDomain = ''; @@ -290,18 +284,6 @@ class Transport { /// Indicates to the default langauge of the last peer. String? peerDefaultLanguage; - /// List of [Handler]s. - final _handlers = []; - - /// List of [StanzaBase] stanzas that incoming stanzas can built from. - final _rootStanza = []; - - /// Indicates session bind. - late bool sessionBind; - - /// Used when wrapping incoming xml stream. - late bool _startStreamHandlerCalled; - /// To avoid processing on bad certification you can use this callback. /// /// Passes [io.X509Certificate] instance when returning boolean value which @@ -312,81 +294,75 @@ class Transport { /// the redirection (see-other-host). late int _redirectAttempts; - /// [Map] to keep "in", "out", and "outSync" type filters to use when there - /// is a need for stanza filtering. - final _filters = >>{}; + /// Indicates if session is started or not. + bool _sessionStarted = false; - /// The reason why whixp disconnects from the server. - String? _disconnectReason; + /// Policy for reconnection, indicates delay between disconnection and + /// reconnection. + late final ReconnectionPolicy _reconnectionPolicy; - /// Indicates if stream compressed or not. - late bool streamCompressed; + /// The list of callbacks that will be triggered before the stanza send. + final callbacksBeforeStanzaSend = + Function(dynamic data)>[]; void _setup() { _reset(); - addEventHandler('disconnected', (_) { - if (_whitespaceKeepAliveTimer != null) { - _whitespaceKeepAliveTimer!.cancel(); + _reconnectionPolicy.performReconnect = () async { + await _rescheduleConnectionAttempt(); + emit('state', data: TransportState.reconnecting); + }; + + addEventHandler('state', (state) async { + if (state == TransportState.disconnected || + state == TransportState.killed) { + if (_keepAliveTimer != null) { + Log.instance.warning('Stopping Ping keep alive...'); + _keepAliveTimer?.cancel(); + } + StreamFeatures.supported.clear(); + await _waitingQueueController.close(); } - _setDisconnected(); }); - addEventHandler('sessionStart', (_) { + addEventHandler('startSession', (_) async { _setSessionStart(); _startKeepAlive(); + _sessionStarted = true; + await _reconnectionPolicy.setShouldReconnect(true); }); + addEventHandler('endSession', (_) => _sessionStarted = false); + addEventHandler('streamNegotiated', (_) => _parser.reset()); } void _reset() { serviceName = ''; - _address = Tuple2(_host, _port); + _address = Tuple2(host, _port); _eventius = Eventius(); - sessionBind = false; - sessionStarted = false; - _startStreamHandlerCalled = false; - - _runOutFilters = null; + _keepAliveTimer = null; - _whitespaceKeepAliveTimer = null; - - _eventWhenConnected = 'connected'; + _eventWhenConnected = TransportState.connected; _redirectAttempts = 0; - _connectFutureWait = 0; - _xmlDepth = 0; defaultNamespace = ''; - _handlers.clear(); - _rootStanza.clear(); - _slowTasks.clear(); - defaultLanguage = null; peerDefaultLanguage = null; - _filters - ..clear() - ..addAll({ - FilterMode.iN: >[], - FilterMode.out: >[], - FilterMode.outSync: >[], - }); - _connecta = null; _connectionTask = null; _scheduledEvents = {}; - - streamCompressed = false; } /// Begin sending whitespace periodically to keep the connection alive. void _startKeepAlive() { - if (whitespaceKeepAlive) { - _whitespaceKeepAliveTimer = async.Timer.periodic( - Duration(seconds: whitespaceKeepAliveInterval), - (_) => sendRaw(''), + Log.instance.info('Starting Ping keep alive...'); + if (pingKeepAlive) { + _keepAliveTimer = async.Timer.periodic( + Duration(seconds: pingKeepAliveInterval), + (_) => send(const SMRequest()), ); } } @@ -395,36 +371,72 @@ class Transport { /// /// The parameters needed to establish a connection in order are passed /// beforehand when creating [Transport] instance. - @internal void connect() { - if (_runOutFilters == null || _runOutFilters!.isCompleted) { - _runOutFilters = async.Completer()..complete(runFilters()); - } + _waitingQueueController = async.StreamController.broadcast(); + callbacksBeforeStanzaSend.clear(); - _disconnectReason = null; + _run(); _cancelConnectionAttempt(); - _connectFutureWait = 0; - _defaultDomain = _address.value1; + _defaultDomain = _address.firstValue; - emit('connecting'); + emit('state', data: TransportState.connecting); _currentConnectionAttempt = async.Completer()..complete(_connect()); } + void _initParser() { + _parser = StreamParser(); + _streamController = async.StreamController(); + _stream = _streamController.stream.transform(_parser); + _stream.listen((objects) async { + for (final object in objects) { + if (object is StreamHeader) { + startStreamHandler(object.attributes); + } else if (object is StreamElement) { + final element = object.element; + if (element.getAttribute('xmlns') == null) { + if (element.localName == 'message' || + element.localName == 'presence' || + element.localName == 'iq') { + element.setAttribute('xmlns', WhixpUtils.getNamespace('CLIENT')); + } else { + element.setAttribute( + 'xmlns', + WhixpUtils.getNamespace('JABBER_STREAM'), + ); + } + } + + await _spawnEvent(element); + } else { + Log.instance.info('End of stream received.'); + abort(); + } + } + }); + } + Future _connect() async { - _currentConnectionAttemptCount++; - _eventWhenConnected = 'connected'; + _eventWhenConnected = TransportState.connected; + _initParser(); - if (_connectFutureWait > 0) { - await Future.delayed(Duration(seconds: _connectFutureWait)); - } + await _reconnectionPolicy.reset(); + await _reconnectionPolicy.setShouldReconnect(true); + _parser.reset(); - final record = await _pickDNSAnswer(_defaultDomain, service: _dnsService); + Tuple3? record; + + try { + record = await _pickDNSAnswer(_defaultDomain, service: _dnsService); + } on async.TimeoutException catch (error) { + Log.instance.warning('Could not pick any SRV record'); + await _handleError(error); + } if (record != null) { - final host = record.value1; - final address = record.value2; - final port = record.value3; + final host = record.firstValue; + final address = record.secondValue; + final port = record.thirdValue; _address = Tuple2(address, port); serviceName = host; @@ -435,122 +447,148 @@ class Transport { if (_useTLS) { _connecta = Connecta( ConnectaToolkit( - hostname: _address.value1, - port: _address.value2, + hostname: _address.firstValue, + port: _address.secondValue, context: _context, timeout: connectionTimeout, connectionType: ConnectionType.tls, onBadCertificateCallback: _onBadCertificateCallback, - supportedProtocols: ['xmpp'], + supportedProtocols: ['TLSv1.2', 'TLSv1.3'], ), ); } else { _connecta = Connecta( ConnectaToolkit( - hostname: _address.value1, - port: _address.value2, + hostname: _address.firstValue, + port: _address.secondValue, timeout: connectionTimeout, context: _context, onBadCertificateCallback: _onBadCertificateCallback, connectionType: _disableStartTLS ? ConnectionType.tcp : ConnectionType.upgradableTcp, - supportedProtocols: ['xmpp'], + supportedProtocols: ['TLSv1.2', 'TLSv1.3'], ), ); } try { Log.instance.info( - 'Trying to connect to ${_address.value1} on port ${_address.value2}', + 'Trying to connect to ${_address.firstValue} on port ${_address.secondValue}', ); _connectionTask = await _connecta!.createTask( ConnectaListener( onData: _dataReceived, - onError: (error, trace) async { + onError: (exception, trace) async { Log.instance.error( 'Connection error occured.', - error: error, + exception: exception, stackTrace: trace as StackTrace, ); - final result = _rescheduleConnectionAttempt(); - if (!result) { - await disconnect(consume: false); - } + await _handleError(exception); }, onDone: _connectionLost, combineWhile: _combineWhile, ), ); - _connectionMade(); - } on ConnectaException catch (error) { - emit('connectionFailed', data: error.message); - final result = _rescheduleConnectionAttempt(); - if (!result) { - await disconnect(consume: false); - } + await _connectionMade(); } on Exception catch (error) { - emit('connectionFailed', data: error); - final result = _rescheduleConnectionAttempt(); - if (!result) { - await disconnect(consume: false); - } + await _handleError(error, connectionFailure: true); + return; } _isConnectionSecured = _connecta!.isConnectionSecure; } + Future _handleError( + dynamic exception, { + bool connectionFailure = false, + }) async { + if (exception is Exception) { + if (exception is ConnectaException) { + emit('connectionFailure', data: exception.message); + } else { + emit('connectionFailure', data: exception); + } + } + if (connectionFailure) { + emit('state', data: TransportState.connectionFailure); + } + + if (!(_currentConnectionAttempt?.isCompleted ?? false)) { + await disconnect(consume: false, sendFooter: false); + _currentConnectionAttempt = null; + try { + _connecta?.destroy(); + } catch (_) {} + return; + } + + if (await _reconnectionPolicy.canTriggerFailure()) { + await _reconnectionPolicy.onFailure(); + } else { + Log.instance.warning('Reconnection is not set'); + } + } + + Future _rescheduleConnectionAttempt() async { + if (_currentConnectionAttempt == null) { + Log.instance.warning('Current connection attempt is null, aborting...'); + return; + } + + _currentConnectionAttempt = async.Completer()..complete(_connect()); + return; + } + /// Called when the TCP connection has been established with the server. - void _connectionMade([bool clearAnswers = false]) { - emit(_eventWhenConnected); + Future _connectionMade([bool clearAnswers = false]) async { + emit('state', data: _eventWhenConnected); _currentConnectionAttempt = null; + sendRaw(streamHeader); - _initParser(); - if (clearAnswers) { - _dnsAnswers = null; - } + + await _reconnectionPolicy.onSuccess(); + if (clearAnswers) _dnsAnswers = null; } /// On any kind of disconnection, initiated by us or not. This signals the /// closure of connection. - void _connectionLost() { - Log.instance.info('Connection lost.'); - _connecta = null; + Future _connectionLost() async { + Log.instance.warning('Connection lost'); if (endSessionOnDisconnect) { - emit('sessionEnd'); - Log.instance.debug('Cancelling slow send tasks.'); - for (final task in _slowTasks) { - task.voided; - } - _slowTasks.clear(); + emit('endSession'); + Log.instance.debug('Session ended'); } - _setDisconnected(); - emit('disconnected', data: _disconnectReason); + await disconnect(sendFooter: false); } /// Performs a handshake for TLS. /// /// If the handshake is successful, the XML stream will need to be restarted. - @internal Future startTLS() async { if (_connecta == null) return false; + if (_disableStartTLS) { Log.instance.info('Disable StartTLS is enabled.'); return false; } - _eventWhenConnected = 'tlsSuccess'; + _eventWhenConnected = TransportState.tlsSuccess; try { await _connecta!.upgradeConnection( listener: ConnectaListener( onData: _dataReceived, - onError: (error, trace) => Log.instance.error( - 'Connection error occured.', - error: error, - stackTrace: trace as StackTrace, - ), + onError: (exception, trace) async { + Log.instance.error( + 'Connection error occured.', + exception: exception, + stackTrace: trace as StackTrace, + ); + await _handleError(exception, connectionFailure: true); + }, onDone: _connectionLost, combineWhile: _combineWhile, ), @@ -572,23 +610,10 @@ class Transport { /// Combines while the given condition is true. Works with [Connecta]. bool _combineWhile(List bytes) { const messageEof = {''}; - late String data; - if (streamCompressed) { - final List decompressed; - try { - decompressed = io.zlib.decode(bytes); - } on Exception { - return false; - } - data = memo0(() => WhixpUtils.unicode(decompressed)).call(); - } else { - data = memo0(() => WhixpUtils.unicode(bytes)).call(); - } + final data = WhixpUtils.unicode.call(bytes); for (final eof in messageEof) { - if (data.endsWith(eof)) { - return true; - } + if (data.endsWith(eof)) return true; } return false; @@ -597,369 +622,90 @@ class Transport { /// Called when incoming data is received on the socket. We feed that data /// to the parser and then see if this produced any XML event. This could /// trigger one or more event. - Future _dataReceived(List bytes) async { - bool wrapped = false; - late String data; - if (streamCompressed) { - late List decompressed; - try { - decompressed = io.zlib.decode(bytes); - } on Exception { - decompressed = bytes; - } - data = memo0(() => WhixpUtils.unicode(decompressed)).call(); - } else { - data = memo0(() => WhixpUtils.unicode(bytes)).call(); - } - if (data.contains('')) { - data = _streamWrapper(data); - wrapped = true; - } - - void onStartElement(parser.XmlStartElementEvent event) { - if (event.isSelfClosing || - (event.name == 'stream:stream' && _startStreamHandlerCalled)) return; - if (_xmlDepth == 0) { - /// We have received the start of the root element. - Log.instance.info('RECEIVED: $data'); - _disconnectReason = 'End of the stream'; - startStreamHandler(event.attributes); - _startStreamHandlerCalled = true; - } - _xmlDepth++; - } - - Future onEndElement(parser.XmlEndElementEvent event) async { - if (event.name == 'stream:stream' && wrapped) return; - _xmlDepth--; - if (_xmlDepth == 0) { - /// The stream's root element has closed, terminating the stream. - Log.instance.info('End of stream received.'); - abort(); - return; - } else if (_xmlDepth == 1) { - int index = data.lastIndexOf('<${event.name}'); - - if (index > event.stop!) { - index = data.indexOf('<${event.name}'); - } - - final substring = data.substring(index, event.stop); - if (substring.isEmpty) return; - - late xml.XmlElement element; - try { - element = xml.XmlDocument.parse(substring).rootElement; - } on Exception { - final fallbackSubstring = - data.substring(data.indexOf('<${event.name}'), event.stop); - try { - element = xml.XmlDocument.parse(fallbackSubstring).rootElement; - } on Exception { - return; - } - } - - if (element.getAttribute('xmlns') == null) { - if (element.localName == 'message' || - element.localName == 'presence' || - element.localName == 'iq') { - element.setAttribute('xmlns', WhixpUtils.getNamespace('CLIENT')); - } else { - element.setAttribute( - 'xmlns', - WhixpUtils.getNamespace('JABBER_STREAM'), - ); - } - } - await _spawnEvent(element); - } - } - - Stream.value(data) - .toXmlEvents(withLocation: true) - .normalizeEvents() - .tapEachEvent( - onStartElement: onStartElement, - onEndElement: onEndElement, - ) - .listen((events) async { - if (events.length == 1) { - final event = events.first; - if (event is parser.XmlStartElementEvent && - event.name == 'stream:stream') { - abort(); - return; - } else if (event is parser.XmlEndElementEvent && - event.name == 'stream:stream') { - abort(); - return; - } - } - if (events.length == 1) { - final element = xml.XmlDocument.parse(data).rootElement; - await _spawnEvent(element); - } - }); - } - - /// Helper method to wrap the incoming stanza in order not to get parser - /// error. - String _streamWrapper(String data) { - if (data.contains(''; - } - return data; + void _dataReceived(List bytes) { + final data = WhixpUtils.unicode(bytes); + _streamController.add(data); } /// Analyze incoming XML stanzas and convert them into stanza objects if - /// applicable and queue steram events to be processed by matching handlers. + /// applicable and queue stream events to be processed by matching handlers. Future _spawnEvent(xml.XmlElement element) async { final stanza = _buildStanza(element); - bool handled = false; - final handlers = []; - for (final handler in _handlers) { - if (handler.match(stanza)) { - handlers.add(handler); - } - } + Router.route(stanza); Log.instance.debug('RECEIVED: $element'); - for (final handler in handlers) { - try { - await handler.run(stanza); - } on Exception catch (excp) { - stanza.exception(excp); - } - handled = true; - } - - if (!handled) { - stanza.unhandled(this); + /// If the session is started and the upcoming stanza is one of these types, + /// then increase inbound count for SM. + if (stanza is IQ || stanza is Message || stanza is Presence) { + if (_sessionStarted) emit('increaseHandled'); } } /// Create a stanza object from a given XML object. /// /// If a specialized stanza type is not found for the XML, then a generic - /// [StanzaBase] stanza will be returned. - StanzaBase _buildStanza(xml.XmlElement element) { - StanzaBase stanzaClass = StanzaBase(element: element, receive: true); - - final tag = '{${element.getAttribute('xmlns')}}${element.localName}'; - - for (final stanza in _rootStanza) { - if (tag == stanza.tag) { - stanzaClass = stanza.copy(element: element, receive: true); - break; - } - } - if (stanzaClass['lang'] == null && peerDefaultLanguage != null) { - stanzaClass['lang'] = peerDefaultLanguage; - } - return stanzaClass; - } - - Future _slowSend( - Task task, - Set> alreadyUsed, - ) async { - final completer = async.Completer(); - completer.complete(task.run()); - - final data = await completer.future; - _slowTasks.remove(task); - if (data == null && !completer.isCompleted) { - return; - } - for (final filter in _filters[FilterMode.out]!) { - if (alreadyUsed.contains(filter)) { - continue; - } - if (filter.value2 != null) { - completer.complete(filter.value2!.call(data as StanzaBase)); - } else { - completer.complete(filter.value1?.call(data as StanzaBase)); - } - if (data == null) { - return; - } - } - } + /// [Stanza] stanza will be returned. + Packet _buildStanza(xml.XmlElement node) => + XMLParser.nextPacket(node, namespace: node.getAttribute('xmlns')); /// Background [Stream] that processes stanzas to send. - @internal - Future runFilters() async { - _waitingQueueController.stream.listen((data) async { - StanzaBase datum = data.value1; - final useFilters = data.value2; - if (useFilters) { - final alreadyRunFilters = >{}; - for (final filter in _filters[FilterMode.out]!) { - alreadyRunFilters.add(filter); - if (filter.value2 != null) { - final task = Task(() => filter.value2!.call(data.value1)); - try { - datum = await task.timeout(const Duration(seconds: 1)).run(); - } on async.TimeoutException { - /// Handle the case where the timeout occurred - Log.instance.error('Slow Future, rescheduling filters...'); - - _slowSend(task, alreadyRunFilters); - } - } else { - datum = filter.value1!.call(datum); - } - } - } - - if (useFilters) { - for (final filter in _filters[FilterMode.outSync]!) { - filter.value1!.call(datum); + void _run() => _waitingQueueController.stream.listen((data) async { + for (final callback in callbacksBeforeStanzaSend) { + await callback.call(data); } - - final rawData = datum.toString(); - - sendRaw(rawData); - } - }); - } - - /// Init the XML parser. The parser must always be reset for each new - /// connection. - void _initParser() { - _xmlDepth = 0; - _startStreamHandlerCalled = false; - } + sendRaw(data.toXMLString()); + }); /// Forcibly close the connection. void abort() { if (_connecta != null) { - _connecta!.destroy(); - emit('killed'); + _connecta?.destroy(); + emit('state', data: TransportState.killed); _cancelConnectionAttempt(); } } - /// Calls disconnect(), and once we are disconnected (after the timeout, or - /// when the server ack is received), call connect(). - void reconnect() { - Log.instance.info('Reconnecting...'); - Future handler(String? data) async { - await Future.delayed(Duration.zero); - connect(); - } - - _eventius.once('disconnected', handler); - disconnect(consume: false); - } - /// Close the XML stream and wait for ack from the server for at most /// [timeout] milliseconds. After the given number of milliseconds have passed /// without a response from the server, or when the server successfully /// responds with a closure of its own stream, abort() is called. Future disconnect({ - String? reason, int timeout = 2000, bool consume = true, + bool sendFooter = true, }) async { - Log.instance.warning('Disconnect method is called.'); - - /// Run abort() if we do not received the disconnected event after a - /// waiting time. - /// - /// Timeout defaults to 2000 milliseconds. - Future endStreamWait() async { - try { - sendRaw(streamFooter); - await _waitUntil('disconnected', timeout: timeout); - } on async.TimeoutException { - abort(); - } - } + Log.instance.warning('Disconnect method is called'); + if (sendFooter) sendRaw(streamFooter); Future consumeSend() async { try { await _waitingQueueController.done .timeout(Duration(milliseconds: timeout)); - } on async.TimeoutException { - /// pass; + } on Exception { + /// pass + } finally { + _connecta?.destroy(); + _cancelConnectionAttempt(); + emit('state', data: TransportState.disconnected); } - _disconnectReason = reason; - await endStreamWait(); } if (_connecta != null && consume) { - _disconnectReason = reason; - await consumeSend(); - - return _cancelConnectionAttempt(); + return consumeSend(); } else { - emit('disconnected', data: reason); - return; - } - } - - /// Utility method to wake on the next firing of an event. - Future _waitUntil(String event, {int timeout = 15000}) async { - final completer = async.Completer(); - - void handler(String? data) { - if (completer.isCompleted) { - Log.instance - .debug('Completer registered on event "$event" is already done.'); - } else { - completer.complete(data); - } + emit('state', data: TransportState.disconnected); + return _connecta?.destroy(); } - - addEventHandler(event, handler, once: true); - - /// Emit disconnected while consuming. - emit('disconnected'); - - return completer.future.timeout(Duration(milliseconds: timeout)); } - /// Adds a stanza class as a known root stanza. - /// - /// A root stanza is one that appears as a direct child of the stream's root - /// element. - /// - /// Stanzas that appear as substanzas of a root stanza do not need to be - /// registered here. That is done using [registerPluginStanza] from [XMLBase]. - void registerStanza(StanzaBase stanza) => _rootStanza.add(stanza); - - /// Removes a stanza from being a known root stanza. - /// - /// A root stanza is one that appears as a direct child of the stream's root - /// element. - /// - /// Stanzas that are not registered will not be converted into stanza objects, - /// but may still be processed using handlers and matchers. - void removeStanza(StanzaBase stanza) => _rootStanza.remove(stanza); - /// Add a stream event handler that will be executed when a matching stanza /// is received. - void registerHandler(Handler handler) { - if (handler.transport == null) { - handler.transport = this; - _handlers.add(handler); - } - } + void registerHandler(Handler handler) => Router.addHandler(handler); /// Removes any transport callback handlers with the given [name]. - bool removeHandler(String name) { - for (final handler in _handlers) { - if (handler.name == name) { - _handlers.remove(handler); - return true; - } - } - return false; - } + void removeHandler(String name) => Router.removeHandler(name); /// Triggers a custom [event] manually. async.FutureOr emit(String event, {T? data}) async => @@ -967,7 +713,6 @@ class Transport { /// Adds a custom [event] handler that will be executed whenever its event is /// manually triggered. Works with [Eventius] instance. - @internal void addEventHandler( String event, async.FutureOr Function(B? data) listener, { @@ -1043,29 +788,16 @@ class Transport { /// Immediately cancel the current [connect] [Future]. void _cancelConnectionAttempt() { _currentConnectionAttempt = null; - if (_connectionTask != null) { - _connectionTask!.cancel(); - } - _currentConnectionAttemptCount = 0; + _connectionTask?.cancel(); _connecta = null; - } - - bool _rescheduleConnectionAttempt() { - if (_currentConnectionAttempt == null || - (maxReconnectionAttempt <= _currentConnectionAttemptCount)) { - return false; - } - _connectFutureWait = math.min(300, _connectFutureWait * 2 + 1); - _currentConnectionAttempt = async.Completer()..complete(_connect()); - return true; + _sessionStarted = false; } /// Performs any initialization actions, such as handshakes, once the stream /// header has been sent. /// /// Must be overrided. - late void Function([List? attributes]) - startStreamHandler; + late void Function(Map attributes) startStreamHandler; /// Pick a server and port from DNS answers. /// @@ -1078,16 +810,26 @@ class Transport { String domain, { String? service, }) async { - Log.instance.warning('DNS: Use of IPv6 has been disabled.'); - ResolveResponse? response; final srvs = []; final results = >[]; if (service != null) { - response = await DNSolve() - .lookup('_$service._tcp.$domain', type: RecordType.srv) - .timeout(Duration(milliseconds: connectionTimeout)); + try { + response = await DNSolve() + .lookup('_$service._tcp.$domain', type: RecordType.srv) + .timeout( + Duration(milliseconds: connectionTimeout), + onTimeout: () { + throw async.TimeoutException( + 'Connection timed out', + Duration(milliseconds: connectionTimeout), + ); + }, + ); + } catch (_) { + /// pass + } } if (response != null && @@ -1131,16 +873,29 @@ class Transport { } if (_useIPv6) { - response = await DNSolve().lookup(domain, type: RecordType.aaaa); + try { + response = await DNSolve().lookup(domain, type: RecordType.aaaa); + } catch (_) { + Log.instance.warning( + 'DNS lookup: Failed to parse IPv6 records for $domain, processing with provided record', + ); + return null; + } } else { - response = await DNSolve().lookup(domain); + Log.instance.warning('DNS lookup: Use of IPv6 has been disabled'); + try { + response = await DNSolve().lookup(domain); + } catch (_) { + Log.instance.warning( + 'DNS lookup: Failed to parse records for $domain, processing with provided record', + ); + return null; + } } if (response.answer != null && response.answer!.records != null) { for (final record in response.answer!.records!) { - results.add( - Tuple3(domain, record.name, _address.value2), - ); + results.add(Tuple3(domain, record.name, _address.secondValue)); } } @@ -1157,8 +912,10 @@ class Transport { return null; } - @internal - void handleStreamError(String otherHost, {int maxRedirects = 5}) { + Future handleStreamError( + String otherHost, { + int maxRedirects = 5, + }) async { if (_redirectAttempts > maxRedirects) { return; } @@ -1182,102 +939,111 @@ class Transport { _address = Tuple2(host, port); _defaultDomain = host; _dnsAnswers = null; - reconnect(); - } - - /// Add a filter for incoming or outgoing stnzas. - /// - /// These filters are applied before incoming stanzas are passed to any - /// handlers, and before outgoing stanzas are put in the send queue. - /// - /// [mode] can be "iN", "out", and "outSync". Either sync or async filter - /// can be passed at the same time. You can not assign two type filters once. - void addFilter({ - FilterMode mode = FilterMode.iN, - SyncFilter? filter, - AsyncFilter? asyncFilter, - }) { - if (filter != null) { - _filters[mode]!.add(Tuple2(filter, null)); - } else if (asyncFilter != null) { - _filters[mode]!.add(Tuple2(null, asyncFilter)); - } - } - - /// Removes an incoming or outgoing filter. - void removeFilter({ - FilterMode mode = FilterMode.iN, - SyncFilter? filter, - AsyncFilter? asyncFilter, - }) { - if (_filters[mode] != null) { - _filters[mode]!.remove(Tuple2(filter, asyncFilter)); - } } /// Wraps basic send method declared in this class privately. Helps to send - /// [StanzaBase] objects. - void send(StanzaBase data, {bool useFilters = true}) { - if (!sessionStarted) { + /// [Extension] objects. + void send(Packet data) { + if (!_sessionStarted) { bool passthrough = false; - if (data is RootStanza && !passthrough) { - if (data.getPlugin('bind', check: true) != null) { - passthrough = true; - } else if (data.getPlugin('session', check: true) != null) { - passthrough = true; - } else if (data.getPlugin('register', check: true) != null) { + if (!passthrough) { + if (data.name.startsWith('sasl') || + data.name.startsWith('sm') || + data is IQ) { passthrough = true; + } else { + switch (data.name) { + case 'proceed': + passthrough = true; + case 'bind': + passthrough = true; + case 'session': + passthrough = true; + case 'register': + passthrough = true; + } } - } else if (data is Handshake) { - passthrough = true; } - if (data is RootStanza && !passthrough) { - _queuedStanzas.add(Tuple2(data, useFilters)); - return; - } + if (!passthrough) return _queuedStanzas.add(data); } - _waitingQueueController.add(Tuple2(data, useFilters)); + + _waitingQueueController.add(data); } - /// Send raw data accross the socket. - /// - /// [data] can be either [List] of integers or [String]. - void sendRaw(dynamic data) { - List rawData; - if (data is String) { - rawData = WhixpUtils.utf8Encode(data); - } else if (data is List) { - rawData = data; + Future sendAwait( + String handlerName, + Packet data, + String successPacket, { + int timeout = 3, + String? failurePacket, + }) async { + final completer = async.Completer(); + + final handler = Handler( + handlerName, + (stanza) async { + if (stanza is S) { + await Future.microtask(() { + if (!completer.isCompleted) completer.complete(stanza); + }).timeout(Duration(seconds: timeout)); + removeHandler(handlerName); + } else if (stanza is F) { + await Future.microtask(() { + if (!completer.isCompleted) completer.complete(null); + }).timeout(Duration(seconds: timeout)); + removeHandler(handlerName); + } + }, + ); + + if (failurePacket?.isNotEmpty ?? false) { + handler.sf(Tuple2(successPacket, failurePacket!)); } else { - throw ArgumentError( - 'Passed data to be sent is neither List nor String', - ); + handler.packet(successPacket); } - Log.instance.debug('SEND: ${WhixpUtils.unicode(rawData)}'); + registerHandler(handler); - if (_connecta != null) { - if (streamCompressed) { - _connecta!.send(io.zlib.encode(rawData)); - } else { - _connecta!.send(rawData); - } + void callbackTimeout() { + async.runZonedGuarded( + () { + if (!completer.isCompleted) { + completer.complete(null); + throw StanzaException.timeout(null); + } + }, + (error, trace) => Log.instance.warning(error.toString()), + ); + removeHandler(handlerName); } + + schedule(handlerName, callbackTimeout, seconds: timeout); + send(data); + + return synchronized(() => completer.future); + } + + /// Send raw data accross the socket. + /// + /// [data] can be either [List] of integers or [String]. + void sendRaw(String data) { + final raw = WhixpUtils.utf8Encode(data); + + Log.instance.debug('SEND: ${WhixpUtils.unicode(raw)}'); + + if (_connecta != null) _connecta?.send(raw); } /// On session start, queue all pending stanzas to be sent. void _setSessionStart() { - sessionStarted = true; for (final stanza in _queuedStanzas) { _waitingQueueController.add(stanza); } _queuedStanzas.clear(); } - void _setDisconnected() => sessionStarted = false; - /// Host and port keeper. First value refers to host and the second to port. Tuple2 get address => _address; From 5883a1d0eca5132aebec56191a565054d05b89a5 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:04:16 +0400 Subject: [PATCH 52/81] refactor: Add StartTLS, TLSProceed, and TLSFailure classes for XMPP TLS negotiation --- lib/src/plugins/starttls.dart | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 lib/src/plugins/starttls.dart diff --git a/lib/src/plugins/starttls.dart b/lib/src/plugins/starttls.dart new file mode 100644 index 0000000..cbb2b8c --- /dev/null +++ b/lib/src/plugins/starttls.dart @@ -0,0 +1,106 @@ +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/transport.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +final _namespace = WhixpUtils.getNamespace('STARTTLS'); + +/// Represents a StartTLS stanza. +/// +/// This stanza is used to initiate a TLS negotiation. +class StartTLS extends Stanza { + /// Constructs a [StartTLS] stanza. + const StartTLS({this.required = false}); + + /// Indicates whether TLS is required. + /// + /// Default value is `false`. + final bool required; + + /// Constructs a [StartTLS] stanza from XML. + factory StartTLS.fromXML(xml.XmlElement node) { + if (node.getAttribute('xmlns') != _namespace) { + throw WhixpInternalException.invalidNode(node.localName, 'proceed'); + } + + bool required = false; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'required': + required = true; + } + } + + final tls = StartTLS(required: required); + + return tls; + } + + @override + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement(name, namespace: _namespace); + if (required) { + element.children.add(WhixpUtils.xmlElement('required').copy()); + } + + return element; + } + + /// Handle notification that the server supports TLS. + static bool handleStartTLS() { + StreamFeatures.supported.add('starttls'); + Transport.instance().send(const TLSProceed()); + return true; + } + + @override + String get name => 'starttls'; +} + +/// Represents a TLS Proceed stanza. +/// +/// This stanza is used to indicate that the TLS negotiation can proceed. +class TLSProceed extends Stanza { + /// Constructs a [TLSProceed] stanza. + const TLSProceed(); + + /// Constructs a [TLSProceed] stanza from XML. + factory TLSProceed.fromXML(xml.XmlElement node) { + if (node.getAttribute('xmlns') != _namespace) { + throw WhixpInternalException.invalidNode(node.localName, 'proceed'); + } + return const TLSProceed(); + } + + @override + xml.XmlElement toXML() => WhixpUtils.xmlElement(name, namespace: _namespace); + + @override + String get name => 'proceed'; +} + +/// Represents a TLS Failure stanza. +/// +/// This stanza is used to indicate that the TLS negotiation has failed. +class TLSFailure extends Stanza { + /// Constructs a [TLSFailure] stanza. + TLSFailure(); + + /// Constructs a [TLSFailure] stanza from XML. + factory TLSFailure.fromXML(xml.XmlElement node) { + if (node.getAttribute('xmlns') != _namespace) { + throw WhixpInternalException.invalidNode(node.localName, 'failure'); + } + return TLSFailure(); + } + + @override + xml.XmlElement toXML() => WhixpUtils.xmlElement(name, namespace: _namespace); + + @override + String get name => 'failure'; +} From 9d4ec3dc1564c2f03f76d3d762a3857f48b38dfd Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:04:24 +0400 Subject: [PATCH 53/81] refactor: Add Version class for representing XMPP version IQ stanzas --- lib/src/plugins/version.dart | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 lib/src/plugins/version.dart diff --git a/lib/src/plugins/version.dart b/lib/src/plugins/version.dart new file mode 100644 index 0000000..11c2512 --- /dev/null +++ b/lib/src/plugins/version.dart @@ -0,0 +1,92 @@ +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +/// Represents an XMPP version IQ stanza. +/// +/// This stanza is typically used to query for information about the software +/// version of an XMPP entity. +/// +/// Example usage: +/// ```xml +/// +/// +/// +/// ``` +class Version extends IQStanza { + static const String _namespace = 'jabber:iq:version'; + static const String _name = 'query'; + + /// Constructs a version IQ stanza. + const Version({this.versionName, this.version, this.os}); + + /// The name of the software version. + final String? versionName; + + /// The version of the software. + final String? version; + + /// The operating system information. + final String? os; + + /// Constructs a version IQ stanza from an XML element node. + factory Version.fromXML(xml.XmlElement node) { + String? versionName; + String? version; + String? os; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'name': + versionName = child.innerText; + case 'version': + version = child.innerText; + case 'os': + os = child.innerText; + } + } + + return Version(versionName: versionName, version: version, os: os); + } + + /// Converts the version IQ stanza to its XML representation. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element( + name, + nest: () { + if (versionName?.isNotEmpty ?? false) { + builder.element('name', nest: () => builder.text(versionName!)); + } + if (version?.isNotEmpty ?? false) { + builder.element('version', nest: () => builder.text(version!)); + } + if (os?.isNotEmpty ?? false) { + builder.element('os', nest: () => builder.text(os!)); + } + }, + ); + + return builder.buildDocument().rootElement + ..setAttribute('xmlns', namespace); + } + + /// Sets the information for the software version. + Version setInfo({String? name, String? version, String? os}) => + Version(versionName: name, version: version, os: os); + + /// Returns the name of the version IQ stanza. + @override + String get name => _name; + + /// Returns the namespace of the version IQ stanza. + @override + String get namespace => _namespace; + + @override + String get tag => versionTag; +} From 832200cc0d2efd599aff95e9ec747fbda07dc12f Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:04:47 +0400 Subject: [PATCH 54/81] refactor: Add SASLMechanisms class for handling SASL mechanisms in XMPP --- lib/src/plugins/mechanisms/feature.dart | 149 +++++++----------- lib/src/plugins/mechanisms/mechanisms.dart | 45 ++++++ lib/src/plugins/mechanisms/stanza/_auth.dart | 52 +++--- .../plugins/mechanisms/stanza/_challenge.dart | 51 +++--- .../plugins/mechanisms/stanza/_failure.dart | 109 ++++++------- .../plugins/mechanisms/stanza/_response.dart | 53 ++++--- .../plugins/mechanisms/stanza/_success.dart | 52 +++--- 7 files changed, 261 insertions(+), 250 deletions(-) create mode 100644 lib/src/plugins/mechanisms/mechanisms.dart diff --git a/lib/src/plugins/mechanisms/feature.dart b/lib/src/plugins/mechanisms/feature.dart index a689668..a796857 100644 --- a/lib/src/plugins/mechanisms/feature.dart +++ b/lib/src/plugins/mechanisms/feature.dart @@ -1,15 +1,15 @@ -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; +import 'dart:collection'; import 'package:whixp/src/exception.dart'; import 'package:whixp/src/handler/handler.dart'; import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; +import 'package:whixp/src/plugins/features.dart'; import 'package:whixp/src/sasl/sasl.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/transport.dart'; import 'package:whixp/src/utils/utils.dart'; +import 'package:whixp/src/whixp.dart'; import 'package:xml/xml.dart' as xml; @@ -18,30 +18,36 @@ part 'stanza/_failure.dart'; part 'stanza/_challenge.dart'; part 'stanza/_response.dart'; part 'stanza/_success.dart'; -part 'stanza/stanza.dart'; -@internal typedef SASLCallback = Map Function( Set required, Set optional, ); -@internal typedef SecurityCallback = Map Function(Set values); -class FeatureMechanisms extends PluginBase { - FeatureMechanisms({ +const String _namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'; +const String _challenge = 'sasl:challenge'; +const String _response = 'sasl:response'; +const String _success = 'sasl:success'; +const String _failure = 'sasl:failure'; + +class FeatureMechanisms { + FeatureMechanisms( + this.whixp, { this.saslCallback, this.securityCallback, bool encryptedPlain = false, bool unencryptedPlain = false, bool unencryptedScram = true, - }) : super('mechanisms', description: 'SASL') { + }) { _encryptedPlain = encryptedPlain; _unencryptedPlain = unencryptedPlain; _unencryptedScram = unencryptedScram; } + final WhixpBase whixp; + SASLCallback? saslCallback; SecurityCallback? securityCallback; @@ -54,50 +60,18 @@ class FeatureMechanisms extends PluginBase { late final attemptedMechanisms = []; late Mechanism _mech; - @override void pluginInitialize() { - base.registerFeature( - 'mechanisms', - _handleSASLAuth, - restart: true, - order: 100, - ); - - final challenge = _Challenge(); - final success = _Success(); - final failure = _Failure(); - - base.transport + Transport.instance() ..registerHandler( - CallbackHandler( - 'SASL Challenge', - (stanza) => _handleChallenge(challenge.copy(element: stanza.element)), - matcher: XPathMatcher(challenge.tag), - ), + Handler('SASL Challenge', _handleChallenge)..packet(_challenge), ) ..registerHandler( - CallbackHandler( - 'SASL Success', - (stanza) => _handleSuccess(success.copy(element: stanza.element)), - matcher: XPathMatcher(success.tag), - ), + Handler('SASL Success', _handleSuccess)..packet(_success), ) ..registerHandler( - CallbackHandler( - 'SASL Failure', - (stanza) => _handleFailure(failure.copy(element: stanza.element)), - matcher: XPathMatcher(failure.tag), - ), + Handler('SASL Failure', _handleFailure)..packet(_failure), ); - base.transport - ..registerStanza(_Auth()) - ..registerStanza(_Failure()) - ..registerStanza(challenge) - ..registerStanza(_Response()) - ..registerStanza(success) - ..registerStanza(failure); - saslCallback ??= _defaultCredentials; securityCallback ??= _defaultSecurity; } @@ -106,20 +80,20 @@ class FeatureMechanisms extends PluginBase { Set required, Set optional, ) { - final credentials = base.credentials; + final credentials = whixp.credentials; final results = {}; final params = {...required, ...optional}; for (final param in params) { if (param == 'username') { - results[param] = credentials[param] ?? base.requestedJID.user; + results[param] = credentials[param] ?? whixp.requestedJID.user; } else if (param == 'email') { - final jid = base.requestedJID.bare; + final jid = whixp.requestedJID.bare; results[param] = credentials[param] ?? jid; } else if (param == 'host') { - results[param] = base.requestedJID.domain; + results[param] = whixp.requestedJID.domain; } else if (param == 'service-name') { - results[param] = base.transport.serviceName; + results[param] = Transport.instance().serviceName; } else if (param == 'service') { results[param] = credentials[param] ?? 'xmpp'; } else if (credentials.keys.contains(param)) { @@ -135,9 +109,9 @@ class FeatureMechanisms extends PluginBase { for (final value in values) { if (value == 'encrypted') { - if (base.features.contains('starttls')) { + if (StreamFeatures.supported.contains('starttls')) { result[value] = true; - } else if (base.transport.isConnectionSecured) { + } else if (Transport.instance().isConnectionSecured) { result[value] = true; } else { result[value] = false; @@ -157,12 +131,10 @@ class FeatureMechanisms extends PluginBase { return result; } - bool _handleSASLAuth(StanzaBase stanza) { - if (base.features.contains('mechanisms')) { - return false; - } - - mechanisms.addAll(stanza['mechanisms'] as List); + bool handleSASLAuth(Packet features) { + if (StreamFeatures.supported.contains('mechanisms')) return false; + if (features is! StreamFeatures) return false; + mechanisms.addAll(features.mechanisms?.list ?? []); return _sendAuthentication(); } @@ -170,73 +142,66 @@ class FeatureMechanisms extends PluginBase { bool _sendAuthentication() { final mechList = mechanisms ..removeWhere((mech) => attemptedMechanisms.contains(mech)); - final sasl = SASL(base); + final sasl = SASL(whixp); try { _mech = sasl.choose(mechList, saslCallback!, securityCallback!); } on SASLException { - Log.instance.error('No appropriate login method'); + Log.instance + .error('No appropriate login method found. Aborting connection...'); - base.transport.disconnect(); + Transport.instance().disconnect(consume: false); return false; } on StringPreparationException { Log.instance.error('A credential value did not pass SASL preperation'); - base.transport.disconnect(); + Transport.instance().disconnect(consume: false); return false; } - final response = _Auth(transport: base.transport); - response['mechanism'] = _mech.name; + String? body; try { - response['value'] = WhixpUtils.btoa(_mech.process()); + body = WhixpUtils.btoa(_mech.process()); } on SASLException { attemptedMechanisms.add(_mech.name); return _sendAuthentication(); } - response.send(); + Transport.instance().send(_Auth(mechanism: _mech.name, body: body)); return true; } - void _handleChallenge(StanzaBase stanza) { - final response = _Response(transport: base.transport); + void _handleChallenge(Packet challenge) { + if (challenge is! SASLChallenge) return; + String? body; try { - response['value'] = _mech.challenge(stanza['value'] as String); + body = _mech.challenge(challenge.body!); } on SASLException { - /// Disconnects if there is any [SASLException] occures. - base.transport.disconnect(); + /// Disconnect s if there is any [SASLException] occures. + Transport.instance().disconnect(); return; } - response.send(); + Transport.instance().send(SASLResponse(body: body)); } - void _handleSuccess(StanzaBase stanza) { + void _handleSuccess(Packet success) { + if (success is! SASLSuccess) return; attemptedMechanisms.clear(); - base.features.add('mechanisms'); - base.transport.sendRaw(base.transport.streamHeader); + StreamFeatures.supported.add('mechanisms'); + Transport.instance().sendRaw(Transport.instance().streamHeader); } - bool _handleFailure(StanzaBase stanza) { + bool _handleFailure(Packet failure) { + if (failure is! SASLFailure) return false; attemptedMechanisms.add(_mech.name); Log.instance.info( - 'Authentication failed: ${stanza['condition']}, mechanism: ${_mech.name}', - ); - base.transport.emit( - 'failedAuthentication', - data: stanza['condition'] as String, + 'Authentication failed: ${failure.reason}, mechanism: ${_mech.name}', ); + Transport.instance() + .emit('failedAuthentication', data: failure.reason); _sendAuthentication(); return true; } - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} } diff --git a/lib/src/plugins/mechanisms/mechanisms.dart b/lib/src/plugins/mechanisms/mechanisms.dart new file mode 100644 index 0000000..2f401f2 --- /dev/null +++ b/lib/src/plugins/mechanisms/mechanisms.dart @@ -0,0 +1,45 @@ +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +/// Represents SASL mechanisms. +/// +/// This class handles SASL mechanisms supported by the server. +class SASLMechanisms { + /// The name of the mechanisms. + static const name = 'mechanisms'; + + /// Constructs a [SASLMechanisms] instance. + SASLMechanisms(); + + /// List of supported mechanisms. + final list = []; + + /// Constructs a [SASLMechanisms] instance from XML. + factory SASLMechanisms.fromXML(xml.XmlElement node) { + final mechanisms = SASLMechanisms(); + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'mechanism': + mechanisms.list.add(child.innerText); + } + } + + return mechanisms; + } + + /// Converts [SASLMechanisms] instance to XML. + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement( + name, + namespace: 'urn:ietf:params:xml:ns:xmpp-sasl', + ); + + for (final mech in list) { + element.children.add(xml.XmlText(mech).copy()); + } + + return element; + } +} diff --git a/lib/src/plugins/mechanisms/stanza/_auth.dart b/lib/src/plugins/mechanisms/stanza/_auth.dart index bff421b..dea9df4 100644 --- a/lib/src/plugins/mechanisms/stanza/_auth.dart +++ b/lib/src/plugins/mechanisms/stanza/_auth.dart @@ -1,25 +1,33 @@ part of '../feature.dart'; -class _Auth extends StanzaBase { - _Auth({super.transport}) - : super( - name: 'auth', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'mechanism', 'value'}, - pluginAttribute: 'auth', - getters: { - const Symbol('value'): (args, base) => - WhixpUtils.arrayBufferToBase64( - Uint8List.fromList(base.element!.innerText.codeUnits), - ), - }, - setters: { - const Symbol('value'): (values, args, base) => - base.element!.innerText = WhixpUtils.unicode(values), - }, - deleters: { - const Symbol('value'): (args, base) => base.element!.innerText = '', - }, - ); +/// Represents an authentication packet. +/// +/// This packet is used for authentication in the Simple Authentication and +/// Security Layer (SASL). +class _Auth with Packet { + static const String _name = 'auth'; + + /// Constructs an [_Auth] packet. + const _Auth({this.mechanism, this.body}); + + /// The authentication mechanism. + final String? mechanism; + + /// The authentication body. + final String? body; + + @override + xml.XmlElement toXML() { + final dictionary = HashMap(); + dictionary['xmlns'] = _namespace; + if (mechanism?.isNotEmpty ?? false) dictionary['mechanism'] = mechanism!; + + final element = WhixpUtils.xmlElement(_name, attributes: dictionary); + if (body != null) element.children.add(xml.XmlText(body!).copy()); + + return element; + } + + @override + String get name => 'sasl:$_name'; } diff --git a/lib/src/plugins/mechanisms/stanza/_challenge.dart b/lib/src/plugins/mechanisms/stanza/_challenge.dart index 94df997..a0800d6 100644 --- a/lib/src/plugins/mechanisms/stanza/_challenge.dart +++ b/lib/src/plugins/mechanisms/stanza/_challenge.dart @@ -1,28 +1,29 @@ part of '../feature.dart'; -class _Challenge extends StanzaBase { - _Challenge() - : super( - name: 'challenge', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'value'}, - pluginAttribute: 'challenge', - getters: { - const Symbol('value'): (args, base) => - WhixpUtils.atob(base.element!.innerText), - }, - setters: { - const Symbol('value'): (value, args, base) { - if ((value as String).isNotEmpty) { - base.element!.innerText = WhixpUtils.btoa(value); - } else { - base.element!.innerText = '='; - } - }, - }, - deleters: { - const Symbol('value'): (args, base) => base.element!.innerText = '', - }, - ); +/// Represents a challenge packet. +/// +/// This packet is used to send a challenge during an authentication process. +class SASLChallenge with Packet { + /// Constructs a [SASLChallenge] packet. + const SASLChallenge({this.body}); + + /// The body of the challenge. + final String? body; + + /// Constructs a [SASLChallenge] packet from XML. + factory SASLChallenge.fromXML(xml.XmlElement node) => + SASLChallenge(body: WhixpUtils.atob(node.innerText)); + + @override + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement('challenge', namespace: _namespace); + if (body?.isNotEmpty ?? false) { + element.children.add(xml.XmlText(body!).copy()); + } + + return element; + } + + @override + String get name => _challenge; } diff --git a/lib/src/plugins/mechanisms/stanza/_failure.dart b/lib/src/plugins/mechanisms/stanza/_failure.dart index 2a676b4..ef53f79 100644 --- a/lib/src/plugins/mechanisms/stanza/_failure.dart +++ b/lib/src/plugins/mechanisms/stanza/_failure.dart @@ -1,73 +1,60 @@ part of '../feature.dart'; -class _Failure extends StanzaBase { - _Failure() - : super( - name: 'failure', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'condition', 'text'}, - pluginAttribute: 'failure', - subInterfaces: {'text'}, - ) { - addGetters( - { - const Symbol('condition'): (args, base) { - for (final child in base.element!.childElements) { - final condition = child.localName; - if (_conditions.contains(condition)) { - return condition; - } - } - return 'not-authorized'; - }, - }, - ); +/// Represents a failure packet. +/// +/// This packet is used to indicate a failure in an operation. +class SASLFailure with Packet { + /// Constructs a [SASLFailure] packet. + const SASLFailure({this.reason, this.type, this.any}); - addSetters( - { - const Symbol('condition'): (value, args, base) { - if (_conditions.contains(value)) { - base.delete('condition'); - base.element!.children - .add(xml.XmlElement(xml.XmlName(value as String))); - } - }, - }, - ); + /// The reason for the failure. + final String? reason; + + /// The type of failure. + final String? type; + + /// Additional nodes associated with the failure. + final Nodes? any; + + /// Constructs a [SASLFailure] packet from XML. + factory SASLFailure.fromXML(xml.XmlElement node) { + String? reason; + String? type; - addDeleters( - { - const Symbol('condition'): (args, base) { - for (final child in base.element!.children) { - final condition = child.innerText; - if (_conditions.contains(condition)) { - base.element!.children.remove(child); - } - } - }, - }, + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'text': + reason = child.innerText; + case 'type': + type = child.localName; + } + } + final failure = SASLFailure( + reason: reason, + type: type, + any: Nodes.fromXML( + node.children + .whereType() + .map((node) => Node.fromXML(node)) + .toList(), + ), ); + + return failure; } @override - bool setup([xml.XmlElement? element]) { - if (element != null) { - this['condition'] = 'not-authorized'; + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement('failure', namespace: _namespace); + if (any?.nodes.isNotEmpty ?? false) { + for (final node in any!.nodes) { + element.children.add(node.toXML().copy()); + } } - return super.setup(element); + + return element; } - final _conditions = { - 'aborted', - 'account-disabled', - 'credentials-expired', - 'encryption-required', - 'incorrect-encoding', - 'invalid-authzid', - 'invalid-mechanism', - 'malformed-request', - 'mechansism-too-weak', - 'not-authorized', - 'temporary-auth-failure', - }; + @override + String get name => _failure; } diff --git a/lib/src/plugins/mechanisms/stanza/_response.dart b/lib/src/plugins/mechanisms/stanza/_response.dart index e350253..1e57a54 100644 --- a/lib/src/plugins/mechanisms/stanza/_response.dart +++ b/lib/src/plugins/mechanisms/stanza/_response.dart @@ -1,28 +1,31 @@ part of '../feature.dart'; -class _Response extends StanzaBase { - _Response({super.transport}) - : super( - name: 'response', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'value'}, - pluginAttribute: 'response', - getters: { - const Symbol('value'): (args, base) => - WhixpUtils.atob(base.element!.innerText), - }, - setters: { - const Symbol('value'): (value, args, base) { - if ((value as String).isNotEmpty) { - base.element!.innerText = WhixpUtils.btoa(value); - } else { - base.element!.innerText = '='; - } - }, - }, - deleters: { - const Symbol('value'): (args, base) => base.element!.innerText = '', - }, - ); +/// Represents a response packet. +/// +/// This packet is used to send a response during an authentication process. +class SASLResponse with Packet { + /// Constructs a [SASLResponse] packet. + const SASLResponse({this.body}); + + /// The body of the response. + final String? body; + + /// Constructs a [SASLResponse] packet from XML. + factory SASLResponse.fromXML(xml.XmlElement node) => + SASLResponse(body: node.innerText); + + @override + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement('response', namespace: _namespace); + + element.children.add( + xml.XmlText((body?.isNotEmpty ?? false) ? WhixpUtils.btoa(body!) : '=') + .copy(), + ); + + return element; + } + + @override + String get name => _response; } diff --git a/lib/src/plugins/mechanisms/stanza/_success.dart b/lib/src/plugins/mechanisms/stanza/_success.dart index be432e9..f721b63 100644 --- a/lib/src/plugins/mechanisms/stanza/_success.dart +++ b/lib/src/plugins/mechanisms/stanza/_success.dart @@ -1,28 +1,30 @@ part of '../feature.dart'; -class _Success extends StanzaBase { - _Success() - : super( - name: 'success', - namespace: WhixpUtils.getNamespace('SASL'), - interfaces: {'value'}, - pluginAttribute: 'success', - getters: { - const Symbol('value'): (args, base) => - WhixpUtils.atob(base.element!.innerText), - }, - setters: { - const Symbol('value'): (value, args, base) { - if ((value as String).isNotEmpty) { - base.element!.innerText = WhixpUtils.btoa(value); - } else { - base.element!.innerText = '='; - } - }, - }, - deleters: { - const Symbol('value'): (args, base) => base.element!.innerText = '', - }, - ); +/// Represents a success packet. +/// +/// This packet is used to indicate a successful operation. +class SASLSuccess with Packet { + /// Constructs a [SASLSuccess] packet. + const SASLSuccess({this.body}); + + /// The body of the success message. + final String? body; + + /// Constructs a [SASLSuccess] packet from XML. + factory SASLSuccess.fromXML(xml.XmlElement node) { + final success = SASLSuccess(body: node.innerText); + + return success; + } + + @override + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement('success', namespace: _namespace); + if (body != null) element.children.add(xml.XmlText(body!).copy()); + + return element; + } + + @override + String get name => _success; } From 121988f3f02d0dc32ac808a5e6187d320decfff9 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:05:29 +0400 Subject: [PATCH 55/81] refactor: Remove commented-out code in _database.dart --- lib/src/plugins/command/_database.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/plugins/command/_database.dart b/lib/src/plugins/command/_database.dart index 84bc14a..c4fc27d 100644 --- a/lib/src/plugins/command/_database.dart +++ b/lib/src/plugins/command/_database.dart @@ -1,16 +1,16 @@ -part of 'command.dart'; +// part of 'command.dart'; -class _HiveDatabase { - factory _HiveDatabase() => _instance; +// class _HiveDatabase { +// factory _HiveDatabase() => _instance; - _HiveDatabase._(); +// _HiveDatabase._(); - late Box> box; +// late Box> box; - static final _HiveDatabase _instance = _HiveDatabase._(); +// static final _HiveDatabase _instance = _HiveDatabase._(); - Future initialize(String name, [String? path]) async => - box = await Hive.openBox>(name, path: path); +// Future initialize(String name, [String? path]) async => +// box = await Hive.openBox>(name, path: path); - Map? getSession(String sessionID) => box.get(sessionID); -} +// Map? getSession(String sessionID) => box.get(sessionID); +// } From 9c9a83164ed661bd19837a61b2c96a9129f0cb2a Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:06 +0400 Subject: [PATCH 56/81] refactor: Improve code organization and functionality for XMPP classes --- lib/src/plugins/form/field.dart | 396 +++++++++++---------------- lib/src/plugins/form/form.dart | 462 ++++++++++++++------------------ 2 files changed, 360 insertions(+), 498 deletions(-) diff --git a/lib/src/plugins/form/field.dart b/lib/src/plugins/form/field.dart index 5c7aef4..cf387a7 100644 --- a/lib/src/plugins/form/field.dart +++ b/lib/src/plugins/form/field.dart @@ -1,267 +1,185 @@ -part of 'dataforms.dart'; - -class FormField extends XMLBase { - FormField({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'field', - namespace: WhixpUtils.getNamespace('FORMS'), - includeNamespace: false, - pluginAttribute: 'field', - pluginMultiAttribute: 'fields', - interfaces: { - 'answer', - 'desc', - 'required', - 'value', - 'label', - 'type', - 'var', - }, - subInterfaces: {'desc'}, - ) { - addGetters({ - const Symbol('answer'): (args, base) => answer, - const Symbol('options'): (args, base) => options, - const Symbol('required'): (args, base) => required, - }); - - addSetters( - { - const Symbol('type'): (value, args, base) => setType(value as String), - const Symbol('answer'): (value, args, base) => - setAnswer(value as String), - const Symbol('false'): (value, args, base) => setFalse(), - const Symbol('options'): (value, args, base) => - setOptions(value as List), - const Symbol('required'): (value, args, base) => - setRequired(value as bool), - const Symbol('true'): (value, args, base) => setTrue(), - const Symbol('value'): (value, args, base) => setValue(value), - }, - ); - - addDeleters({ - const Symbol('options'): (args, base) => deleteOptions(), - const Symbol('required'): (args, base) => removeRequired(), - const Symbol('value'): (args, base) => removeValue(), - }); - - registerPlugin(FieldOption(), iterable: true); - } - - String? _type; - final _optionTypes = {'list-multi', 'list-single'}; - final _trueValues = {true, 'true', '1'}; - final _multivalueTypes = { - 'hidden', - 'jid-multi', - 'list-multi', - 'text-multi', - }; - - @override - bool setup([xml.XmlElement? element]) { - final result = super.setup(element); - if (result) { - _type = null; - } else { - _type = this['type'] as String; - } - - return result; - } - - void setType(String value) { - setAttribute('type', value); - if (value.isNotEmpty) { - _type = value; - } +part of 'form.dart'; + +/// Represents a field that can be used in a form. +class Field { + /// Default constructor for creating a [Field] object. + Field({ + this.variable, + this.label, + this.description, + this.required = false, + this.type, + List? values, + List? options, + }) { + this.values = values ?? []; + this.options = options ?? []; } - List> get options { - final options = >[]; - - final optionsElement = - element!.findAllElements('option', namespace: namespace); - for (final option in optionsElement) { - final opt = FieldOption(element: option); - options.add({ - 'label': opt['label'] as String, - 'value': opt['value'] as String, - }); + /// The variable associated with the field. + final String? variable; + + /// The label of the field. + final String? label; + + /// The description of the field. + final String? description; + + /// Indicates whether the field is required or not. + final bool required; + + /// The type of the field. + final FieldType? type; + + /// A list of possible values for the field. + List values = []; + + /// A list of options for the field. + List options = []; + + /// Factory constructor to create a [Field] object from an XML element. + factory Field.fromXML(xml.XmlElement node) { + String? variable; + String? label; + FieldType? type; + String? description; + final values = []; + bool required = false; + final options = []; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'var': + variable = attribute.value; + case 'label': + label = attribute.value; + case 'type': + switch (attribute.value) { + case "boolean": + type = FieldType.boolean; + case "fixed": + type = FieldType.fixed; + case "hidden": + type = FieldType.hidden; + case "jid-multi": + type = FieldType.jidMulti; + case "jid-single": + type = FieldType.jidSingle; + case "list-multi": + type = FieldType.listMulti; + case "list-single": + type = FieldType.listSingle; + case "text-multi": + type = FieldType.textMulti; + case "text-private": + type = FieldType.textPrivate; + case "text-single": + type = FieldType.textSingle; + } + } } - return options; - } - void addOption({String label = '', String value = ''}) { - if (_type == null || _optionTypes.contains(_type)) { - final option = FieldOption(); - option['label'] = label; - option['value'] = value; - add(option); - } else { - Log.instance.warning('Cannot add options to ${this['type']} field.'); - } - } - - void setOptions(List options) { - for (final value in options) { - if (value is Map) { - addOption(value: value['value']!, label: value['label']!); - } else { - addOption(value: value as String); + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'desc': + description = child.innerText; + case 'value': + values.add(child.innerText); + case 'required': + required = true; + case 'option': + options.add(FieldOption.fromXML(child)); } } - } - void deleteOptions() { - final options = element!.findAllElements('option', namespace: namespace); - for (final option in options) { - element!.children.remove(option); - } + return Field( + variable: variable, + label: label, + description: description, + required: required, + type: type, + values: values, + options: options, + ); } - bool get required { - final requiredElement = - element!.getElement('required', namespace: namespace); - return requiredElement != null; - } + /// Converts the [Field] object to an XML element. + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = HashMap(); + + if (variable?.isNotEmpty ?? false) dictionary['var'] = variable!; + if (label?.isNotEmpty ?? false) dictionary['label'] = label!; + if (type != null) dictionary['type'] = _enum2String(type!); + + builder.element( + 'field', + attributes: dictionary, + nest: () { + if (description?.isNotEmpty ?? false) { + builder.element('desc', nest: () => builder.text(description!)); + } + if (values.isNotEmpty) { + for (final value in values) { + builder.element('value', nest: () => builder.text(value)); + } + } + if (required) builder.element('required'); + }, + ); - void setRequired(bool required) { - final exists = this['required'] as bool; - if (!exists && required) { - element!.children.add(WhixpUtils.xmlElement('required')); - } else if (exists && !required) { - delete('required'); - } - } + final root = builder.buildDocument().rootElement; - void removeRequired() { - final required = element!.getElement('required', namespace: namespace); - if (required != null) { - element!.children.remove(required); + if (options.isNotEmpty) { + for (final option in options) { + root.children.add(option.toXML().copy()); + } } - } - String get answer => this['value'] as String; + return root; + } +} - void setAnswer(String answer) => this['value'] = answer; +/// Represents an option for a field. +class FieldOption { + /// Creates a [FieldOption] with the provided label and value. + const FieldOption(this.label, this.value); - void setFalse() => this['value'] = false; + /// The label of the option. + final String? label; - void setTrue() => this['value'] = true; + /// The value of the option. + final String? value; - dynamic value({bool convert = true}) { - final values = element!.findAllElements('value', namespace: namespace); - if (values.isEmpty) { - return null; - } else if (_type == 'boolean') { - if (convert) { - return _trueValues.contains(values.first.innerText); - } - return values.first.innerText; - } else if (_multivalueTypes.contains(_type) || values.length > 1) { - dynamic vals = []; - for (final value in values) { - (vals as List).add(value.innerText); - } - if (_type == 'text-multi' && convert) { - vals = values.join('\n'); - } - return vals; - } else { - if (values.first.innerText.isEmpty) { - return ''; - } - return values.first.innerText; - } + /// Factory constructor to create a [FieldOption] from an XML element. + factory FieldOption.fromXML(xml.XmlElement node) { + final label = node.getAttribute('label'); + final value = node.getElement('value')?.innerText; + return FieldOption(label, value); } - void setValue(dynamic value) { - delete('value'); + /// Converts the [FieldOption] object to an XML element. + xml.XmlElement toXML() { + final dictionary = HashMap(); - if (_type == 'boolean') { - if (_trueValues.contains(value)) { - final valueElement = WhixpUtils.xmlElement('value'); - valueElement.innerText = '1'; - element!.children.add(valueElement); - } else { - final valueElement = WhixpUtils.xmlElement('value'); - valueElement.innerText = '0'; - element!.children.add(valueElement); - } - } else if (_multivalueTypes.contains(_type) || - (_type == null || _type!.isEmpty)) { - dynamic val = value; - if (val is bool) { - val = [val]; - } - if (val is! List) { - val = (val as String).replaceAll('\r', ''); - val = val.split('\n'); - } - for (final value in val as List) { - String val = value; - if ((_type == null || _type!.isEmpty) && _trueValues.contains(value)) { - val = '1'; - } - final valueElement = WhixpUtils.xmlElement('value'); - valueElement.innerText = val; - element!.children.add(valueElement); - } - } else { - if (value is List) { - Log.instance - .warning('Cannot add multiple values to a ${this['type']} field.'); - } - element!.children - .add(WhixpUtils.xmlElement('value')..innerText = value as String); + if (label != null) dictionary['label'] = label!; + final element = WhixpUtils.xmlElement('option', attributes: dictionary); + if (value != null) { + element.children + .add(xml.XmlElement(xml.XmlName('value'), [], [xml.XmlText(value!)])); } - } - void removeValue() { - final value = element!.findAllElements('value', namespace: namespace); - if (value.isNotEmpty) { - for (final val in value) { - element!.children.remove(val); - } - } + return element; } @override - FormField copy({xml.XmlElement? element, XMLBase? parent}) => FormField( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class FieldOption extends XMLBase { - FieldOption({super.element, super.parent}) - : super( - name: 'option', - namespace: WhixpUtils.getNamespace('FORMS'), - includeNamespace: false, - pluginAttribute: 'option', - pluginMultiAttribute: 'options', - interfaces: {'label', 'value'}, - subInterfaces: {'value'}, - ); + bool operator ==(Object other) => + identical(this, other) || + other is FieldOption && + runtimeType == other.runtimeType && + label == other.label && + value == other.value; @override - FieldOption copy({xml.XmlElement? element, XMLBase? parent}) => FieldOption( - element: element, - parent: parent, - ); + int get hashCode => label.hashCode ^ value.hashCode; } diff --git a/lib/src/plugins/form/form.dart b/lib/src/plugins/form/form.dart index 30cebce..d3772d3 100644 --- a/lib/src/plugins/form/form.dart +++ b/lib/src/plugins/form/form.dart @@ -1,291 +1,235 @@ -part of 'dataforms.dart'; - -class Form extends XMLBase { - Form({ - String? title, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'x', - namespace: WhixpUtils.getNamespace('FORMS'), - pluginAttribute: 'form', - pluginMultiAttribute: 'forms', - interfaces: { - 'instructions', - 'reported', - 'title', - 'type', - 'items', - 'values', - }, - subInterfaces: {'title'}, - ) { - _title = title; - if (_title != null) { - this['title'] = _title; - } - addGetters({ - const Symbol('instructions'): (args, base) => instructions, - const Symbol('fields'): (args, base) => fields, - }); - - addSetters( - { - const Symbol('instructions'): (value, args, base) => - setInstructions(value), - }, - ); - - addDeleters({ - const Symbol('instructions'): (args, base) => deleteInstructions(), - const Symbol('fields'): (args, base) => deleteFields(), - }); - - registerPlugin(FormField(), iterable: true); - } - - String? _title; - - /// Whenever there is a need to send form without any additional data, this - /// static variable can be used. - static Form defaultConfig = Form(); - - @override - bool setup([xml.XmlElement? element]) { - final result = super.setup(element); - if (result) { - this['type'] = 'form'; - } - - return result; - } +import 'dart:collection'; + +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +part 'field.dart'; + +const String _namespace = 'jabber:x:data'; +const String _name = 'x'; + +/// * `form` The form-processing entity is asking the form-submitting entity to +/// complete a form. +/// * `submit` The form-submitting entity is submitting data to the +/// form-processing entity. The submission MAY include fields that were not +/// provided in the empty form, but the form-processing entity MUST ignore any +/// fields that it does not understand. Furthermore, the submission MAY omit +/// fields not marked with by the form-processing entity. +/// * `cancel` The form-submitting entity has cancelled submission of data to +/// the form-processing entity. +/// * `result` The form-processing entity is returning data (e.g., search +/// results) to the form-submitting entity, or the data is a generic data set. +enum FormType { form, submit, cancel, result } + +/// * `boolean` The field enables an entity to gather or provide an either-or +/// choice between two options. The default value is "false". +/// * `fixed` The field is intended for data description (e.g., human-readable +/// text such as "section" headers) rather than data gathering or provision. The +/// child SHOULD NOT contain newlines (the \n and \r characters); +/// instead an application SHOULD generate multiple fixed fields, each with one child. +/// * `hidden` The field is not shown to the form-submitting entity, but +/// instead is returned with the form. The form-submitting entity SHOULD NOT +/// modify the value of a hidden field, but MAY do so if such behavior is +/// defined for the "using protocol". +/// * `jid-multi` The field enables an entity to gather or provide multiple +/// Jabber IDs. Each provided JID SHOULD be unique (as determined by comparison +/// that includes application of the Nodeprep, Nameprep, and Resourceprep +/// profiles of Stringprep as specified in XMPP Core), and duplicate JIDs MUST +/// be ignored. +/// * `jid-single` The field enables an entity to gather or provide a single +/// Jabber ID. +/// * `list-multi` The field enables an entity to gather or provide one or +/// more options from among many. A form-submitting entity chooses one or more +/// items from among the options presented by the form-processing entity and +/// MUST NOT insert new options. The form-submitting entity MUST NOT modify the +/// order of items as received from the form-processing entity, since the order +/// of items MAY be significant. +/// * `list-single` The field enables an entity to gather or provide one option +/// from among many. A form-submitting entity chooses one item from among the +/// options presented by the form-processing entity and MUST NOT insert new +/// options. +/// * `text-multi` The field enables an entity to gather or provide multiple +/// lines of text. +/// * `text-private` The field enables an entity to gather or provide a single +/// line or word of text, which shall be obscured in an interface (e.g., with +/// multiple instances of the asterisk character). +/// * `text-single` The field enables an entity to gather or provide a single +/// line or word of text, which may be shown in an interface. This field type is +/// the default and MUST be assumed if a form-submitting entity receives a field +/// type it does not understand. +enum FieldType { + boolean, + fixed, + hidden, + jidMulti, + jidSingle, + listMulti, + listSingle, + textMulti, + textPrivate, + textSingle, +} - void addItem(Map values) { - final itemElement = WhixpUtils.xmlElement('item'); - element!.children.add(itemElement); - final reportedVariables = reported.keys; - for (final variable in reportedVariables) { - final field = FormField(); - field._type = reported[variable]?['type'] as String; - field['var'] = variable; - field['value'] = values[variable]; - itemElement.children.add(field.element!); - } +/// Converts a [FieldType] enum value to a string representation. +String _enum2String(FieldType type) { + switch (type) { + case FieldType.jidMulti: + return 'jid-multi'; + case FieldType.jidSingle: + return 'jid-single'; + case FieldType.listMulti: + return 'list-multi'; + case FieldType.listSingle: + return 'list-single'; + case FieldType.textMulti: + return 'text-multi'; + case FieldType.textPrivate: + return 'text-private'; + case FieldType.textSingle: + return 'text-single'; + default: + return type.name; } +} - void setType(String formType) { - setAttribute('type', formType); - if (formType == 'submit') { - for (final value in fields.entries) { - final field = fields[value.key]!; - if (field['type'] != 'hidden') { - field.delete('type'); +/// Represents a form. +class Form extends MessageStanza { + /// Default constructor for creating a [Form] object. + Form({ + this.instructions, + this.title, + this.type = FormType.form, + this.reported, + List? fields, + }) { + this.fields = fields ?? []; + } + + /// Instructions for filling out the form. + final String? instructions; + + /// Title of the form. + final String? title; + + /// Type of the form. + FormType type; + + /// The reported field of the form. + final Field? reported; + + /// List of fields in the form. + List fields = []; + + /// Factory constructor to create a [Form] object from an XML element. + factory Form.fromXML(xml.XmlElement node) { + late FormType type; + String? instructions; + String? title; + final fields = []; + Field? reported; + + for (final attribute in node.attributes) { + if (attribute.localName == 'type') { + switch (attribute.value) { + case 'result': + type = FormType.result; + case 'submit': + type = FormType.submit; + case 'cancel': + type = FormType.cancel; + default: + type = FormType.form; } - field - ..delete('label') - ..delete('desc') - ..delete('required') - ..deleteOptions(); } - } else if (formType == 'cancel') { - deleteFields(); } - } - void setValues(Map values) { - final fields = this.fields; - for (final field in values.entries) { - if (!this.fields.containsKey(field.key)) { - fields[field.key] = addField(variable: field.key); + for (final child in node.children.whereType()) { + if (child.localName == 'instructions') { + instructions = child.innerText; } - this.fields[field.key]?['value'] = values[field.key]; - } - } - - Map get fields { - final fields = {}; - for (final stanza in this['substanzas'] as List) { - if (stanza is FormField) { - fields[stanza['var'] as String] = stanza; + if (child.localName == 'title') { + title = child.innerText; } - } - - return fields; - } - - FormField addField({ - String variable = '', - String? formType, - String? label, - String? description, - bool? required = false, - dynamic value, - List>? options, - }) { - final field = FormField(); - field['var'] = variable; - field['type'] = formType; - field['value'] = value; - if ({'form', 'result'}.contains(this['type'])) { - field['label'] = label; - field['desc'] = description; - field['required'] = required; - if (options != null) { - for (final option in options) { - field.addOption( - label: option['label'] ?? '', - value: option['value'] ?? '', - ); + if (child.localName == 'field') { + fields.add(Field.fromXML(child)); + } + if (child.localName == 'reported') { + for (final node in child.children.whereType()) { + reported = Field.fromXML(node); } } - } else { - if (field['type'] != 'hidden') { - field.delete('type'); + if (child.localName == 'item') { + for (final node in child.children.whereType()) { + fields.add(Field.fromXML(node)); + } } } - add(field); - return field; - } - void setFields(Map> fields) { - delete('fields'); - for (final field in fields.entries) { - addField( - variable: field.key, - label: field.value['label'] as String?, - description: field.value['desc'] as String?, - required: field.value['required'] as bool?, - value: field.value['value'], - options: field.value['options'] as List>?, - formType: field.value['type'] as String?, - ); - } + return Form( + type: type, + instructions: instructions, + title: title, + reported: reported, + fields: fields, + ); } - void deleteFields() { - final fields = element!.findAllElements('field', namespace: namespace); + /// Sets the specified [value] to the given [variable]. + void setFieldValue(String variable, dynamic value) { for (final field in fields) { - element!.children.remove(field); + if (field.variable == variable) { + field.values = [value.toString()]; + } } } - String get instructions { - final instructionsElement = - element!.getElement('instructions', namespace: namespace); - if (instructionsElement == null) return ''; - return instructionsElement.children - .map((element) => element.innerText) - .join('\n'); - } - - void setInstructions(dynamic instructions) { - delete('instructions'); + /// Converts the [Form] object to an XML element. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = HashMap(); + dictionary['xmlns'] = _namespace; + dictionary['type'] = type.name; + + builder.element( + _name, + attributes: dictionary, + nest: () { + if (instructions?.isNotEmpty ?? false) { + builder.element( + 'instructions', + nest: () => builder.text(instructions!), + ); + } + }, + ); - List temp = []; - if (instructions is String? && - (instructions == null || instructions.isEmpty)) { - return; + final root = builder.buildDocument().rootElement; + for (final field in fields) { + root.children.add(field.toXML().copy()); } - if (instructions is! List) { - temp = (instructions as String).split('\n'); + if (reported != null) { + final element = WhixpUtils.xmlElement('reported'); + element.children.add(reported!.toXML().copy()); + root.children.add(element); } - for (final instruction in temp) { - element!.children - .add(WhixpUtils.xmlElement('instructions')..innerText = instruction); - } + return root; } - void deleteInstructions() { - final instructions = element!.findAllElements('instructions'); - for (final instruction in instructions) { - element!.children.remove(instruction); - } - } + /// Adds a list of fields to the form. + void addFields(List fields) => this.fields.addAll(fields); - Map getValues() { - final values = {}; - final fields = this.fields; - for (final field in fields.entries) { - values[field.key] = field.value; - } - return values; - } - - Map get reported { - final fields = {}; - final fieldElement = - element!.findAllElements('reported', namespace: namespace); - final reporteds = []; - - for (final element in fieldElement) { - final elements = element.findAllElements( - 'field', - namespace: FormField().namespace, - ); - for (final element in elements) { - reporteds.add(element); - } - } - - for (final reported in reporteds) { - final field = FormField(element: reported); - fields[field['var'] as String] = field; - } - - return fields; - } - - FormField addReported( - String variable, { - String? formType, - String label = '', - String description = '', - }) { - xml.XmlElement? reported = - element!.getElement('reoprted', namespace: namespace); - if (reported == null) { - reported = WhixpUtils.xmlElement('reported'); - element!.children.add(reported); - } - final fieldElement = WhixpUtils.xmlElement('field'); - reported.children.add(fieldElement); - final field = FormField(element: fieldElement); - field['var'] = variable; - field['type'] = formType; - field['label'] = label; - field['desc'] = description; - return field; - } + /// Clears all fields from the form. + void clearFields() => fields.clear(); - void setReported(Map> reported) { - for (final variable in reported.entries) { - addReported( - variable.key, - formType: variable.value['type'] as String?, - description: (variable.value['desc'] as String?) ?? '', - label: (variable.value['label'] as String?) ?? '', - ); - } - } + @override + String get name => 'dataforms'; @override - Form copy({xml.XmlElement? element, XMLBase? parent}) => Form( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - title: _title, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); + String get tag => formsTag; } From 54af5a607076ffb88c9ab90cbdc063e834794e44 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:11 +0400 Subject: [PATCH 57/81] refactor: Improve code organization and functionality for XMPP classes --- lib/src/stanza/presence.dart | 265 ++++++++++++++++------------------- 1 file changed, 120 insertions(+), 145 deletions(-) diff --git a/lib/src/stanza/presence.dart b/lib/src/stanza/presence.dart index a9bb1af..5499830 100644 --- a/lib/src/stanza/presence.dart +++ b/lib/src/stanza/presence.dart @@ -1,7 +1,7 @@ -import 'package:whixp/src/plugins/delay/delay.dart'; +import 'package:whixp/src/exception.dart'; import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/root.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; @@ -26,160 +26,135 @@ import 'package:xml/xml.dart' as xml; /// 1 /// /// ``` -class Presence extends RootStanza { - Presence({ - /// [showtypes] may be one of: dnd, chat, xa, away. - this.showtypes = const {'dnd', 'chat', 'xa', 'away'}, - super.transport, - super.stanzaType, - super.stanzaTo, - super.stanzaFrom, - super.receive = false, - super.includeNamespace = false, - super.getters, - super.setters, - super.deleters, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginMultiAttribute, - super.pluginIterables, - super.overrides, - super.isExtension, - super.boolInterfaces, - super.element, - super.parent, - }) : super( - name: 'presence', - namespace: WhixpUtils.getNamespace('CLIENT'), - pluginAttribute: 'presence', - interfaces: { - 'type', - 'to', - 'from', - 'id', - 'show', - 'status', - 'priority', - }, - subInterfaces: {'show', 'status', 'priority'}, - languageInterfaces: {'status'}, - types: { - 'available', - 'unavailable', - 'error', - 'probe', - 'subscribe', - 'subscribed', - 'unsubscribe', - 'unsubscribed', - }, - ) { - if (!receive && this['id'] == '') { - if (transport != null) { - this['id'] = WhixpUtils.getUniqueId(); +class Presence extends Stanza with Attributes { + static const String _name = "presence"; + + /// Constructs a presence stanza. + Presence({this.show, this.status, this.nick, this.priority, this.error}); + + /// The current presence show information. + final String? show; + + /// The status message associated with the presence. + final String? status; + + /// The nick associated with the presence. + final String? nick; + + /// The priority of the presence. + final int? priority; + + /// Error stanza associated with this presence stanza, if any. + final ErrorStanza? error; + + /// List of payloads associated with this presence stanza. + final payloads = []; + + /// Constructs a presence stanza from a string representation. + /// + /// Throws [WhixpInternalException] if the input XML is invalid. + factory Presence.fromString(String stanza) { + try { + final doc = xml.XmlDocument.parse(stanza); + final root = doc.rootElement; + + return Presence.fromXML(root); + } catch (_) { + throw WhixpInternalException.invalidXML(); + } + } + + /// Constructs a presence stanza from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory Presence.fromXML(xml.XmlElement node) { + if (node.localName != _name) { + throw WhixpInternalException.invalidNode(node.localName, _name); + } + + String? show; + String? status; + int? priority; + String? nick; + ErrorStanza? error; + final payloads = []; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'show': + show = child.innerText; + case 'status': + status = child.innerText; + case 'priority': + priority = int.parse(child.innerText); + case 'nick': + nick = child.innerText; + case 'error': + error = ErrorStanza.fromXML(child); + default: + try { + final tag = WhixpUtils.generateNamespacedElement(child); + + payloads.add(Stanza.payloadFromXML(tag, child)); + } catch (ex) { + // XMPPLogger.warn(ex); + } + break; } } - addGetters({ - const Symbol('type'): (args, base) { - String out = base.getAttribute('type'); - if (out.isEmpty && showtypes.contains(base['show'])) { - out = this['show'] as String; + final presence = Presence( + show: show, + status: status, + nick: nick, + priority: priority, + error: error, + )..payloads.addAll(payloads); + presence.loadAttributes(node); + + return presence; + } + + /// Converts the presence stanza to its XML representation. + @override + xml.XmlElement toXML() { + final dict = attributeHash; + final builder = WhixpUtils.makeGenerator(); + + builder.element( + _name, + attributes: dict, + nest: () { + if (show?.isNotEmpty ?? false) { + builder.element('show', nest: () => builder.text(show!)); } - if (out.isEmpty) { - out = 'available'; + if (status?.isNotEmpty ?? false) { + builder.element('status', nest: () => builder.text(status!)); } - return out; - }, - const Symbol('priority'): (args, base) { - String presence = base.getSubText('priority') as String; - if (presence.isEmpty) { - presence = '0'; + if (nick?.isNotEmpty ?? false) { + builder.element('nick', nest: () => builder.text(nick!)); + } + if (priority != null && priority != 0) { + builder.element( + 'priority', + nest: () => builder.text(priority.toString()), + ); } - return presence; - }, - }); - - addSetters( - { - const Symbol('type'): (value, args, base) { - if (types.contains(value)) { - base['show'] = null; - if (value == 'available') { - value = ''; - } - base.setAttribute('type', value as String); - } else if (showtypes.contains(value)) { - base['show'] = value; - } - }, - const Symbol('priority'): (value, args, base) => base.setSubText(name), - const Symbol('show'): (value, args, base) { - final show = value as String?; - if (show == null || show.isEmpty) { - deleteSub('show'); - } else if (showtypes.contains(show)) { - setSubText('show', text: show); - } - }, - }, - ); - - addDeleters( - { - const Symbol('type'): (args, base) { - base.deleteAttribute('type'); - base.deleteSub('show'); - }, }, ); - /// Register all required stanzas beforehand, so we won't need to declare - /// them one by one whenever there is a need to specific stanza. - /// - /// If you have not used the specified stanza, then you have to enable the - /// stanza through the usage of `pluginAttribute` parameter. - registerPlugin(StanzaError()); - registerPlugin(DelayStanza()); - } + final root = builder.buildDocument().rootElement; - /// Creates a new reply [Presence] from the current stanza. - Presence replyPresence({bool clear = true}) { - final presence = super.reply(copiedStanza: copy(), clear: clear); + if (error != null) root.children.add(error!.toXML().copy()); - if (this['type'] == 'unsubscribe') { - presence['type'] = 'unsubscribed'; - } else if (this['type'] == 'subscribe') { - presence['type'] = 'subscribed'; - } - - return presence; + return root; } + /// Gets the payload of a specific type from the presence stanza. + PresenceStanza? get

() => + payloads.firstWhere((payload) => payload is P) as PresenceStanza?; + + /// Returns the name of the presence stanza. @override - Presence copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Presence( - transport: transport, - receive: receive, - includeNamespace: includeNamespace, - getters: getters, - setters: setters, - deleters: deleters, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginMultiAttribute: pluginMultiAttribute, - pluginIterables: pluginIterables, - overrides: overrides, - isExtension: isExtension, - boolInterfaces: boolInterfaces, - element: element, - parent: parent, - ); - - /// [showtypes] may be one of: dnd, chat, xa, away. - final Set showtypes; + String get name => _name; } From 68e1b9e346d381f450c8b15dff8616200be13ab3 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:19 +0400 Subject: [PATCH 58/81] refactor: Improve code organization and functionality for XMPP classes --- lib/src/stanza/message.dart | 375 ++++++++++++++++-------------------- 1 file changed, 166 insertions(+), 209 deletions(-) diff --git a/lib/src/stanza/message.dart b/lib/src/stanza/message.dart index aa69c93..2736537 100644 --- a/lib/src/stanza/message.dart +++ b/lib/src/stanza/message.dart @@ -1,9 +1,9 @@ -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/delay/delay.dart'; -import 'package:whixp/src/plugins/form/dataforms.dart'; -import 'package:whixp/src/plugins/pubsub/pubsub.dart'; -import 'package:whixp/src/stanza/root.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/log/log.dart'; +import 'package:whixp/src/stanza/error.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; @@ -30,235 +30,192 @@ enum MessageType { chat, error, groupchat, headline, normal } /// ### Example: /// ```xml /// -/// salam! +/// hohoho! /// /// ``` -/// -/// For more information on "id" and "type" please refer to [XML stanzas](https://xmpp.org/rfcs/rfc3920.html#stanzas) -class Message extends RootStanza { - /// [types] may be one of: normal, chat, headline, groupchat, or error. - /// - /// All parameters are extended from [RootStanza]. For more information please - /// take a look at [RootStanza]. +class Message extends Stanza with Attributes { + static const String _name = 'message'; + + /// Constructs a message stanza. Message({ - super.stanzaTo, - super.stanzaFrom, - super.stanzaType, - super.transport, - super.includeNamespace = false, - super.getters, - super.setters, - super.deleters, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginMultiAttribute, - super.pluginIterables, - super.overrides, - super.isExtension, - super.boolInterfaces, - super.receive, - super.element, - super.parent, - }) : super( - name: 'message', - namespace: WhixpUtils.getNamespace('CLIENT'), - pluginAttribute: 'message', - interfaces: { - 'type', - 'to', - 'from', - 'id', - 'body', - 'subject', - 'thread', - 'parent_thread', - 'mucroom', - 'mucnick', - }, - subInterfaces: {'body', 'subject', 'thread'}, - languageInterfaces: {'body', 'subject', 'thread'}, - types: {'normal', 'chat', 'headline', 'error', 'groupchat'}, - ) { - if (!receive && this['id'] == '') { - if (transport != null) { - this['id'] = WhixpUtils.getUniqueId(); - } - } + this.subject, + this.body, + this.thread, + this.nick, + this.error, + }); - addGetters({ - const Symbol('type'): (args, base) => base.getAttribute('type', 'normal'), - const Symbol('id'): (args, base) => base.getAttribute('id'), - const Symbol('origin-id'): (args, base) { - var element = base.element; - if (element != null) { - element = base.element! - .getElement('origin-id', namespace: 'urn:xmpp:sid:0'); - } - if (element != null) { - return element.getAttribute('id') ?? ''; - } - return ''; - }, - const Symbol('parent_thread'): (args, base) { - final element = base.element; - if (element != null) { - final thread = - base.element!.getElement('thread', namespace: namespace); - if (thread != null) { - return thread.getAttribute('parent') ?? ''; - } - } - return ''; - }, - }); + /// The subject of the message. + final String? subject; - addSetters( - { - const Symbol('id'): (value, args, base) => _setIDs(value, base), - const Symbol('origin-id'): (value, args, base) => _setIDs(value, base), - const Symbol('parent_thread'): (value, args, base) { - var element = base.element; - if (element != null) { - element = base.element!.getElement('thread', namespace: namespace); - } - if (value != null) { - if (element == null) { - final thread = - WhixpUtils.xmlElement('thread', namespace: namespace); - base.element!.children.add(thread); - } - } else { - if (element != null && element.getAttribute('parent') != null) { - element.removeAttribute('parent'); - } - } - }, - }, - ); + /// The body of the message. + final String? body; - addDeleters( - { - const Symbol('origin-id'): (args, base) { - var element = base.element; - if (element != null) { - element = base.element! - .getElement('origin-id', namespace: 'urn:xmpp:sid:0'); - } - if (element != null) { - base.element!.children.remove(element); - } - }, - const Symbol('parent_thread'): (args, base) { - var element = base.element; - if (element != null) { - element = - base.element!.getElement('origin-id', namespace: namespace); - } - if (element != null && element.getAttribute('parent') != null) { - element.removeAttribute('parent'); - } - }, - }, - ); + /// The thread ID associated with the message. + final String? thread; - /// Register all required stanzas beforehand, so we won't need to declare - /// them one by one whenever there is a need to specific stanza. - /// - /// If you have not used the specified stanza, then you have to enable the - /// stanza through the usage of `pluginAttribute` parameter. - registerPlugin(Form()); - registerPlugin(DelayStanza()); - registerPlugin(PubSubEvent()); - } + /// The nick associated with the message. + final String? nick; - /// Set the message type to "chat". - void chat() => this['type'] = 'chat'; + /// Error stanza associated with this message stanza, if any. + final ErrorStanza? error; - /// Set the message type to "normal". - void normal() => this['type'] = 'normal'; + /// List of payloads associated with this message stanza. + final _payloads = []; - /// Overrider of [reply] method for [Message] stanza class. Can take optional - /// [body] parameter which is assigned to the body of the [Message] stanza. + /// List of extension nodes associated with this message stanza. + final extensions = []; + + /// Constructs a message stanza from a string representation. /// - /// Sets proper "to" attribute if the message is a from a MUC. - Message replyMessage({String? body, bool clear = true}) { - final message = super.reply(copiedStanza: copy(), clear: clear); + /// Throws [WhixpInternalException] if the input XML is invalid. + factory Message.fromString(String stanza) { + try { + final doc = xml.XmlDocument.parse(stanza); + final root = doc.rootElement; + + return Message.fromXML(root); + } catch (_) { + throw WhixpInternalException.invalidXML(); + } + } - if (this['type'] == 'groupchat') { - message['to'] = JabberID(message['to'] as String).bare; + /// Constructs a message stanza from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory Message.fromXML(xml.XmlElement node) { + String? subject; + String? body; + String? thread; + String? nick; + ErrorStanza? error; + final payloads = []; + final extensions = []; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'subject': + subject = child.innerText; + case 'body': + body = child.innerText; + case 'thread': + thread = child.innerText; + case 'nick': + nick = child.innerText; + case 'error': + error = ErrorStanza.fromXML(child); + default: + try { + final tag = WhixpUtils.generateNamespacedElement(child); + final stanza = Stanza.payloadFromXML(tag, child); + + payloads.add(stanza); + } on WhixpException catch (exception) { + if (child.localName.isNotEmpty && child.attributes.isNotEmpty) { + final extension = MessageExtension(child.localName); + for (final attribute in child.attributes) { + extension.addAttribute(attribute.localName, attribute.value); + } + extensions.add(extension); + } else { + Log.instance.error(exception.message); + } + } + break; + } } + final message = Message( + subject: subject, + body: body, + thread: thread, + nick: nick, + error: error, + ); + message._payloads.addAll(payloads); + message.extensions.addAll(extensions); + message.loadAttributes(node); - message['thread'] = this['thread']; - message['parent_thread'] = this['parent_thread']; + return message; + } - message.delete('id'); + /// Converts the message stanza to its XML representation. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = attributeHash; + + builder.element( + _name, + attributes: attributes, + nest: () { + if (subject?.isNotEmpty ?? false) { + builder.element('subject', nest: () => builder.text(subject!)); + } + if (body?.isNotEmpty ?? false) { + builder.element('body', nest: () => builder.text(body!)); + } + if (nick?.isNotEmpty ?? false) { + builder.element('nick', nest: () => builder.text(nick!)); + } + if (thread?.isNotEmpty ?? false) { + builder.element('thread', nest: () => builder.text(thread!)); + } + }, + ); - if (transport != null) { - message['id'] = WhixpUtils.getUniqueId(); + final root = builder.buildDocument().rootElement; + + if (error != null) root.children.add(error!.toXML().copy()); + for (final payload in _payloads) { + root.children.add(payload.toXML().copy()); } - if (body != null) { - message['body'] = body; + for (final extension in extensions) { + root.children.add(extension.toXML().copy()); } - return message; + return root; } - /// Return the name of the MUC room where the message originated. - String get mucRoom { - if (this['type'] == 'groupchat') { - return JabberID(this['from'] as String).bare; - } - return ''; - } + /// Adds payload [Stanza] to the given [Message]. + void addPayload(Stanza payload) => _payloads.add(payload); - /// Return the nickname of the MUC user that sent the message. - String get mucNick { - if (this['type'] == 'groupchat') { - return JabberID(this['from'] as String).resource; - } - return ''; - } + /// Adds an extension to the given [Message]. + void addExtension(MessageExtension extension) => extensions.add(extension); + + /// Returns a list of payloads of a specific type associated with this message + /// stanza. + List get() => _payloads.whereType().toList(); + /// Returns the name of the message stanza. @override - Message copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Message( - pluginMultiAttribute: pluginMultiAttribute, - overrides: overrides, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - boolInterfaces: boolInterfaces, - pluginIterables: pluginIterables, - isExtension: isExtension, - includeNamespace: includeNamespace, - getters: getters, - setters: setters, - deleters: deleters, - receive: receive, - element: element, - parent: parent, - ); + String get name => _name; - void _setIDs(dynamic value, XMLBase base) { - if (value == null || value == '') { - return; - } + @override + bool operator ==(Object other) => + identical(this, other) || + other is Message && + runtimeType == other.runtimeType && + type == other.type && + subject == other.subject && + body == other.body && + thread == other.thread && + error == other.error && + _payloads == other._payloads && + extensions == other.extensions; - base.element!.setAttribute('id', value as String); + @override + int get hashCode => + subject.hashCode ^ + id.hashCode ^ + body.hashCode ^ + thread.hashCode ^ + error.hashCode ^ + _payloads.hashCode ^ + extensions.hashCode; +} - final sub = - base.element!.getElement('origin-id', namespace: 'urn:xmpp:sid:0'); - if (sub != null) { - sub.setAttribute('id', value); - } else { - final sub = - WhixpUtils.xmlElement('origin-id', namespace: 'urn:xmpp:sid:0'); - sub.setAttribute('id', value); - return base.element!.children.add(sub); - } - } +/// An extension for the message that can be added beside of the message stanza. +class MessageExtension extends Node { + MessageExtension(super.name); } From 2b5671d2ab0609ad9378725f4ab2d447c8ecc088 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:28 +0400 Subject: [PATCH 59/81] refactor: Update Delay class to use DateTimeProfile for timestamp parsing and formatting --- lib/src/plugins/delay/delay.dart | 70 +++++-------------------- lib/src/plugins/delay/stanza.dart | 85 ++++++++++++++++--------------- 2 files changed, 56 insertions(+), 99 deletions(-) diff --git a/lib/src/plugins/delay/delay.dart b/lib/src/plugins/delay/delay.dart index d852660..8b45aff 100644 --- a/lib/src/plugins/delay/delay.dart +++ b/lib/src/plugins/delay/delay.dart @@ -1,8 +1,8 @@ +import 'package:whixp/src/_static.dart'; import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/time/time.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/plugins/plugins.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; @@ -34,7 +34,7 @@ part 'stanza.dart'; /// ``` /// /// see -class Delay extends PluginBase { +class Delay { /// XMPP stanzas are sometimes withheld for delivery due to the receipent /// being offline, or are resent in order to establish recent history as is /// the case with MUCs. In any case, it is impoprtant to konw when the stanza @@ -57,63 +57,19 @@ class Delay extends PluginBase { /// stanza for this plugin will be registered beforehand. So, you will only /// need to capture the incoming Message or Presence stanzas and parse delayed /// message by type casting. - Delay() : super('delay', description: 'XEP-0203: Delayed Delivery'); + Delay() : super(); - late final DateTimeProfile? _time; + late final _time = const DateTimeProfile(); /// Gets timestamp from the provided [DelayStanza]. - DateTime? getStamp(DelayStanza stanza) { - if (_time == null) { - Log.instance.warning( - 'In order to parse timestamp, you need to register XMPP Date and Time Profiles plugin', - ); - return null; - } + DateTime? getStamp(DelayStanza stanza, {bool toLocal = false}) { + final timestamp = stanza.stamp; + if (timestamp?.isEmpty ?? true) return null; - final timestamp = stanza.getAttribute('stamp'); - if (timestamp.isEmpty) { - return null; - } - return _time.parse(timestamp); + if (toLocal) return _time.parse(timestamp!).toLocal(); + return _time.parse(timestamp!); } /// Sets the provided [value] to the stanza as timestamp. - /// - /// Provided [value] must be [DateTime] or [String]. - void setStamp(DelayStanza stanza, dynamic value) { - assert( - value is DateTime || value is String, - 'Provided value must be either DateTime or String', - ); - - if (_time == null) { - Log.instance.warning( - 'In order to parse timestamp, you need to register XMPP Date and Time Profiles plugin', - ); - return; - } - - late String timestamp; - - if (value is DateTime) { - timestamp = _time.format(value.toUtc()); - } else { - timestamp = value as String; - } - - stanza.setAttribute('stamp', timestamp); - } - - @override - void pluginInitialize() { - _time = base.getPluginInstance('time'); - } - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - /// Do not implement. - @override - void pluginEnd() {} + String? convertToStamp(DateTime date) => _time.format(date.toUtc()); } diff --git a/lib/src/plugins/delay/stanza.dart b/lib/src/plugins/delay/stanza.dart index aa07d30..a90ec69 100644 --- a/lib/src/plugins/delay/stanza.dart +++ b/lib/src/plugins/delay/stanza.dart @@ -1,52 +1,53 @@ part of 'delay.dart'; -class DelayStanza extends XMLBase { - DelayStanza({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'delay', - namespace: 'urn:xmpp:delay', - pluginAttribute: 'delay', - interfaces: {'from', 'stamp', 'text'}, - ) { - addGetters({ - const Symbol('from'): (args, base) => from, - const Symbol('text'): (args, base) => text, - }); - - addSetters({ - const Symbol('from'): (value, args, base) => setFrom(value as JabberID), - const Symbol('text'): (value, args, base) => text = value as String, - }); - } +class DelayStanza extends MessageStanza { + const DelayStanza(this.from, this.stamp, this.text); - JabberID? get from { - final jid = getAttribute('from'); - if (jid.isNotEmpty) { - return JabberID(jid); - } - return null; + final JabberID? from; + final String? stamp; + final String? text; + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; + + if (from != null) attributes['from'] = from.toString(); + if (stamp?.isNotEmpty ?? false) attributes['stamp'] = stamp!; + + builder.element( + name, + attributes: {'xmlns': 'urn:xmpp:delay'} + ..addAll(attributes), + nest: () { + if (text?.isNotEmpty ?? false) builder.text(text!); + }, + ); + + return builder.buildDocument().rootElement; } - void setFrom(JabberID jid) => setAttribute('from', jid.toString()); + factory DelayStanza.fromXML(xml.XmlElement node) { + JabberID? jid; + String? stamp; - String get text => element!.innerText; + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'from': + jid = JabberID(attribute.value); + case 'stamp': + stamp = attribute.value; + default: + break; + } + } - set text(String value) => element!.innerText = value; + return DelayStanza(jid, stamp, node.innerText); + } + + @override + String get name => 'delay'; @override - DelayStanza copy({xml.XmlElement? element, XMLBase? parent}) => DelayStanza( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); + String get tag => delayTag; } From ba9731be8516a4b831d36a3cfb17cec74c0b0e25 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:40 +0400 Subject: [PATCH 60/81] refactor: Add VCard4 class for representing vCard 4.0 in XMPP --- lib/src/plugins/pubsub/vcard4.dart | 233 +++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 lib/src/plugins/pubsub/vcard4.dart diff --git a/lib/src/plugins/pubsub/vcard4.dart b/lib/src/plugins/pubsub/vcard4.dart new file mode 100644 index 0000000..6e9ab1c --- /dev/null +++ b/lib/src/plugins/pubsub/vcard4.dart @@ -0,0 +1,233 @@ +part of 'pubsub.dart'; + +/// The `VCard4` class represents a vCard 4.0, which is a standard for +/// electronic business cards. +/// +/// This class extends [IQStanza] and includes properties for a person's full +/// name, surnames, usernames, photo, birthday, and email. +class VCard4 extends IQStanza { + /// Creates an instance of `VCard4`. + const VCard4({ + this.fullname, + this.uid, + this.surnames, + this.username, + this.photo, + this.binval, + this.bday, + this.email, + this.gender, + }); + + /// The full name of the person. + final String? fullname; + + /// The UID of the vCard. + final String? uid; + + /// A map containing the person's surnames, with keys for 'surname', 'given', + /// and 'additional'. + final Map? surnames; + + /// A list of usernames or nicknames for the person. + final List? username; + + /// A URI to the person's photo. + final String? photo; + + /// A BINVAL to the person's photo in base64. + final String? binval; + + /// The person's birthday. + final String? bday; + + /// The person's email address. + final String? email; + + /// Person's gender. + final String? gender; + + /// Creates a `VCard4` instance from an XML element. + /// + /// - [node]: An XML element representing a vCard 4.0. + factory VCard4.fromXML(xml.XmlElement node) { + String? fullname; + String? uid; + final surnames = {}; + final usernames = []; + String? photo; + String? binval; + String? bday; + String? email; + String? gender; + + // Iterate over the child elements of the node to extract vCard information + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'fn': + final text = child.getElement('text'); + if (text != null) { + fullname = text.innerText; + } + case 'uid': + final text = child.getElement('text'); + if (text != null) { + uid = text.innerText; + } + case 'n': + if (child.getElement('surname') != null) { + surnames['surname'] = child.getElement('surname')!.innerText; + } + if (child.getElement('given') != null) { + surnames['given'] = child.getElement('given')!.innerText; + } + if (child.getElement('additional') != null) { + surnames['additional'] = child.getElement('additional')!.innerText; + } + case 'nickname': + final text = child.getElement('text'); + if (text != null) { + usernames.add(text.innerText); + } + case 'photo': + final uri = child.getElement('uri'); + final bin = child.getElement('BINVAL'); + if (uri != null) { + photo = uri.innerText; + } + if (bin != null) { + binval = bin.innerText; + } + case 'bday': + final date = child.getElement('date'); + if (date != null) { + bday = date.innerText; + } + case 'email': + final element = child.getElement('text'); + if (element != null) { + email = element.innerText; + } + case 'gender': + final element = child.getElement('sex')?.getElement('text'); + if (element != null) { + gender = element.innerText; + } + } + } + + return VCard4( + fullname: fullname, + uid: uid, + surnames: surnames, + username: usernames, + photo: photo, + binval: binval, + bday: bday, + email: email, + gender: gender, + ); + } + + /// Converts the `VCard4` instance to an XML element. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element( + name, + attributes: {'xmlns': namespace}, + nest: () { + if (fullname?.isNotEmpty ?? false) { + builder.element( + 'fn', + nest: () => + builder.element('text', nest: () => builder.text(fullname!)), + ); + } + if (uid?.isNotEmpty ?? false) { + builder.element( + 'uid', + nest: () => builder.element('text', nest: () => builder.text(uid!)), + ); + } + if (surnames?.isNotEmpty ?? false) { + builder.element( + 'n', + nest: () { + for (final surname in surnames!.entries) { + builder.element( + surname.key, + nest: () => builder.text(surname.value), + ); + } + }, + ); + } + if (username?.isNotEmpty ?? false) { + for (final nick in username!) { + builder.element( + 'nickname', + nest: () => + builder.element('text', nest: () => builder.text(nick)), + ); + } + } + if (bday?.isNotEmpty ?? false) { + builder.element( + 'bday', + nest: () => + builder.element('date', nest: () => builder.text(bday!)), + ); + } + if (photo?.isNotEmpty ?? false) { + builder.element( + 'photo', + nest: () => + builder.element('uri', nest: () => builder.text(photo!)), + ); + } + if (binval?.isNotEmpty ?? false) { + builder.element( + 'photo', + nest: () => + builder.element('BINVAL', nest: () => builder.text(binval!)), + ); + } + if (email?.isNotEmpty ?? false) { + builder.element( + 'email', + nest: () => + builder.element('text', nest: () => builder.text(email!)), + ); + } + if (gender?.isNotEmpty ?? false) { + builder.element( + 'gender', + nest: () => builder.element( + 'sex', + nest: () => builder.element( + 'text', + nest: () => builder.text(gender!), + ), + ), + ); + } + }, + ); + + return builder.buildDocument().rootElement; + } + + /// The name of the XML element representing the vCard. + @override + String get name => 'vcard'; + + /// The XML namespace for the vCard 4.0. + @override + String get namespace => 'urn:ietf:params:xml:ns:vcard-4.0'; + + /// A tag used to identify the vCard element. + @override + String get tag => vCard4Tag; +} From a33cfb32eb0c7d9183cab652e190b6004d3f5a28 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:50 +0400 Subject: [PATCH 61/81] refactor: Add Tune and Mood classes for representing tune and mood stanzas in XMPP --- lib/src/plugins/pubsub/pep.dart | 220 ++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 lib/src/plugins/pubsub/pep.dart diff --git a/lib/src/plugins/pubsub/pep.dart b/lib/src/plugins/pubsub/pep.dart new file mode 100644 index 0000000..ea87603 --- /dev/null +++ b/lib/src/plugins/pubsub/pep.dart @@ -0,0 +1,220 @@ +part of 'pubsub.dart'; + +/// The Tune class represents a musical tune and extends the [MessageStanza] +/// class. +/// +/// It contains several properties related to the tune such as artist, length, +/// rating, source, title, track, and URI. The class also includes methods to +/// convert from and to XML. +/// +/// Information about tunes is provided by the user and propagated on the +/// network by the user's client. The information container for tune data is a +/// element that is qualified by the 'http://jabber.org/protocol/tune' +/// namespace. +/// +/// For more information refer to: +/// +/// ### Example: +/// ```xml +/// +/// Yes +/// 686 +/// 8 +/// Yessongs +/// Heart of the Sunrise +/// 3 +/// http://www.yesworld.com/lyrics/Fragile.html#9 +/// +/// ``` +class Tune extends MessageStanza { + const Tune({ + this.artist, + this.length, + this.rating, + this.source, + this.title, + this.track, + this.uri, + }); + + /// The name of the artist. + final String? artist; + + /// The length of the tune in seconds. + final int? length; + + /// The rating of the tune. + final int? rating; + + /// The source of the tune. + final String? source; + + /// The title of the tune. + final String? title; + + /// The track number. + final String? track; + + /// The URI of the tune. + final String? uri; + + /// Creates an instance of [Tune] from an XML element. This constructor parses + /// the XML element and sets the corresponding properties of the Tune object. + factory Tune.fromXML(xml.XmlElement node) { + String? artist; + int? length; + int? rating; + String? source; + String? title; + String? track; + String? uri; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'artist': + artist = child.innerText; + case 'length': + length = int.parse(child.innerText); + case 'rating': + rating = int.parse(child.innerText); + case 'source': + source = child.innerText; + case 'title': + title = child.innerText; + case 'track': + track = child.innerText; + case 'uri': + uri = child.innerText; + } + } + + return Tune( + artist: artist, + length: length, + rating: rating, + source: source, + title: title, + track: track, + uri: uri, + ); + } + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element( + name, + attributes: { + 'xmlns': 'http://jabber.org/protocol/tune', + }, + nest: () { + if (artist?.isNotEmpty ?? false) { + builder.element('artist', nest: () => builder.text(artist!)); + } + if (length != null) { + builder.element( + 'length', + nest: () => builder.text(length!.toString()), + ); + } + if (rating != null) { + builder.element( + 'rating', + nest: () => builder.text(rating!.toString()), + ); + } + if (source?.isNotEmpty ?? false) { + builder.element('source', nest: () => builder.text(source!)); + } + if (title?.isNotEmpty ?? false) { + builder.element('title', nest: () => builder.text(title!)); + } + if (track?.isNotEmpty ?? false) { + builder.element('track', nest: () => builder.text(track!)); + } + if (uri?.isNotEmpty ?? false) { + builder.element('uri', nest: () => builder.text(uri!)); + } + }, + ); + + return builder.buildDocument().rootElement; + } + + @override + String get name => 'tune'; + + @override + String get tag => tuneTag; +} + +/// Represents a mood and extends the [MessageStanza] class. It contains +/// properties related to the mood such as value and text. +/// +/// The class also includes methods to convert from and to XML. +/// +/// Information about user moods is provided by the user and propagated on the +/// network by the user's client. The information is structured via a +/// element that is qualified by the 'http://jabber.org/protocol/mood' +/// namespace. The mood itself is provided as the element name of a defined +/// child element of the element (e.g., ); one such child +/// element is REQUIRED. +/// +/// ### Example: +/// ```xml +/// +// +// Yay, the mood spec has been approved! +// +/// ``` +class Mood extends MessageStanza { + const Mood({this.value, this.text}); + + /// The value of the mood. + final String? value; + + /// Additional text describing the mood. + final String? text; + + /// Creates an instance of [Mood] from an XML element. This constructor parses + /// the XML element and sets the corresponding properties of the Mood object. + factory Mood.fromXML(xml.XmlElement node) { + String? text; + String? value; + + for (final child in node.children.whereType()) { + if (child.localName == 'text') { + text = child.innerText; + } else { + value = child.localName; + } + } + + return Mood(value: value, text: text); + } + + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element( + 'mood', + attributes: {'xmlns': 'http://jabber.org/protocol/mood'}, + nest: () { + if (value?.isNotEmpty ?? false) builder.element(value!); + if (text?.isNotEmpty ?? false) { + builder.element('text', nest: () => builder.text(text!)); + } + }, + ); + + return builder.buildDocument().rootElement; + } + + @override + String get name => 'mood'; + + @override + String get tag => moodTag; +} From a1e9877d8bdf8827fa3a7a5c31793cadf6cb3846 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:06:56 +0400 Subject: [PATCH 62/81] refactor: Remove unused code in DateTimeProfile class --- lib/src/plugins/time/time.dart | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/lib/src/plugins/time/time.dart b/lib/src/plugins/time/time.dart index b92f46c..85a154b 100644 --- a/lib/src/plugins/time/time.dart +++ b/lib/src/plugins/time/time.dart @@ -1,13 +1,7 @@ -import 'package:meta/meta.dart'; - -import 'package:whixp/src/plugins/base.dart'; - part '_methods.dart'; -class DateTimeProfile extends PluginBase { - DateTimeProfile() - : super('time', description: 'XEP-0082: XMPP Date and Time Profiles'); - +class DateTimeProfile { + const DateTimeProfile(); /// Converts a [String] into a [DateTime] object. /// /// In the context of ISO 8601 date and time representaiton, the "Z" at the @@ -46,16 +40,4 @@ class DateTimeProfile extends PluginBase { /// object through the [asString] boolean. dynamic date({int? year, int? month, int? day, bool asString = true}) => _date(year: year, month: month, day: day, asString: asString); - - /// Do not implement. - @override - void pluginEnd() {} - - /// Do not implement. - @override - void pluginInitialize() {} - - /// Do not implement. - @override - void sessionBind(String? jid) {} } From 24c7b1f43e4a3941b241c4acb6aea05175ebff9f Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:07:18 +0400 Subject: [PATCH 63/81] refactor: Remove unused code in Mechanism class --- lib/src/sasl/mechanism.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/sasl/mechanism.dart b/lib/src/sasl/mechanism.dart index 9fa5a8f..3310dff 100644 --- a/lib/src/sasl/mechanism.dart +++ b/lib/src/sasl/mechanism.dart @@ -1,6 +1,5 @@ part of 'sasl.dart'; -@internal abstract class Mechanism { Mechanism( this._base, { From b847b86be25865118eff0952f23f7453dde1890c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:07:28 +0400 Subject: [PATCH 64/81] refactor: Improve code organization and functionality for XMPP classes --- lib/src/stanza/error.dart | 367 +++++++++++++++----------------------- 1 file changed, 142 insertions(+), 225 deletions(-) diff --git a/lib/src/stanza/error.dart b/lib/src/stanza/error.dart index 5101552..bea16f3 100644 --- a/lib/src/stanza/error.dart +++ b/lib/src/stanza/error.dart @@ -1,258 +1,175 @@ -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; -/// Represents an XMPP stanza error. -/// -/// Extends [XMLBase] and implements [Exception] interface. -/// -/// This class is designed to handle XMPP stanza errors, specifically related -/// to client communication. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// Some error text. -/// -/// -/// ``` -class StanzaError extends XMLBase implements Exception { - /// Creates a new instance of [StanzaError] with optional parameters. - /// - /// [conditionNamespace] represents the XML namespace for conditions. - StanzaError({ - super.getters, - super.setters, - super.deleters, - String? conditionNamespace, - super.element, - super.parent, - }) : super( - name: 'error', - namespace: WhixpUtils.getNamespace('CLIENT'), - pluginAttribute: 'error', - interfaces: { - 'code', - 'condition', - 'text', - 'type', - 'gone', - 'redirect', - 'by', - }, - subInterfaces: {'text'}, - ) { - _conditionNamespace = - conditionNamespace ?? WhixpUtils.getNamespace('STANZAS'); - - if (parent != null) { - parent!['type'] = 'error'; - } +/// Represents an error stanza in XMPP. +class ErrorStanza extends Stanza { + /// Holds the name of the error stanza. + static const String _name = 'error'; - addGetters( - { - const Symbol('condition'): (args, base) => condition, + ErrorStanza(); - /// Retrieves the contents of the element. - const Symbol('text'): (args, base) => base.getSubText('text'), - const Symbol('gone'): (args, base) => base.getSubText('gone'), - }, - ); + /// Returns the error code of the stanza. + int? code; - addSetters( - { - const Symbol('condition'): (value, args, base) { - if (_conditions.contains(value as String)) { - base.delete('condition'); - base.element!.children.add(WhixpUtils.xmlElement(value)); - } - }, - }, - ); + /// Returns the type of the error. The type is an attribute of the stanza that + /// identifies the category of the error. + String? type; - addDeleters( - { - /// Removes the condition element. - const Symbol('condition'): (args, base) { - final elements = []; - for (final child in base.element!.childElements) { - if (child.getAttribute('xmlns') == _conditionNamespace) { - final condition = child.localName; - if (_conditions.contains(condition)) { - elements.add(child); - } - } - } + /// Returns the reason for the error. Attribute of the stanza that provides a + /// human-readable explanation of the error. + String? reason; - for (final element in elements) { - base.element!.children.remove(element); - } - }, + /// Attribute of the stanza that provides additional information about the + /// error. + String? text; + + /// Creates an instance of the stanza from a string. + factory ErrorStanza.fromString(String stanza) { + try { + final root = xml.XmlDocument.parse(stanza); + return ErrorStanza.fromXML(root.rootElement); + } catch (_) { + throw WhixpInternalException.invalidXML(); + } + } + + /// Overrides of the `toXML` method that returns an XML representation of the + /// stanza. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final dictionary = {}; + + if (type != null) dictionary['type'] = type!; + if (code != null && code != 0) dictionary['code'] = code!.toString(); + dictionary.addAll({ + 'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas', + }); + + builder.element( + _name, + attributes: dictionary, + nest: () { + if (reason?.isNotEmpty ?? false) { + builder.element('reason', nest: () => builder.text(reason!)); + } + if (text?.isNotEmpty ?? false) { + builder.element('text', nest: () => builder.text(text!)); + } }, ); - } - /// The namespace for the condition element. - late final String _conditionNamespace; + return builder.buildDocument().rootElement; + } - @override - bool setup([xml.XmlElement? element]) { - final setup = super.setup(element); - if (setup) { - this['type'] = 'cancel'; - this['condition'] = 'feature-not-implemented'; + /// Creates an instance of the stanza from an XML node. + factory ErrorStanza.fromXML(xml.XmlElement node) { + if (node.localName != _name) { + throw WhixpInternalException.invalidNode(node.localName, _name); } - if (parent != null) { - parent!['type'] = 'error'; + + final error = ErrorStanza(); + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case "code": + final innerText = attribute.value; + if (innerText.isNotEmpty) { + error.code = innerText.isNotEmpty ? int.parse(innerText) : 0; + } + case "type": + final innerText = attribute.value; + error.type = innerText.isNotEmpty ? innerText : null; + default: + break; + } } - return setup; - } - /// Returns the condition element's name. - String get condition { - for (final child in element!.childElements) { - if (child.getAttribute('xmlns') == _conditionNamespace) { - final condition = child.localName; - if (_conditions.contains(condition)) { - return condition; - } + for (final child in node.children.whereType()) { + if (child.localName == "text") { + error.text = child.innerText; + } else { + error.reason = child.localName; } } - return ''; + return error; } @override - StanzaError copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - StanzaError( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - conditionNamespace: _conditionNamespace, - ); + String get name => _name; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ErrorStanza && + runtimeType == other.runtimeType && + code == other.code && + type == other.type && + reason == other.reason && + text == other.text; + + @override + int get hashCode => + code.hashCode ^ type.hashCode ^ reason.hashCode ^ text.hashCode; } -/// Represents an XMPP stream error. -/// -/// This class is designed to handle stream errors. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// XML was not well-formed. -/// -/// -/// ``` -class StreamError extends StanzaBase implements Exception { - /// XMPP stanzas of type `error` should inclue an ____ stanza that - /// describes the nature of the error and how it should be handled. - /// - /// The __stream:error__ stanza is used to provide more information for - /// error that occur with underlying XML stream itself, and not a particular - /// stanza. - /// - /// [conditionNamespace] represents the XML namespace for conditions. - StreamError({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - String? conditionNamespace, - }) : super( - name: 'error', - namespace: WhixpUtils.getNamespace('JABBER_STREAM'), - pluginAttribute: 'error', - interfaces: {'condition', 'text', 'see_other_host'}, - ) { - _conditionNamespace = - conditionNamespace ?? WhixpUtils.getNamespace('STREAM'); - - addGetters( - { - const Symbol('see_other_host'): (args, base) { - final namespace = _conditionNamespace; - - return base.getSubText('{$namespace}see-other-host'); - }, - }, - ); +/// Represents an error in the XMPP stream. +class StreamError with Packet { + static const String _name = 'error'; + static const String _namespace = 'http://etherx.jabber.org/streams'; - addSetters( - { - const Symbol('see_other_host'): (value, args, base) { - if (value is String && value.isNotEmpty) { - base.delete('condition'); + StreamError(); - final namespace = _conditionNamespace; + /// The specific error node associated with the stream error. + Node? error; - return base.getSubText('{$namespace}see-other-host'); - } - }, - }, - ); + /// If the error is `see-other-host`. + bool seeOtherHost = false; - addDeleters( - { - const Symbol('see_other_host'): (args, base) { - final namespace = _conditionNamespace; + /// The error text message, if available. + String? text; - return base.getSubText('{$namespace}see-other-host'); - }, - }, - ); + /// Constructs a [StreamError] instance from the given XML [node]. + factory StreamError.fromXML(xml.XmlElement node) { + final error = StreamError(); + + for (final child in node.children.whereType()) { + if (child.localName == 'see-other-host') { + error.seeOtherHost = true; + error.text = child.innerText; + } + if (child.localName == 'text' && + child.namespaceUri == 'urn:ietf:params:xml:ns:xmpp-streams') { + error.text = child.innerText; + } else { + error.error = Node.fromXML(child); + } + } + + return error; } - /// The namespace for the condition element. - late final String _conditionNamespace; + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element(_name, attributes: {'xmlns': _namespace}); + if (text?.isNotEmpty ?? false) { + builder.element('text', nest: () => builder.text(text!)); + } + + final root = builder.buildDocument().rootElement; + + if (error != null) root.children.add(error!.toXML().copy()); + + return root; + } @override - StreamError copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - StreamError( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - conditionNamespace: _conditionNamespace, - ); + String get name => 'stream:error'; } - -const _conditions = { - 'bad-request', - 'conflict', - 'feature-not-implemented', - 'forbidden', - 'gone', - 'internal-server-error', - 'item-not-found', - 'jid-malformed', - 'not-acceptable', - 'not-allowed', - 'not-authorized', - 'payment-required', - 'recipient-unavailable', - 'redirect', - 'registration-required', - 'remote-server-not-found', - 'remote-server-timeout', - 'resource-constraint', - 'service-unavailable', - 'subscription-required', - 'undefined-condition', - 'unexpected-request', -}; From f83be82bb2a712ad7795981bce095fcc14bf49db Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:07:38 +0400 Subject: [PATCH 65/81] refactor: Update SASL class to use const WhixpUtils instance for utf16to8 conversion --- lib/src/sasl/sasl.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/sasl/sasl.dart b/lib/src/sasl/sasl.dart index a904568..0c695fa 100644 --- a/lib/src/sasl/sasl.dart +++ b/lib/src/sasl/sasl.dart @@ -1,7 +1,5 @@ import 'dart:math' as math; -import 'package:meta/meta.dart'; - import 'package:whixp/src/exception.dart'; import 'package:whixp/src/plugins/mechanisms/feature.dart'; import 'package:whixp/src/sasl/scram.dart'; @@ -173,7 +171,7 @@ class _SASLPlain extends Mechanism { auth = '$auth$username'; auth = '$auth\u0000'; auth = '$auth$password'; - return WhixpUtils.utf16to8(auth); + return const WhixpUtils().utf16to8(auth); } @override From 8ec51e39f38a2ee6bcc0b399dcf26cbe1c02e2d3 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:07:56 +0400 Subject: [PATCH 66/81] refactor: Update Node class to support XML node manipulation and conversion --- lib/src/stanza/node.dart | 124 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 lib/src/stanza/node.dart diff --git a/lib/src/stanza/node.dart b/lib/src/stanza/node.dart new file mode 100644 index 0000000..112c86b --- /dev/null +++ b/lib/src/stanza/node.dart @@ -0,0 +1,124 @@ +import 'dart:collection'; + +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; + +import 'package:xml/xml.dart' as xml; + +/// Represents an XML node. +class Node { + /// Constructs an empty XML node. + Node(this.name); + + /// Name of the XML node. + final String name; + + /// Contents of the XML node. + String? content; + + /// List of child nodes. + late final nodes = []; + + /// Associated stanzas with the created or parsed node. + late final stanzas = []; + + /// Attributes associated with the XML node. + late final attributes = HashMap(); + + /// Constructs an XML node from an XML element node. + factory Node.fromXML(xml.XmlElement node) { + final nod = Node(node.localName); + + for (final attribute in node.attributes) { + nod.attributes[attribute.localName] = attribute.value; + } + + nod.content = node.innerText; + + for (final child in node.children.whereType()) { + try { + final tag = WhixpUtils.generateNamespacedElement(child); + final stanza = Stanza.payloadFromXML(tag, child); + nod.stanzas.add(stanza); + } catch (_) { + nod.nodes.add(Node.fromXML(child)); + } + } + + return nod; + } + + /// Converts the XML node to its XML representation. + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement(xmlName, attributes: attributes); + + if (nodes.isNotEmpty) { + for (final node in nodes) { + element.children.add(node.toXML().copy()); + } + } + + for (final stanza in stanzas) { + element.children.add(stanza.toXML().copy()); + } + + return element; + } + + /// Puts the given [stanza] to the list of stanzas. + void addStanza(Stanza stanza) => stanzas.add(stanza); + + /// Add an attribute [value] with the given [name]. + void addAttribute(String name, [String? value]) { + if (value?.isEmpty ?? true) return; + attributes.putIfAbsent(name, () => value!); + } + + /// Gets stanzas with the given [S] type and the given [name]. + List get(String name) => + stanzas.whereType().where((stanza) => stanza.name == name).toList(); + + /// Returns the XML name of the node. + String get xmlName => name; + + @override + String toString() => toXML().toString(); +} + +/// Represents a collection of XML nodes. +class Nodes { + /// Constructs an empty XML nodes. + Nodes(); + + /// List of XML nodes. + late final List nodes = []; + + /// Constructs a collection of XML nodes from a list of nodes. + factory Nodes.fromXML(List nodes) { + final nods = Nodes(); + + /// Constructs a collection of XML nodes from a list of nodes. + nodes.whereType().map((element) { + if (element.nodeType == xml.XmlNodeType.ELEMENT) { + nods.nodes.add(Node.fromXML(element)); + } + }); + + return nods; + } + + /// Converts the collection of XML nodes to its XML representation. + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + builder.element( + 'xml', + nest: () => nodes.map((node) { + builder.element( + 'node', + nest: () => builder.text(node.toXML().toString()), + ); + }), + ); + return builder.buildDocument().rootElement; + } +} From 30f8b6834e8714ac8492696de184d6d6b5de6893 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:08:13 +0400 Subject: [PATCH 67/81] refactor: Update Push class to enable and disable push notifications --- lib/src/plugins/push/push.dart | 91 ++++++++---------------- lib/src/plugins/push/stanza.dart | 115 ++++++++++++++++++++++--------- 2 files changed, 113 insertions(+), 93 deletions(-) diff --git a/lib/src/plugins/push/push.dart b/lib/src/plugins/push/push.dart index f265086..a32f238 100644 --- a/lib/src/plugins/push/push.dart +++ b/lib/src/plugins/push/push.dart @@ -1,81 +1,50 @@ -import 'dart:async'; +import 'dart:async' as async; +import 'package:whixp/src/_static.dart'; import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/form/dataforms.dart'; -import 'package:whixp/src/stanza/error.dart'; +import 'package:whixp/src/plugins/form/form.dart'; import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/utils.dart'; import 'package:xml/xml.dart' as xml; part 'stanza.dart'; -class Push extends PluginBase { - Push() - : super( - 'push', - description: 'XEP-0357: Push Notifications', - dependencies: {'disco'}, - ); - - @override - void pluginInitialize() {} - - FutureOr enablePush( +// ignore: avoid_classes_with_only_static_members +/// Provides static methods to enable and disable push notifications for a +/// specific Jabber ID (JID) and node. +class Push { + const Push(); + + /// Enables push notifications for the specified [jid] and [node]. + /// + /// If [node] is not provided, then unique one will be created and will be + /// returned to the user. + static String enableNotifications( JabberID jid, { - required String node, - Form? config, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, + String? node, + Form? payload, }) { - final iq = base.makeIQSet(); - final enable = _EnablePush(); - enable['jid'] = jid.bare; - enable['node'] = node; - - if (config != null) enable.add(config); + final nod = node ?? WhixpUtils.generateUniqueID('push'); + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..payload = Enable(jid, nod, payload: payload); - iq.add(enable); + iq.send(); - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); + return nod; } - FutureOr disablePush( + /// Disables push notifications for the specified [jid] and optional [node]. + static async.FutureOr disableNotifications( JabberID jid, { String? node, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, }) { - final iq = base.makeIQSet(); - final disable = _DisablePush(); - disable['jid'] = jid.bare; - disable['node'] = node; + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..payload = Disable(jid, node: node); - iq.add(disable); - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); + return iq.send(); } - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - /// Do not implement. - @override - void pluginEnd() {} } diff --git a/lib/src/plugins/push/stanza.dart b/lib/src/plugins/push/stanza.dart index 912d199..d9704d9 100644 --- a/lib/src/plugins/push/stanza.dart +++ b/lib/src/plugins/push/stanza.dart @@ -1,44 +1,95 @@ part of 'push.dart'; -class _EnablePush extends XMLBase { - _EnablePush({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'enable', - namespace: 'urn:xmpp:push:0', - pluginAttribute: 'enable', - interfaces: {'node', 'jid'}, - ) { - registerPlugin(Form()); - } +/// Represents an IQ stanza used to enable push notifications for a specific +/// Jabber ID (JID) and node. +/// +/// ```dart +/// final xml = enableStanza.toXML(); +/// ``` +class Enable extends IQStanza { + const Enable(this.jid, this.node, {this.payload}); + + final JabberID jid; + final String node; + final Form? payload; @override - _EnablePush copy({xml.XmlElement? element, XMLBase? parent}) { - return _EnablePush( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginTagMapping, - element: element, - parent: parent, + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + + builder.element( + name, + attributes: { + 'xmlns': namespace, + 'jid': jid.toString(), + 'node': node, + }, ); + + final element = builder.buildDocument().rootElement; + if (payload != null) element.children.add(payload!.toXML()); + + return element; } + + @override + String get name => 'enable'; + + @override + String get namespace => 'urn:xmpp:push:0'; + + @override + String get tag => enableTag; } -class _DisablePush extends XMLBase { - _DisablePush({ - super.element, - super.parent, - }) : super( - name: 'disable', - namespace: 'urn:xmpp:push:0', - pluginAttribute: 'disable', - interfaces: {'node', 'jid'}, - ); +/// Represents an IQ stanza used to disable push notifications for a specific +/// Jabber ID (JID). +class Disable extends IQStanza { + const Disable(this.jid, {this.node}); + + final JabberID jid; + final String? node; @override - _DisablePush copy({xml.XmlElement? element, XMLBase? parent}) { - return _DisablePush(element: element, parent: parent); + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; + + attributes['jid'] = jid.toString(); + if (node?.isNotEmpty ?? false) attributes['node'] = node!; + + builder.element( + name, + attributes: {'xmlns': namespace}..addAll(attributes), + ); + + return builder.buildDocument().rootElement; + } + + factory Disable.fromXML(xml.XmlElement node) { + String? jid; + String? nod; + + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'jid': + jid = attribute.value; + case 'node': + nod = attribute.value; + default: + break; + } + } + + return Disable(JabberID(jid), node: nod); } + + @override + String get name => 'disable'; + + @override + String get namespace => 'urn:xmpp:push:0'; + + @override + String get tag => disableTag; } From c5480c514d11ed9aa76130ed5d11bdb629a25427 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:08:24 +0400 Subject: [PATCH 68/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/plugins/pubsub/pubsub.dart | 1131 +++++--------------- lib/src/plugins/pubsub/stanza.dart | 1606 +++++----------------------- 2 files changed, 513 insertions(+), 2224 deletions(-) diff --git a/lib/src/plugins/pubsub/pubsub.dart b/lib/src/plugins/pubsub/pubsub.dart index 160ea8d..a04fba6 100644 --- a/lib/src/plugins/pubsub/pubsub.dart +++ b/lib/src/plugins/pubsub/pubsub.dart @@ -1,28 +1,26 @@ -import 'dart:async'; +import 'dart:async' as async; +import 'package:whixp/src/_static.dart'; import 'package:whixp/src/handler/handler.dart'; import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stanza/atom.dart'; import 'package:whixp/src/stanza/error.dart'; import 'package:whixp/src/stanza/iq.dart'; import 'package:whixp/src/stanza/message.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/transport.dart'; import 'package:whixp/src/utils/utils.dart'; -import 'package:whixp/src/whixp.dart'; import 'package:xml/xml.dart' as xml; -part 'event.dart'; +part 'pep.dart'; part 'stanza.dart'; +part 'vcard4.dart'; -final _$namespace = WhixpUtils.getNamespace('PUBSUB'); -final _$event = '${WhixpUtils.getNamespace('PUBSUB')}#event'; -final _$owner = '${WhixpUtils.getNamespace('PUBSUB')}#owner'; +enum PubSubNodeType { leaf, collection } +// ignore: avoid_classes_with_only_static_members /// # PubSub /// /// [PubSub] is designed to facilitate the integration of XMPP's @@ -48,194 +46,30 @@ final _$owner = '${WhixpUtils.getNamespace('PUBSUB')}#owner'; /// More information about this service and the server implementation can be /// found in the following link: /// -class PubSub extends PluginBase { - /// ### Example: - /// ```dart - /// void main() { - /// final whixp = Whixp(); /// ...construct this instance - /// final pubsub = PubSub(); - /// whixp.registerPlugin(pubsub); - /// - /// whixp.connect(); - /// whixp.addEventHandler('sessionStart', (_) async { - /// whixp.getRoster(); - // whixp.sendPresence(); - - /// await pubsub.getNodeConfig(JabberID('vsevex@example.com')); - /// }); - /// } - /// ``` - PubSub() - : super( - 'pubsub', - description: 'XEP-0060: Publish-Subscribe', - dependencies: {'disco', 'forms', 'RSM'}, - ); - - RSM? _rsm; - ServiceDiscovery? _disco; - late final _nodeEvents = {}; - Iterator>? _iterator; - - @override - void pluginInitialize() { - _rsm = base.getPluginInstance('RSM'); - _disco = base.getPluginInstance('disco'); - _iterator = null; - - base.transport - ..registerHandler( - CallbackHandler( - 'PubSub IQ Publish', - (stanza) => _handleIQPubsub(stanza as IQ), - matcher: StanzaPathMatcher('iq/pubsub/publish'), - ), - ) - ..registerHandler( - CallbackHandler( - 'PubSub Items', - (stanza) => _handleEventItems(stanza as Message), - matcher: StanzaPathMatcher('message/pubsub_event/items'), - ), - ) - ..registerHandler( - CallbackHandler( - 'PubSub Subscription', - (stanza) => _handleEventSubscription(stanza as Message), - matcher: StanzaPathMatcher('message/pubsub_event/subscription'), - ), - ) - ..registerHandler( - CallbackHandler( - 'PubSub Configuration', - (stanza) => _handleEventConfiguration(stanza as Message), - matcher: StanzaPathMatcher('message/pubsub_event/configuration'), - ), - ) - ..registerHandler( - CallbackHandler( - 'PubSub Delete', - (stanza) => _handleEventDelete(stanza as Message), - matcher: StanzaPathMatcher('message/pubsub_event/delete'), - ), - ) - ..registerHandler( - CallbackHandler( - 'PubSub Purge', - (stanza) => _handleEventPurge(stanza as Message), - matcher: StanzaPathMatcher('message/pubsub_event/purge'), - ), +class PubSub { + /// An empty constructor, will not be used. + const PubSub(); + + static void initialize() => Transport.instance().registerHandler( + Handler( + 'PubSub Event items', + (packet) => _handleEventItems(packet as Message), + )..descendant('message/event/items'), ); - } - - void _handleIQPubsub(IQ iq) { - base.transport.emit('pubsubIQ', data: iq); - } - - void _handleEventItems(Message message) { - final event = message['pubsub_event'] as PubSubEvent; - final items = event['items'] as PubSubEventItems; - - final multi = items.iterables.length > 1; - final node = items['node'] as String; - final values = {}; - - if (multi) { - values.addAll(message.values); - values.remove('pubsub_event'); - } - for (final item in items.iterables) { - final name = _nodeEvents[node]; - String type = 'publish'; - if (item.name == 'retract') { - type = 'retract'; - } + static void _handleEventItems(Message message) { + final items = message.get().first.items; + if (items?.isEmpty ?? true) return; - if (multi) { - final condensed = Message(); - condensed.values = values; - final items = (condensed['pubsub_event'] as PubSubEvent)['items'] - as PubSubEventItems; - items['node'] = node; - items.add(item); - base.transport - .emit('pubsub${type.capitalize()}', data: message); - if (name != null && name.isNotEmpty) { - base.transport.emit('${name}_$type', data: condensed); - } - } else { - base.transport - .emit('pubsub${type.capitalize()}', data: message); - if (name != null && name.isNotEmpty) { - base.transport.emit('${name}_$type', data: message); - } + for (final item in items!.entries) { + final payload = item.value.last.payload; + if (payload != null) { + Transport.instance() + .emit(item.key, data: item.value.last.payload); } } } - void _handleEventSubscription(Message message) { - final node = ((message['pubsub_event'] as PubSubEvent)['subscription'] - as PubSubEventSubscription)['node'] as String; - final eventName = _nodeEvents[node]; - - base.transport.emit('pubsubSubscription', data: message); - if (eventName != null) { - base.transport.emit('${eventName}Subscription', data: message); - } - } - - void _handleEventConfiguration(Message message) { - final node = ((message['pubsub_event'] as PubSubEvent)['configuration'] - as PubSubEventConfiguration)['node'] as String; - final eventName = _nodeEvents[node]; - - base.transport.emit('pubsubConfiguration', data: message); - if (eventName != null) { - base.transport.emit('${eventName}Config', data: message); - } - } - - void _handleEventDelete(Message message) { - final node = ((message['pubsub_event'] as PubSubEvent)['delete'] - as PubSubEventDelete)['node'] as String; - final eventName = _nodeEvents[node]; - - base.transport.emit('pubsubDelete', data: message); - if (eventName != null) { - base.transport.emit('${eventName}Delete', data: message); - } - } - - void _handleEventPurge(Message message) { - final node = ((message['pubsub_event'] as PubSubEvent)['purge'] - as PubSubEventPurge)['node'] as String; - final eventName = _nodeEvents[node]; - - base.transport.emit('pubsubPurge', data: message); - if (eventName != null) { - base.transport.emit('${eventName}Purge', data: message); - } - } - - /// Maps [node] names to specified [eventName]. - /// - /// When a pubsub event is received for the given [node], raise the provided - /// event. - /// - /// ### Example: - /// ```dart - /// final pubsub = PubSub(); - /// - /// pubsub.mapNodeEvent('http://jabber.org/protocol/tune', 'userTune'); - /// ``` - /// - /// This code will produce the events 'userTunePublish' and 'userTuneRetract' - /// when the respective notifications are received from the node - /// 'http://jabber.org/protocol/tune', among other events. - void mapNodeEvent(String node, String eventName) => - _nodeEvents[node] = eventName; - /// Creates a new [node]. This method works in two different ways: /// /// 1. Creates a node with default configuration for the specified [nodeType]. @@ -279,49 +113,42 @@ class PubSub extends PluginBase { /// timed out. [timeout] defaults to `10` seconds. If there is not any result /// or error from the server after the given seconds, then client stops to /// wait for an answer. - FutureOr createNode( + static async.FutureOr createNode( JabberID jid, { String? node, Form? config, - String? nodeType, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + PubSubNodeType? nodeType, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub'] as PubSubStanza)['create'] as PubSubCreate)['node'] = node; + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeSet; + + final pubsub = + PubSubStanza(nodes: [Node('create')..addAttribute('node', node)]); if (config != null) { const formType = 'http://jabber.org/protocol/pubsub#node_config'; - if (config.fields.containsKey('FORM_TYPE')) { - final field = config.fields['FORM_TYPE']; - if (field != null) { - field['value'] = formType; - } - } else { - config.addField( + config.fields.add( + Field( variable: 'FORM_TYPE', - formType: 'hidden', - value: formType, - ); - } + type: FieldType.hidden, + values: [formType], + ), + ); if (nodeType != null) { - if (config.fields.containsKey('pubsub#node_type')) { - final field = config.fields['pubsub#node_type']; - if (field != null) { - field['value'] = nodeType; - } - } else { - config.addField(variable: 'pubsub#node_type', value: nodeType); - } + config.fields + .add(Field(variable: 'pubsub#node_type', values: [nodeType.name])); } - ((iq['pubsub'] as PubSubStanza)['configure'] as PubSubConfigure) - .add(config.element!.copy()); + pubsub.nodes.add(Node('configure')..addStanza(config)); } - return iq.sendIQ( + iq.payload = pubsub; + + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -329,67 +156,94 @@ class PubSub extends PluginBase { ); } - /// Adds a new item to a [node], or edits an existing item. - /// - /// [PubSub] client service supports the ability to publish items. Any entity - /// that is allowed to publish items to a node (publisher or owner) may do so - /// at any time by sending an IQ-set to the service containing a pubsub - /// element. - /// - /// When including a payload and you do not provide an [id], then the service - /// will generally create an [id] for you. - /// - /// Publish [options] may be specified, and how those options are processed is - /// left to the service, such as treating the options as preconditions that - /// the [node]'s settings must match. - /// - /// For more information related to this ability of the service, please refer - /// to: - /// - /// - /// For publishing [options]: - ///
- /// - /// [jid] should be provided as the JID of the pubsub service. - /// - /// The [payload] MUST be in the [XMLBase] or [xml.XmlElement] type or it will - /// throw an assertion exception. - /// - /// The rest of the parameters are related with the [IQ] stanza and each of - /// them are responsible what to do when the server send the response. The - /// server can send result stanza, error type stanza or the request can be - /// timed out. [timeout] defaults to `10` seconds. If there is not any result - /// or error from the server after the given seconds, then client stops to - /// wait for an answer. - FutureOr publish( + /// Deletes a pubsub [node]. + static async.FutureOr deleteNode( JabberID jid, String node, { - String? id, - Form? options, - JabberID? iqFrom, - dynamic payload, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - final publish = (iq['pubsub'] as PubSubStanza)['publish'] as PubSubPublish; - publish['node'] = node; - final item = publish['item'] as PubSubItem; - if (id != null) { - item['id'] = id; + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeGet; + final pubsub = PubSubStanza( + owner: true, + nodes: [Node('delete')..addAttribute('node', node)], + ); + + iq.payload = pubsub; + + return iq.send( + callback: callback, + failureCallback: failureCallback, + timeoutCallback: timeoutCallback, + timeout: timeout, + ); + } + + /// Retrieves the configuration for a [node], or the pubsub service's + /// default configuration for new nodes. + static async.FutureOr getNodeConfig( + JabberID jid, { + String? node, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, + int timeout = 5, + }) async { + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeGet; + final pubsub = PubSubStanza(owner: true); + if (node == null) { + pubsub.addNode(Node('default')); + } else { + pubsub.addNode(Node('configure')..addAttribute('node', node)); } - if (payload != null) { - assert( - payload is XMLBase || payload is xml.XmlElement, - 'The provided payload must be either XMLBase or XmlElement', - ); - item['payload'] = payload; + + iq.payload = pubsub; + + final result = await iq.send( + callback: callback, + failureCallback: failureCallback, + timeoutCallback: timeoutCallback, + timeout: timeout, + ); + + if (result.payload == null) return null; + final owner = result.payload! as PubSubStanza; + + return owner.configuration!.get('dataforms').first; + } + + /// Sets a [config] to the [node]. + static async.FutureOr setNodeConfig( + JabberID jid, + String node, + Form config, { + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, + int timeout = 5, + }) { + /// Check if the type of the form is `submit`, if not, try to set type to + /// [FormType.submit]. + if (config.type != FormType.submit) { + config.type = FormType.submit; } - (iq['pubsub'] as PubSubStanza)['publish_options'] = options; + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeSet + ..payload = PubSubStanza( + owner: true, + configuration: Node('configure') + ..addAttribute('node', node) + ..addStanza(config), + ); - return iq.sendIQ( + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -413,23 +267,24 @@ class PubSub extends PluginBase { /// /// For more information, see: /// - FutureOr subscribe( + static async.FutureOr subscribe( JabberID jid, String node, { Form? options, + JabberID? iqFrom, bool bare = true, String? subscribee, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { String? sub = subscribee; - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - final subscribe = - (iq['pubsub'] as PubSubStanza)['subscribe'] as PubSubSubscribe; - subscribe['node'] = node; + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeSet; + + final subscribe = Node('subscribe')..addAttribute('node', node); if (sub == null) { if (iqFrom != null) { @@ -440,19 +295,21 @@ class PubSub extends PluginBase { } } else { if (bare) { - sub = base.transport.boundJID.bare; + sub = Transport.instance().boundJID?.bare; } else { - sub = base.transport.boundJID.toString(); + sub = Transport.instance().boundJID?.toString(); } } } - subscribe['jid'] = JabberID(sub); + subscribe.addAttribute('jid', sub); + final pubsub = PubSubStanza(nodes: [subscribe]); if (options != null) { - ((iq['pubsub'] as PubSubStanza)['options'] as PubSubOptions).add(options); + pubsub.addNode(Node('options')..addStanza(options)); } + iq.payload = pubsub; - return iq.sendIQ( + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -487,44 +344,38 @@ class PubSub extends PluginBase { /// /// /// ``` - FutureOr unsubscribe( + static async.FutureOr unsubscribe( JabberID jid, String node, { bool bare = true, String? subID, String? subscribee, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { String? sub = subscribee; - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - final unsubscribe = - (iq['pubsub'] as PubSubStanza)['unsubscribe'] as PubSubUnsubscribe; - unsubscribe['node'] = node; + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeSet; + final unsubscribe = Node('unsubscribe')..addAttribute('node', node); if (sub == null) { - if (iqFrom != null) { - if (bare) { - sub = iqFrom.bare; - } else { - sub = iqFrom.toString(); - } + if (bare) { + sub = Transport.instance().boundJID?.bare; } else { - if (bare) { - sub = base.transport.boundJID.bare; - } else { - sub = base.transport.boundJID.toString(); - } + sub = Transport.instance().boundJID?.toString(); } } - unsubscribe['jid'] = JabberID(sub); - unsubscribe['subid'] = subID; + unsubscribe + ..addAttribute('jid', sub) + ..addAttribute('subid', subID); - return iq.sendIQ( + iq.payload = PubSubStanza(nodes: [unsubscribe]); + + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -532,99 +383,24 @@ class PubSub extends PluginBase { ); } - /// Requests the contents of a [node]'s items. - /// - /// The desired items can be specified, or a query for the last few pubslihed - /// items can be used. - /// - /// The service may use result set management (RSM) for nodes with many items, - /// so an [iterator] can be returned if needed. Defining [maxItems] will help - /// the service to bring items not greater than the provided [maxItems]. - /// Defaults to `null`. Means that the service will not use pagination. - /// - /// If [itemIDs] was provided, then the service will bring the results that - /// corresponds to the given ID. - /// - /// For more information, see: - /// - FutureOr getItems( + /// Retrieves the subscriptions associated with a given [node]. + static async.FutureOr getNodeSubscriptions( JabberID jid, String node, { - JabberID? iqFrom, - int? maxItems, - bool iterator = false, - Set? itemIDs, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) async { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - final items = (iq['pubsub'] as PubSubStanza)['items'] as PubSubItems; - items['node'] = node; - - if (itemIDs != null) { - for (final itemID in itemIDs) { - final item = PubSubItem(); - item['id'] = itemID; - items.add(item); - } - } - - if (iterator) { - if (_rsm == null) { - Log.instance.warning( - 'The IQ must be iterated, but Result Set Management plugin is not registered', - ); - } else { - _iterator ??= _rsm! - .iterate( - iq, - 'pubsub', - amount: maxItems ?? 10, - postCallback: callback, - ) - .iterator; - - if (_iterator != null) { - if (_iterator!.moveNext()) { - return await _iterator!.current; - } - } - } - - return null; - } else { - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - } - - /// Retrieves the content of an individual item. - FutureOr getItem( - JabberID jid, - String node, - String itemID, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - - final item = PubSubItem(); - item['id'] = itemID; - - final items = (iq['pubsub'] as PubSubStanza)['items'] as PubSubItems; - items['node'] = node; - items.add(item); + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeGet + ..payload = PubSubStanza( + owner: true, + nodes: [Node('subscriptions')..addAttribute('node', node)], + ); - return iq.sendIQ( + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -632,54 +408,24 @@ class PubSub extends PluginBase { ); } - /// Deletes a single item from a [node]. - /// - /// To delete an item from the node, the publisher sends a retract request as - /// shown in the following example: - /// - /// ```xml - /// - /// - /// - /// - /// - /// - /// - /// ``` - /// - /// If not error occurs, the service MUST delete the item. - /// - /// If there is a need to [notify] about item retraction, then [notify] - /// should equal to either `true` or `1`. - /// - /// The rest of the parameters are related with the [IQ] stanza and each of - /// them are responsible what to do when the server send the response. The - /// server can send result stanza, error type stanza or the request can be - /// timed out. [timeout] defaults to `10` seconds. If there is not any result - /// or error from the server after the given seconds, then client stops to - /// wait for an answer. - FutureOr retract( - JabberID jid, - String node, - String id, { - String? notify, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + /// Publishes the given [vcard] to the given [jid]. + static async.FutureOr publishVCard( + VCard4 vcard, { + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - - final retract = (iq['pubsub'] as PubSubStanza)['retract'] as PubSubRetract; - retract['node'] = node; - retract['notify'] = notify; - (retract['item'] as PubSubItem)['id'] = id; + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..payload = PubSubStanza( + publish: _Publish( + node: 'urn:xmpp:vcard4', + item: _Item(payload: vcard), + ), + ); - return iq.sendIQ( + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -687,38 +433,23 @@ class PubSub extends PluginBase { ); } - /// Removes all items from a [node]. - /// - /// If a service persists all published items, a node owner may want to purge - /// the node of all published items (thus removing all items from the - /// persistent store). - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// ``` - FutureOr purge( - JabberID jid, - String node, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + static async.FutureOr retractVCard( + String id, { + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - - ((iq['pubsub_owner'] as PubSubOwnerStanza)['purge'] - as PubSubOwnerPurge)['node'] = node; + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..payload = PubSubStanza( + retract: _Retract( + node: 'urn:xmpp:vcard4', + item: _Item(id: id), + ), + ); - return iq.sendIQ( + return iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, @@ -726,434 +457,70 @@ class PubSub extends PluginBase { ); } - /// Retrieves all subscriptions for all [node]s. + /// Retrieves vCard information of the given [jid]. /// - /// An entity may want to query the serveice to retrieve its subscriptions for - /// all nodes at the service. - /// - /// If the service returns a list of subscriptions, it MUST return all - /// subscriptions for all JIDs that match the bare JID ( - /// or ) portion of the 'from' attribute on the request. - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// ``` - FutureOr getSubscriptions( + /// Returns all time published [VCard4] items. For last item in the server, + /// use .last.payload getter. + static async.FutureOr> retrieveVCard( JabberID jid, { - String? node, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub'] as PubSubStanza)['subscriptions'] - as PubSubSubscriptions)['node'] = node; - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } + }) async { + final iq = IQ(generateID: true) + ..to = jid + ..type = iqTypeGet + ..payload = PubSubStanza( + nodes: [Node('items')..addAttribute('node', 'urn:xmpp:vcard4')], + ); - /// Retrieves its affiliations for all [node]s at the service. - /// - /// An entity may want to query the service to retrieve its affiliations for - /// all nodes at the service, or query a specific node for its affiliation - /// with that node. - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// ``` - /// - /// see: - FutureOr getAffiliations( - JabberID jid, { - String? node, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub'] as PubSubStanza)['affiliations'] - as PubSubAffiliations)['node'] = node; - return iq.sendIQ( + final result = await iq.send( callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, timeout: timeout, ); - } + if (result.payload == null) return []; - /// Requests the subscription options. - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// ``` - /// - /// see: - /// - FutureOr getSubscriptionOptions( - JabberID jid, { - String? node, - JabberID? userJID, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - final pubsub = iq['pubsub'] as PubSubStanza; - if (userJID == null) { - (pubsub['default'] as PubSubDefault)['node'] = node; - } else { - final options = pubsub['options'] as PubSubOptions; - options['node'] = node; - options['jid'] = userJID; - } + final pubsub = result.payload! as PubSubStanza; + final items = pubsub.items['urn:xmpp:vcard4']; + if (items?.isEmpty ?? true) return []; - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); + return items!; } - /// Sets the subscription options. - /// - /// ### Example: - /// ```xml - /// - /// - /// - /// - /// - /// http://jabber.org/protocol/pubsub#subscribe_options - /// - /// 1 - /// 0 - /// false - /// - /// chat - /// online - /// away - /// - /// - /// - /// - /// - /// ``` - /// see: - /// - FutureOr setSubscriptionOptions( - JabberID jid, - String node, - JabberID userJID, - Form options, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + /// Subscribes to the vCard updates of the given [jid]. + static async.FutureOr subscribeToVCardUpdates( + JabberID jid, { + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, - }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - final pubsub = iq['pubsub'] as PubSubStanza; - - final options = pubsub['options'] as PubSubOptions; - options['node'] = node; - options['jid'] = userJID; - options.add(options); - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Discovers the nodes provided by a PubSub service, using - /// [ServiceDiscovery]. - /// - /// For parameters explanation, please refer to [ServiceDiscovery]'s - /// [getItems] method. - Future getNodes({ - JabberID? jid, - String? node, - JabberID? iqFrom, - bool local = false, - bool iterator = false, - }) { - if (_disco != null) { - return _disco!.getItems( - jid: jid, - node: node, - iqFrom: iqFrom, - local: local, - iterator: iterator, + }) => + subscribe( + jid, + 'urn:xmpp:vcard4', + callback: callback, + failureCallback: failureCallback, + timeoutCallback: timeoutCallback, + timeout: timeout, ); - } else { - Log.instance - .warning("Nodes' discovery requires Service Discovery plugin"); - return Future.value(); - } - } - /// Retrieves the configuration for a [node], or the pubsub service's - /// default configuration for new nodes. - FutureOr getNodeConfig( + /// Unsubscribes the vCard updates from the given [jid]. + static async.FutureOr unsubscribeVCardUpdates( JabberID jid, { - String? node, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - if (node == null) { - (iq['pubsub_owner'] as PubSubOwnerStanza).enable('default'); - } else { - ((iq['pubsub_owner'] as PubSubOwnerStanza)['configure'] - as PubSubOwnerConfigure)['node'] = node; - } - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Sets a [config] to the [node]. - FutureOr setNodeConfig( - JabberID jid, - String node, - Form config, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - final configure = (iq['pubsub_owner'] as PubSubOwnerStanza)['configure'] - as PubSubOwnerConfigure; - configure['node'] = node; - configure.add(config.element!.copy()); - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Retrieves the subscriptions associated with a given [node]. - FutureOr getNodeSubscriptions( - JabberID jid, - String node, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub_owner'] as PubSubOwnerStanza)['subscriptions'] - as PubSubOwnerSubscriptions)['node'] = node; - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Retrieves the affiliations associated with a given [node]. - FutureOr getNodeAffiliations( - JabberID jid, - String node, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQGet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub_owner'] as PubSubOwnerStanza)['affiliations'] - as PubSubOwnerAffiliations)['node'] = node; - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Deletes a pubsub [node]. - FutureOr deleteNode( - JabberID jid, - String node, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, + async.FutureOr Function(IQ iq)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, int timeout = 5, - }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub_owner'] as PubSubOwnerStanza)['delete'] - as PubSubOwnerDelete)['node'] = node; - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Retrieves the ItemIDs hosted by a given node, using [ServiceDiscovery]. - Future getItemIDs( - JabberID jid, - String node, { - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - if (_disco != null) { - return _disco!.getItems( - jid: jid, - node: node, - iqFrom: iqFrom, + }) => + unsubscribe( + jid, + 'urn:xmpp:vcard4', callback: callback, failureCallback: failureCallback, timeoutCallback: timeoutCallback, timeout: timeout, ); - } else { - Log.instance - .warning("ItemIDs' discovery requires Service Discovery plugin"); - return Future.value(); - } - } - - FutureOr modifyAffiliations( - JabberID jid, - String node, { - Map? affiliations, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub_owner'] as PubSubOwnerStanza)['affiliations'] - as PubSubOwnerAffiliations)['node'] = node; - - affiliations ??= {}; - - for (final affiliation in affiliations.entries) { - final aff = PubSubOwnerAffiliation(); - aff['jid'] = affiliation.key; - aff['affiliation'] = affiliation.value; - ((iq['pubsub_owner'] as PubSubOwnerStanza)['affiliations'] - as PubSubOwnerAffiliations) - .add(aff); - } - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - FutureOr modifySubscriptions( - JabberID jid, - String node, { - Map? subscriptions, - JabberID? iqFrom, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 5, - }) { - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - ((iq['pubsub_owner'] as PubSubOwnerStanza)['subscriptions'] - as PubSubOwnerSubscriptions)['node'] = node; - - subscriptions = {}; - - for (final subscription in subscriptions.entries) { - final sub = PubSubOwnerSubscription(); - sub['jid'] = subscription.key; - sub['subscription'] = subscription.value; - ((iq['pubsub_owner'] as PubSubOwnerStanza)['subscriptions'] - as PubSubOwnerSubscriptions) - .add(sub); - } - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - @override - void pluginEnd() { - base.transport - ..removeHandler('Pubsub Items') - ..removeHandler('Pubsub Subscription') - ..removeHandler('Pubsub Configuration') - ..removeHandler('Pubsub Delete') - ..removeHandler('Pubsub Purge'); - } - - /// Do not implement. - @override - void sessionBind(String? jid) {} } diff --git a/lib/src/plugins/pubsub/stanza.dart b/lib/src/plugins/pubsub/stanza.dart index b997c7e..0944921 100644 --- a/lib/src/plugins/pubsub/stanza.dart +++ b/lib/src/plugins/pubsub/stanza.dart @@ -1,970 +1,192 @@ part of 'pubsub.dart'; -class PubSubStanza extends XMLBase { - PubSubStanza({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginMultiAttribute, - super.pluginIterables, - super.element, - super.parent, - }) : super( - name: 'pubsub', - namespace: _$namespace, - pluginAttribute: 'pubsub', - interfaces: {}, - ) { - registerPlugin(PubSubAffiliations()); - registerPlugin(PubSubSubscription(namespace: _$namespace)); - registerPlugin(PubSubSubscriptions()); - registerPlugin(PubSubItems()); - registerPlugin(PubSubCreate()); - registerPlugin(PubSubDefault()); - registerPlugin(PubSubPublish()); - registerPlugin(PubSubRetract()); - registerPlugin(PubSubUnsubscribe()); - registerPlugin(PubSubSubscribe()); - registerPlugin(PubSubConfigure()); - registerPlugin(PubSubOptions()); - registerPlugin(PubSubPublishOptions()); - registerPlugin(RSMStanza()); - } - - @override - PubSubStanza copy({xml.XmlElement? element, XMLBase? parent}) => PubSubStanza( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginMultiAttribute: pluginMultiAttribute, - pluginIterables: pluginIterables, - element: element, - parent: parent, - ); -} - -/// OWNER ---------------------------------------------------------------- - -/// The owner of a PubSub node or service is the user or entity that has -/// administrative privileges over that particular node. This ownership grants -/// certain rights, such as configuring the node, managing subscriptions, and -/// setting access controls. -/// -/// This stanza more specifically refers to the global generalization of the -/// Publish-Subscribe plugin stanza and other substanzas will be available -/// through this one. -class PubSubOwnerStanza extends XMLBase { - PubSubOwnerStanza({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'pubsub', - namespace: _$owner, - pluginAttribute: 'pubsub_owner', - interfaces: {}, - ) { - registerPlugin(PubSubOwnerDefaultConfig()); - registerPlugin(PubSubOwnerAffiliations()); - registerPlugin(PubSubOwnerConfigure()); - registerPlugin(PubSubOwnerDefault()); - registerPlugin(PubSubOwnerDelete()); - registerPlugin(PubSubOwnerPurge()); - registerPlugin(PubSubOwnerSubscriptions()); - } - - @override - PubSubOwnerStanza copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerStanza( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubOwnerDefaultConfig extends XMLBase { - PubSubOwnerDefaultConfig({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'default', - namespace: _$owner, - pluginAttribute: 'default', - includeNamespace: false, - interfaces: {'node', 'config'}, - ) { - addGetters({ - const Symbol('config'): (args, base) => _config, - }); - - addSetters({ - const Symbol('config'): (value, args, base) => _setConfig(value), - }); - - registerPlugin(Form()); - } - - Form get _config => this['form'] as Form; - - void _setConfig(dynamic value) { - delete('form'); - add(value); - } +final _namespace = WhixpUtils.getNamespace('PUBSUB'); +final _ownerNamespace = '$_namespace#owner'; +final _eventNamespace = '$_namespace#event'; - @override - PubSubOwnerDefaultConfig copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerDefaultConfig( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerAffiliations extends PubSubAffiliations { - PubSubOwnerAffiliations({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super(namespace: _$owner) { - registerPlugin(PubSubOwnerAffiliation(), iterable: true); - } - - @override - PubSubOwnerAffiliations copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerAffiliations( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubOwnerAffiliation extends PubSubAffiliation { - PubSubOwnerAffiliation({ - super.getters, - super.setters, - super.element, - super.parent, - }) : super(namespace: _$owner, interfaces: {'affiliation', 'jid'}); - - @override - PubSubOwnerAffiliation copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerAffiliation( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerConfigure extends PubSubConfigure { - PubSubOwnerConfigure({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.element, - super.parent, - }) : super(namespace: _$owner, interfaces: {'node'}) { - registerPlugin(Form()); - } - - @override - PubSubOwnerConfigure copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerConfigure( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerDefault extends PubSubOwnerConfigure { - PubSubOwnerDefault({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.element, - super.parent, +class PubSubStanza extends MessageStanza implements IQStanza { + PubSubStanza({ + this.publish, + this.retract, + this.owner = false, + List? nodes, + Map>? items, + this.configuration, }) { - registerPlugin(Form()); - } - - @override - PubSubOwnerDefault copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerDefault( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerDelete extends XMLBase { - PubSubOwnerDelete({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'delete', - namespace: _$owner, - includeNamespace: false, - pluginAttribute: 'delete', - interfaces: {'node'}, - ) { - addGetters({ - const Symbol('required'): (args, base) => _required, - }); - - addSetters({ - const Symbol('required'): (value, args, base) => _setRequired(value), - }); - - addDeleters({ - const Symbol('required'): (args, base) => _deleteRequired(), - }); - - registerPlugin(PubSubOwnerRedirect()); - } - - bool get _required { - final required = element!.getElement('required', namespace: namespace); - return required != null; - } - - void _setRequired(dynamic value) { - if ({true, 'True', 'true', '1'}.contains(value)) { - element!.children.add(WhixpUtils.xmlElement('required')); - } else if (this['required'] as bool) { - delete('required'); + this.nodes = nodes ?? []; + this.items = items ?? >{}; + } + + final _Publish? publish; + final _Retract? retract; + final Node? configuration; + final bool owner; + late Map> items; + late List nodes; + + factory PubSubStanza.fromXML(xml.XmlElement node) { + _Publish? publish; + _Retract? retract; + Node? configuration; + final nodes = []; + final items = >{}; + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'publish': + publish = _Publish.fromXML(child); + case 'retract': + retract = _Retract.fromXML(child); + case 'configuration': + configuration = Node.fromXML(child); + case 'default': + configuration = Node.fromXML(child); + case 'items': + for (final item in child.children.whereType()) { + if (child.getAttribute('node') != null) { + if (items[child.getAttribute('node')] == null) { + items[child.getAttribute('node')!] = <_Item>[]; + } + } + items[child.getAttribute('node') ?? '']?.add(_Item.fromXML(item)); + } + default: + nodes.add(Node.fromXML(child)); + } } - } - void _deleteRequired() { - final required = element!.getElement('required', namespace: namespace); - if (required != null) { - element!.children.remove(required); - } + return PubSubStanza( + publish: publish, + configuration: configuration, + items: items, + retract: retract, + nodes: nodes, + ); } @override - PubSubOwnerDelete copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerDelete( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerPurge extends XMLBase { - PubSubOwnerPurge({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'purge', - namespace: _$owner, - includeNamespace: false, - pluginAttribute: 'purge', - interfaces: {'node'}, - ) { - addGetters({ - const Symbol('required'): (args, base) => _required, - }); - - addSetters({ - const Symbol('required'): (value, args, base) => _setRequired(value), - }); - - addDeleters({ - const Symbol('required'): (args, base) => _deleteRequired(), - }); - } - - bool get _required { - final required = element!.getElement('required', namespace: namespace); - return required != null; - } - - void _setRequired(dynamic value) { - if ({true, 'True', 'true', '1'}.contains(value)) { - element!.children.add(WhixpUtils.xmlElement('required')); - } else if (this['required'] as bool) { - delete('required'); + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement( + name, + namespace: owner ? _ownerNamespace : _namespace, + ); + + if (publish != null) { + element.children.add(publish!.toXML().copy()); } - } - - void _deleteRequired() { - final required = element!.getElement('required', namespace: namespace); - if (required != null) { - element!.children.remove(required); + if (retract != null) { + element.children.add(retract!.toXML().copy()); } - } - - @override - PubSubOwnerPurge copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerPurge( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerRedirect extends XMLBase { - PubSubOwnerRedirect({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'redirect', - namespace: _$owner, - pluginAttribute: 'redirect', - interfaces: {'node', 'jid'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); - } - - JabberID? get _jid { - final jid = getAttribute('jid'); - if (jid.isEmpty) { - return null; + if (configuration != null) { + element.children.add(configuration!.toXML().copy()); } - return JabberID(jid); - } - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - - @override - PubSubOwnerRedirect copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerRedirect( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class PubSubOwnerSubscriptions extends PubSubSubscriptions { - PubSubOwnerSubscriptions({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super(namespace: _$owner) { - registerPlugin(PubSubOwnerSubscription(), iterable: true); - } - - @override - PubSubOwnerSubscriptions copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerSubscriptions( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class PubSubOwnerSubscription extends XMLBase { - PubSubOwnerSubscription({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'subscription', - namespace: _$owner, - pluginAttribute: 'subscription', - interfaces: {'jid', 'subscription'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); - - registerPlugin(PubSubSubscription(), iterable: true); - } - - JabberID? get _jid { - final jid = getAttribute('jid'); - if (jid.isEmpty) { - return null; + if (nodes.isNotEmpty) { + for (final node in nodes) { + element.children.add(node.toXML().copy()); + } } - return JabberID(jid); - } - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - @override - PubSubOwnerSubscription copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOwnerSubscription( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -/// END OWNER ------------------------------------------------------------ - -/// To manage permissions, the protocol defined herein uses a hierarchy of -/// `affiliations`, similiar to those introduced in MUC. -/// -/// All affiliations MUST be based on a bare JID ( or -/// ) instead of a full JID (). -/// -/// Support for the "owner" and "none" affiliations is REQUIRED. Support for -/// all other affiliations is RECOMMENDED. For each non-required affiliation -/// supported by an implementation, it SHOULD return a service discovery feature -/// of "name-affiliation" where "name" is the name of the affiliation, such as -/// "member", "outcast", or "publisher". -/// -/// see -class PubSubAffiliation extends XMLBase { - PubSubAffiliation({ - String? namespace, - super.interfaces = const {'node', 'affiliation', 'jid'}, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'affiliation', - includeNamespace: false, - pluginAttribute: 'affiliation', - ) { - this.namespace = namespace ?? _$namespace; - - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); + return element; } - JabberID? get _jid { - final jid = getAttribute('jid'); - if (jid.isEmpty) { - return null; - } - return JabberID(jid); - } - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - - @override - PubSubAffiliation copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubAffiliation( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class PubSubAffiliations extends XMLBase { - PubSubAffiliations({ - String? namespace, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'affiliations', - includeNamespace: false, - pluginAttribute: 'affiliations', - interfaces: {'node'}, - ) { - this.namespace = namespace ?? _$namespace; - - registerPlugin(PubSubAffiliation(), iterable: true); - } + /// Adds the passed [node] to the nodes list. + void addNode(Node node) => nodes.add(node); @override - PubSubAffiliations copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubAffiliations( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -/// Subscriptions to a node may exist in several states. -/// -/// None - The node MUST NOT send event notifications or payloads to the Entity. -/// -/// Pending - An entity has requested to subscribe to a node and the request has -/// not yet been approved by a node owner. The node MUST NOT send event -/// notifications or payloads to the entity while it is in this state. -/// -/// Unconfigured - An entity has subscribed but its subscription options have -/// not yet been configured. The node MAY send event notifications or payloads -/// to the entity while it is in this state. The service MAY timeout -/// unconfigured subscriptions. -/// -/// Subscribed - An entity is subscribed to a node. The node MUST send all event -/// notifications (and, if configured, payloads) to the entity while it is in -/// this state (subject to subscriber configuration and content filtering). -class PubSubSubscription extends XMLBase { - PubSubSubscription({ - super.namespace, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'subscription', - includeNamespace: false, - pluginAttribute: 'subscription', - interfaces: {'node', 'subscription', 'subid', 'jid'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); - - registerPlugin(PubSubSubscribeOptions()); - } - - JabberID get _jid => JabberID(getAttribute('jid')); - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); + String get name => 'pubsub'; @override - PubSubSubscription copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubSubscription( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class PubSubSubscriptions extends XMLBase { - PubSubSubscriptions({ - String? namespace, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.element, - super.parent, - }) : super( - name: 'subscriptions', - includeNamespace: false, - pluginAttribute: 'subscriptions', - interfaces: {'node'}, - ) { - this.namespace = namespace ?? _$namespace; - - registerPlugin(PubSubSubscription(), iterable: true); - } - - List get subscriptions { - if (iterables.isNotEmpty) { - return iterables - .map((iterable) => PubSubSubscription(element: iterable.element)) - .toList(); - } - return []; - } + String get namespace => _namespace; @override - PubSubSubscriptions copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubSubscriptions( - namespace: namespace, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - element: element, - parent: parent, - ); + String get tag => pubsubTag; } -/// If a service supports subscription options it MUST advertise that fact in -/// its response to a "disco#info" query by including a feature whose `var` -/// attribute is "pubsub#subscription-options". -/// -/// ### Example: -/// ```xml -/// -/// -/// ... -/// -/// ... -/// -/// -/// ``` -class PubSubSubscribeOptions extends XMLBase { - PubSubSubscribeOptions({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'subscribe-options', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'suboptions', - interfaces: {'required'}, - ) { - addGetters({ - const Symbol('required'): (args, base) => _required, - }); - - addSetters({ - const Symbol('required'): (value, args, base) => _setRequired(value), - }); +class PubSubEvent extends MessageStanza { + const PubSubEvent({this.payloads, this.items, this.retractItems}); - addDeleters({ - const Symbol('required'): (args, base) => _deleteRequired(), - }); - } + final List? payloads; + final Map>? items; + final Map>? retractItems; - bool get _required { - final required = element!.getElement('required', namespace: namespace); - return required != null; - } + factory PubSubEvent.fromXML(xml.XmlElement node) { + final payloads = []; + final items = >{}; + final retracts = >{}; - void _setRequired(dynamic value) { - if ({true, 'True', 'true', '1'}.contains(value)) { - element!.children.add(WhixpUtils.xmlElement('required')); - } else if (this['required'] as bool) { - delete('required'); + for (final child in node.children.whereType()) { + if (child.localName == 'delete') { + payloads.add(Node.fromXML(child)); + } + if (child.localName == 'items') { + final attributeNode = child.getAttribute('node') ?? ''; + final fromNode = items[attributeNode]; + if (fromNode?.isEmpty ?? true) { + items[attributeNode] = <_Item>[]; + } + for (final child in child.children.whereType()) { + if (child.localName == 'item') { + items[attributeNode]?.add(_Item.fromXML(child)); + } + if (child.localName == 'retract') { + final node = retracts[attributeNode] ?? <_Item>[]; + node.add(_Retract.fromXML(child)); + } + } + } } - } - void _deleteRequired() { - final required = element!.getElement('required', namespace: namespace); - if (required != null) { - element!.children.remove(required); - } + return PubSubEvent( + payloads: payloads, + items: items, + retractItems: retracts, + ); } @override - PubSubSubscribeOptions copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubSubscribeOptions( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -/// When a subscription request is successfully processed, the service MAY send -/// the last published item to the new subscriber. The message containing this -/// item SHOULD be stamped with extended information qualified by the -/// 'urn:xmpp:delay' namespace. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// Soliloquy -///

-/// To be, or not to be: that is the question: -/// Whether 'tis nobler in the mind to suffer -/// The slings and arrows of outrageous fortune, -/// Or to take arms against a sea of troubles, -/// And by opposing end them? -/// -/// -/// 2003-12-13T18:30:02Z -/// 2003-12-13T18:30:02Z -/// -/// -/// -/// -/// -/// -/// ``` -class PubSubItem extends XMLBase { - PubSubItem({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'item', - includeNamespace: false, - pluginAttribute: 'item', - interfaces: {'id', 'payload'}, - ) { - addGetters({ - const Symbol('payload'): (args, base) => payload, - }); + xml.XmlElement toXML() { + final element = WhixpUtils.xmlElement('event', namespace: _eventNamespace); - addSetters({ - const Symbol('payload'): (value, args, base) => _setPayload(value), - }); - - addDeleters({ - const Symbol('payload'): (args, base) => _deletePayload(), - }); - - registerPlugin(AtomEntry()); - } - - xml.XmlElement? get payload { - if (element!.childElements.isNotEmpty) { - return element!.childElements.first; - } - return null; - } - - void _setPayload(dynamic value) { - delete('payload'); - if (value is XMLBase) { - if (pluginTagMapping.containsKey(value.tag)) { - initPlugin(value.pluginAttribute, existingXML: value.element); + if (payloads?.isNotEmpty ?? false) { + for (final payload in payloads!) { + element.children.add(payload.toXML().copy()); } - element!.children.add(value.element!.copy()); - } else if (value is xml.XmlElement) { - element!.children.add(value.copy()); } - } - void _deletePayload() { - for (final child in element!.childElements) { - element!.children.remove(child); + if (items?.isNotEmpty ?? false) { + for (final item in items!.entries) { + final itemsElement = WhixpUtils.xmlElement( + 'items', + attributes: {'node': item.key}, + ); + for (final node in item.value) { + itemsElement.children.add(node.toXML().copy()); + } + element.children.add(itemsElement); + } } - } - - @override - PubSubItem copy({xml.XmlElement? element, XMLBase? parent}) => PubSubItem( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} -class PubSubItems extends XMLBase { - PubSubItems({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.setters, - super.element, - super.parent, - }) : super( - name: 'items', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'items', - interfaces: {'node', 'max_items'}, - ) { - addSetters({ - const Symbol('maxItems'): (value, args, base) => - _setMaxItems(value as int), - }); - - registerPlugin(PubSubItem(), iterable: true); - } - - Iterable get allItems { - final allItems = element!.findAllElements('item', namespace: namespace); - final iitems = []; - - for (final item in allItems) { - iitems.add(PubSubItem(element: item)); + if (retractItems?.isNotEmpty ?? false) { + for (final item in retractItems!.entries) { + final itemsElement = WhixpUtils.xmlElement( + 'items', + attributes: {'node': item.key}, + ); + for (final node in item.value) { + itemsElement.children.add(node.toXML().copy()); + } + element.children.add(itemsElement); + } } - return iitems; - } - - void _setMaxItems(int value) { - setAttribute('max_items', value.toString()); + return element; } @override - PubSubItems copy({xml.XmlElement? element, XMLBase? parent}) => PubSubItems( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - setters: setters, - element: element, - parent: parent, - ); -} - -/// An entity may want to create a new node. However, a service MAY disallow -/// creation of nodes based on the identity of the requesting entity, or MAY -/// disallow node creation altogether (e.g., reserving that privilege to a -/// service-wide administrator). -/// -/// There are two ways to create a node: -/// * Create a node with default configuration for the specified node type. -/// * Create and configure a node simultaneously. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// ``` -/// see -class PubSubCreate extends XMLBase { - PubSubCreate({super.element, super.parent}) - : super( - name: 'create', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'create', - interfaces: {'node'}, - ); + String get name => 'pubsubevent'; @override - PubSubCreate copy({xml.XmlElement? element, XMLBase? parent}) => PubSubCreate( - element: element, - parent: parent, - ); -} - -/// This stanza will be used to get default subscription options for a node, -/// the ntity MUST send an empty ____ element to the node, in -/// response, the node SHOLD return the default options. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// ``` -/// -/// To get default subscription configuration options for all (leaf) nodes at a -/// service, the entity MUST send an empty element but not specify -/// a node; in response, the service SHOULD return the default subscription -/// options. -/// -/// ```xml -/// -/// -/// -/// -/// -/// ``` -/// -/// see -class PubSubDefault extends XMLBase { - PubSubDefault({super.getters, super.element, super.parent}) - : super( - name: 'default', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'default', - interfaces: {'node', 'type'}, - ) { - addGetters({ - const Symbol('type'): (args, base) => _type, - }); - } - - String get _type { - final type = getAttribute('type'); - if (type.isEmpty) { - return 'leaf'; - } - return type; - } - - @override - PubSubDefault copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubDefault( - getters: getters, - element: element, - parent: parent, - ); + String get tag => pubsubEventTag; } /// This stanza helps to support the ability to publish items. Any entity that @@ -978,32 +200,41 @@ class PubSubDefault extends XMLBase { /// contain no ____ elements or one ____ element. /// /// see -class PubSubPublish extends XMLBase { - PubSubPublish({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.element, - super.parent, - }) : super( - name: 'publish', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'publish', - interfaces: {'node'}, - ) { - registerPlugin(PubSubItem(), iterable: true); +class _Publish { + const _Publish({this.node, this.item}); + + final String? node; + final _Item? item; + + factory _Publish.fromXML(xml.XmlElement node) { + String? nod; + _Item? item; + + for (final attribute in node.attributes) { + if (attribute.localName == 'node') { + nod = attribute.value; + } + } + + for (final child in node.children.whereType()) { + item = _Item.fromXML(child); + } + + return _Publish(node: nod, item: item); } - @override - PubSubPublish copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubPublish( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - element: element, - parent: parent, - ); + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; + if (node?.isNotEmpty ?? false) attributes['node'] = node!; + + builder.element('publish', attributes: attributes); + final element = builder.buildDocument().rootElement; + + if (item != null) element.children.add(item!.toXML().copy()); + + return element; + } } /// This retract stanzas will be send by the publisher to delete an item. The @@ -1023,429 +254,120 @@ class PubSubPublish extends XMLBase { /// /// /// ``` -class PubSubRetract extends XMLBase { - PubSubRetract({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'retract', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'retract', - interfaces: {'node', 'notify'}, - ) { - addGetters({ - const Symbol('notify'): (args, base) => _notify, - }); +class _Retract { + const _Retract({this.node, this.notify, this.item}); - addSetters({ - const Symbol('notify'): (value, args, base) => _setNotify(value), - }); + final String? node; + final String? notify; + final _Item? item; - registerPlugin(PubSubItem()); - } + factory _Retract.fromXML(xml.XmlElement node) { + String? nod; + String? notify; + _Item? item; - bool? get _notify { - final notify = getAttribute('notify'); - if ({'0', 'false'}.contains(notify)) { - return false; - } else if ({'1', 'true'}.contains(notify)) { - return true; + for (final attribute in node.attributes) { + if (attribute.localName == 'node') { + nod = attribute.value; + } + if (attribute.localName == 'notify') { + notify = attribute.value; + } } - return null; - } - void _setNotify(dynamic value) { - delete('notify'); - if (value == null) { - return; - } else if ({true, '1', 'true', 'True'}.contains(value)) { - setAttribute('notify', 'true'); - } else { - setAttribute('notify', 'false'); + for (final child in node.children.whereType()) { + item = _Item.fromXML(child); } - } - @override - PubSubRetract copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubRetract( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -/// Helps whenever the subscriber want to unsubscribe from a node. The subsriber -/// sends an IQ-set whose ____ child contains ____ -/// element that specifies the node and the subscribed [JabberID]. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// ``` -/// -/// see -class PubSubUnsubscribe extends XMLBase { - PubSubUnsubscribe({ - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'unsubscribe', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'unsubscribe', - interfaces: {'node', 'jid', 'subid'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); + return _Retract(node: nod, notify: notify, item: item); } - JabberID get _jid => JabberID(getAttribute('jid')); + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; + if (node?.isNotEmpty ?? false) attributes['node'] = node!; + if (notify?.isNotEmpty ?? false) attributes['notify'] = notify!; - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); + builder.element('retract', attributes: attributes); + final element = builder.buildDocument().rootElement; - @override - PubSubUnsubscribe copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubUnsubscribe( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -/// When an entity wishes to subscribe to a node, it sends a subscription -/// request to the pubsub service. The subscription request is an IQ-set where -/// the ____ element contains one and only one ____ -/// element. -/// -/// The ____ element SHOULD possess a `node` attribute specifying -/// the node to which the entity wishes to subscribe. The ____ -/// element MUST also possess a `jid` attribute specifying the exact XMPP -/// address to be used as the subscribed JID -- often a bare JID -/// ( or ) or full JID -/// (. -class PubSubSubscribe extends XMLBase { - PubSubSubscribe({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'subscribe', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'subscribe', - interfaces: {'node', 'jid'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - }); + if (item != null) { + element.children.add(item!.toXML().copy()); + } - registerPlugin(PubSubOptions()); + return element; } - - JabberID get _jid => JabberID(getAttribute('jid')); - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - - @override - PubSubSubscribe copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubSubscribe( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - setters: setters, - element: element, - parent: parent, - ); } -/// The node creation may requrie to configure it. This stanza will come to help -/// whenever the publisher needs to configure the node. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// ``` -class PubSubConfigure extends XMLBase { - PubSubConfigure({ - String? namespace, - super.interfaces = const {'node', 'type'}, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.element, - super.parent, - }) : super( - name: 'configure', - includeNamespace: false, - pluginAttribute: 'configure', - ) { - this.namespace = namespace ?? _$namespace; - addGetters({ - const Symbol('type'): (args, base) => _type, - }); - - registerPlugin(Form()); - } - - String get _type { - final type = getAttribute('type'); - if (type.isEmpty) { - return 'leaf'; +class _Item { + const _Item({this.id, this.publisher, this.payload, this.tune, this.mood}); + + final String? id; + final String? publisher; + final Stanza? payload; + final Tune? tune; + final Mood? mood; + + factory _Item.fromXML(xml.XmlElement node) { + String? id; + String? publisher; + Stanza? stanza; + Tune? tune; + Mood? mood; + + for (final attribute in node.attributes) { + if (attribute.localName == 'id') { + id = attribute.value; + } + if (attribute.localName == 'publisher') { + publisher = attribute.value; + } } - return type; - } - - @override - PubSubConfigure copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubConfigure( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - getters: getters, - element: element, - parent: parent, - ); -} - -/// This stanza is helpful when the subscriber want to request the subscription -/// options by including ____ element inside an IQ-get stanza. -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// ``` -/// -/// see -class PubSubOptions extends XMLBase { - PubSubOptions({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'options', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'options', - interfaces: {'jid', 'node', 'options'}, - ) { - addGetters({ - const Symbol('jid'): (args, base) => _jid, - const Symbol('options'): (args, base) => _options, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => _setJid(value as JabberID), - const Symbol('options'): (value, args, base) => _setOptions(value), - }); - - addDeleters({ - const Symbol('options'): (args, base) => _deleteOptions(), - }); - } - JabberID get _jid => JabberID(getAttribute('jid')); - - void _setJid(JabberID jid) => setAttribute('jid', jid.toString()); - - Form get _options { - final config = - element!.getElement('x', namespace: WhixpUtils.getNamespace('FORMS')); - return Form(element: config); - } - - void _setOptions(dynamic value) { - if (value is XMLBase) { - element!.children.add(value.element!); - } else if (value is xml.XmlElement) { - element!.children.add(value); + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'tune': + tune = Tune.fromXML(child); + case 'mood': + mood = Mood.fromXML(child); + default: + final tag = WhixpUtils.generateNamespacedElement(child); + stanza = Stanza.payloadFromXML(tag, child); + } } - } - void _deleteOptions() { - final config = - element!.getElement('x', namespace: WhixpUtils.getNamespace('FORMS')); - element!.children.remove(config); + return _Item( + id: id, + publisher: publisher, + payload: stanza, + tune: tune, + mood: mood, + ); } - @override - PubSubOptions copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubOptions( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; + if (id?.isNotEmpty ?? false) attributes['id'] = id!; + if (publisher?.isNotEmpty ?? false) attributes['publisher'] = publisher!; -/// A pubsub service MAY support the ability to specify options along with a -/// publish request. -/// -/// The ____ element MUST contain a data form (see XEP-0004). -/// -/// ### Example: -/// ```xml -/// -/// -/// -/// -/// -/// Soliloquy -/// -/// To be, or not to be: that is the question: -/// Whether 'tis nobler in the mind to suffer -/// The slings and arrows of outrageous fortune, -/// Or to take arms against a sea of troubles, -/// And by opposing end them? -/// -/// -/// tag:denmark.lit,2003:entry-32397 -/// 2003-12-13T18:30:02Z -/// 2003-12-13T18:30:02Z -/// -/// -/// -/// -/// -/// -/// http://jabber.org/protocol/pubsub#publish-options -/// -/// -/// presence -/// -/// -/// -/// -/// -/// ``` -class PubSubPublishOptions extends XMLBase { - PubSubPublishOptions({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'publish-options', - namespace: _$namespace, - includeNamespace: false, - pluginAttribute: 'publish_options', - interfaces: {'publish_options'}, - ) { - addGetters({ - const Symbol('publish_options'): (args, base) => _publishOptions, - }); - - addSetters({ - const Symbol('publish_options'): (value, args, base) => - _setPublishOptions(value), - }); + builder.element('item', attributes: attributes); + final element = builder.buildDocument().rootElement; - addDeleters({ - const Symbol('publish_options'): (args, base) => _deletePublishOptions(), - }); - - registerPlugin(Form()); - } - - Form? get _publishOptions { - final config = - element!.getElement('x', namespace: WhixpUtils.getNamespace('FORMS')); - if (config == null) { - return null; + if (tune != null) { + element.children.add(tune!.toXML().copy()); } - final form = Form(element: config); - return form; - } - - void _setPublishOptions(dynamic value) { - if (value == null) { - _deletePublishOptions(); - } else { - if (value is XMLBase) { - element!.children.add(value.element!); - } else if (value is xml.XmlElement) { - element!.children.add(value); - } + if (mood != null) { + element.children.add(mood!.toXML().copy()); } - } - - void _deletePublishOptions() { - final config = - element!.getElement('x', namespace: WhixpUtils.getNamespace('FORMS')); - if (config != null) { - element!.children.remove(config); + if (payload != null) { + element.children.add(payload!.toXML().copy()); } - parent!.element!.children.remove(element); + + return element; } @override - PubSubPublishOptions copy({xml.XmlElement? element, XMLBase? parent}) => - PubSubPublishOptions( - pluginAttributeMapping: pluginAttributeMapping, - pluginTagMapping: pluginTagMapping, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); + String toString() => + '''PubSub Item (id: $id, publisher: $publisher, stanza: $payload, tune: $tune, mood: $mood) '''; } From 9eb744d6c59daeeee386be0dfcb6d5e7f7712d87 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:08:30 +0400 Subject: [PATCH 69/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/stanza/iq.dart | 341 +++++++++++++++-------------------------- 1 file changed, 122 insertions(+), 219 deletions(-) diff --git a/lib/src/stanza/iq.dart b/lib/src/stanza/iq.dart index 23876cc..25900d7 100644 --- a/lib/src/stanza/iq.dart +++ b/lib/src/stanza/iq.dart @@ -1,17 +1,14 @@ import 'dart:async'; import 'package:synchronized/extension.dart'; + import 'package:whixp/src/exception.dart'; import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/jid/jid.dart'; import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/bind/bind.dart'; -import 'package:whixp/src/plugins/plugins.dart'; import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/root.dart'; -import 'package:whixp/src/stanza/roster.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/stanza/node.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/transport.dart'; import 'package:whixp/src/utils/utils.dart'; @@ -41,125 +38,84 @@ import 'package:xml/xml.dart' as xml; /// ``` /// /// For more information on "id" and "type" please refer to [XML stanzas](https://xmpp.org/rfcs/rfc3920.html#stanzas) -class IQ extends RootStanza { - /// All parameters are extended from [RootStanza]. For more information please - /// take a look at [RootStanza]. - IQ({ - bool generateID = true, - super.stanzaType, - super.stanzaTo, - super.stanzaFrom, - super.stanzaID, - super.transport, - super.subInterfaces, - super.languageInterfaces, - super.includeNamespace = false, - super.getters, - super.setters, - super.deleters, - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginMultiAttribute, - super.pluginIterables, - super.overrides, - super.isExtension, - super.boolInterfaces, - super.receive, - super.element, - super.parent, - }) : super( - name: 'iq', - namespace: WhixpUtils.getNamespace('CLIENT'), - interfaces: {'type', 'to', 'from', 'id', 'query'}, - types: {'get', 'result', 'set', 'error'}, - pluginAttribute: 'iq', - ) { - _generateID = generateID; - if (_generateID) { - if (!receive && (this['id'] == '' || this['id'] == null)) { - if (transport != null) { - this['id'] = WhixpUtils.getUniqueId(); - } else { - this['id'] = '0'; - } - } +class IQ extends Stanza with Attributes { + static const String _name = 'iq'; + + /// Constructs an IQ stanza. + IQ({bool generateID = false}) { + if (generateID) id = WhixpUtils.generateUniqueID(); + _handlerID = WhixpUtils.generateUniqueID('handler'); + } + + /// `Error` stanza associated with this IQ stanza, if any. + ErrorStanza? error; + + /// Payload of the IQ stanza. + Stanza? payload; + + /// Any other XML node associated with this IQ stanza. + Node? any; + + String? _handlerID; + + /// Constructs an IQ stanza from a string representation. + /// + /// Throws [WhixpInternalException] if the input XML is invalid. + factory IQ.fromString(String stanza) { + try { + final root = xml.XmlDocument.parse(stanza).rootElement; + + return IQ.fromXML(root); + } catch (_) { + throw WhixpInternalException.invalidXML(); + } + } + + /// Constructs an IQ stanza from an XML element node. + /// + /// Throws [WhixpInternalException] if the provided XML node is invalid. + factory IQ.fromXML(xml.XmlElement node) { + if (node.localName != _name) { + throw WhixpInternalException.invalidNode(node.localName, _name); } - addSetters( - { - const Symbol('query'): (value, args, base) { - xml.XmlElement? query = base.element!.getElement(value as String); - if (query == null && value.isNotEmpty) { - final plugin = base - .pluginTagMapping['<${base.name} xmlns="${base.namespace}"/>']; - if (plugin != null) { - base.enable(plugin.pluginAttribute); - } else { - base.clear(); - query = WhixpUtils.xmlElement('query', namespace: value); - base.element!.children.add(query); - } - } - }, - }, - ); - - addGetters( - { - const Symbol('query'): (args, base) { - for (final child in base.element!.childElements) { - if (child.qualifiedName.endsWith('query')) { - final namespace = child.getAttribute('xmlns'); - return namespace; - } - } - return ''; - }, - }, - ); - - addDeleters( - { - const Symbol('query'): (args, base) { - final elements = []; - for (final child in base.element!.childElements) { - if (child.qualifiedName.endsWith('query')) { - elements.add(child); - } - } + final iq = IQ(); + iq.loadAttributes(node); + + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'error': + iq.error = ErrorStanza.fromXML(child); + default: + try { + final tag = WhixpUtils.generateNamespacedElement(child); - for (final element in elements) { - base.element!.children.removeWhere((el) => el == element); + iq.payload = Stanza.payloadFromXML(tag, child); + } on WhixpException catch (exception) { + iq.any = Node.fromXML(child); + Log.instance.warning(exception.message); } - }, - }, - ); + } + } - /// Register all required stanzas beforehand, so we won't need to declare - /// them one by one whenever there is a need to specific stanza. - /// - /// If you have not used the specified stanza, then you have to enable the - /// stanza through the usage of `pluginAttribute` parameter. - registerPlugin(StanzaError()); - registerPlugin(BindStanza()); - registerPlugin(Roster()); - registerPlugin(Form()); - registerPlugin(PingStanza()); - registerPlugin(DiscoveryItems()); - registerPlugin(DiscoveryInformation()); - registerPlugin(RSMStanza()); - registerPlugin(PubSubStanza()); - registerPlugin(PubSubOwnerStanza()); - registerPlugin(Register()); - registerPlugin(VCardTempStanza()); - registerPlugin(Command()); + return iq; } - /// The id of the attached [Handler]. - String? _handlerID; + /// Converts the IQ stanza to its XML representation. + @override + xml.XmlElement toXML() { + final dictionary = attributeHash; + final builder = WhixpUtils.makeGenerator(); - /// Indicates that whether generate ID or not. - late final bool _generateID; + builder.element(_name, attributes: dictionary); + final root = builder.buildDocument().rootElement; + + if (payload != null) root.children.add(payload!.toXML().copy()); + if (error != null) root.children.add(error!.toXML().copy()); + if (any != null) root.children.add(any!.toXML().copy()); + + return root; + } /// Sends an IQ stanza over the XML stream. /// @@ -172,7 +128,7 @@ class IQ extends RootStanza { /// /// You can set the return of the callback return type you have provided or /// just avoid this. - FutureOr sendIQ({ + FutureOr send({ /// Sync or async callback function which accepts the incoming "result" /// iq stanza. FutureOr Function(IQ iq)? callback, @@ -181,7 +137,7 @@ class IQ extends RootStanza { /// /// It is handy way of handling the failure, 'cause the [Completer] can not /// handle the uncaught exceptions. - FutureOr Function(StanzaError error)? failureCallback, + FutureOr Function(ErrorStanza error)? failureCallback, /// Whenever there is a timeout, this callback method will be called. FutureOr Function()? timeoutCallback, @@ -189,68 +145,44 @@ class IQ extends RootStanza { /// The length of time (in seconds) to wait for a response before the /// [timeoutCallback] is called, instead of the sync callback. Defaults to /// `10` seconds. - int timeout = 10, + int timeout = 5, }) async { final completer = Completer(); final errorCompleter = Completer(); final resultCompleter = Completer(); Handler? handler; - BaseMatcher? matcher; - if (transport!.sessionBind) { - final toJID = to ?? JabberID(transport!.boundJID.domain); - - matcher = MatchIDSender( - IDMatcherCriteria( - transport!.boundJID, - toJID, - this['id'] as String, - ), - ); - } else { - matcher = MatcherID(this['id'] as String); - } - - Future successCallback(StanzaBase iq) async { - IQ stanza; - if (iq is! IQ) { - stanza = IQ(element: iq.element); - } else { - stanza = iq; - } - final type = stanza['type']; + Future successCallback(IQ iq) async { + final type = iq.type; if (type == 'result') { if (!completer.isCompleted) { - completer.complete(stanza); - resultCompleter.complete(stanza); + completer.complete(iq); + resultCompleter.complete(iq); errorCompleter.complete(null); } } else if (type == 'error') { if (!completer.isCompleted && !errorCompleter.isCompleted) { try { completer.complete(null); - errorCompleter.complete(stanza); - resultCompleter.complete(stanza); - - throw StanzaException.iq(stanza); + errorCompleter.complete(iq); + resultCompleter.complete(iq); } catch (error) { Log.instance.error(error.toString()); } } } else { - handler = FutureCallbackHandler( + handler = Handler( _handlerID!, - (stanza) => Future.microtask(() => successCallback(stanza)) + (stanza) => Future.microtask(() => successCallback(stanza as IQ)) .timeout(Duration(seconds: timeout)), - matcher: matcher!, - ); + )..id(id!); - transport?.registerHandler(handler!); + Transport.instance().registerHandler(handler!); } - transport?.cancelSchedule(_handlerID!); + Transport.instance().cancelSchedule(_handlerID!); /// Run provided callback if there is any completed IQ stanza. if (callback != null) { @@ -263,8 +195,8 @@ class IQ extends RootStanza { /// Run provided failure callback if there is any completed error stanza. if (failureCallback != null) { final result = await errorCompleter.future; - if (result != null) { - await failureCallback.call(result['error'] as StanzaError); + if (result != null && result.error != null) { + await failureCallback.call(result.error!); } } } @@ -272,12 +204,10 @@ class IQ extends RootStanza { void callbackTimeout() { runZonedGuarded( () { - if (!resultCompleter.isCompleted) { - throw StanzaException.timeout(this); - } + if (!resultCompleter.isCompleted) throw StanzaException.timeout(this); }, (error, trace) { - transport?.removeHandler(_handlerID!); + Transport.instance().removeHandler(_handlerID!); if (timeoutCallback != null) { timeoutCallback.call(); } @@ -285,75 +215,48 @@ class IQ extends RootStanza { ); } - if ({'get', 'set'}.contains(this['type'] as String)) { - _handlerID = this['id'] as String; - - handler = FutureCallbackHandler( + if ({'get', 'set'}.contains(type)) { + handler = Handler( _handlerID!, - (iq) => Future.microtask(() => successCallback(iq)) + (iq) => Future.microtask(() => successCallback(iq as IQ)) .timeout(Duration(seconds: timeout)), - matcher: matcher, - ); + )..id(id!); - transport! - ..schedule(_handlerID!, callbackTimeout, seconds: timeout) - ..registerHandler(handler!); + Transport.instance() + ..registerHandler(handler!) + ..schedule(handler!.name, callbackTimeout, seconds: timeout); } - send(); + Transport.instance().send(this); return synchronized(() => resultCompleter.future); } - /// Send a 'feature-not-implemented' error stanza if the stanza is not - /// handled. - @override - void unhandled([Transport? transport]) { - if ({'get', 'set'}.contains(this['type'])) { - final iq = replyIQ(); - iq.transport ??= transport ?? this.transport; - iq - ..registerPlugin(StanzaError()) - ..enable('error'); - final error = iq['error'] as XMLBase; - error['condition'] = 'feature-not-implemented'; - error['text'] = 'No handlers registered'; - iq.sendIQ(); - } + /// Sets an error for this IQ stanza. + void makeError(ErrorStanza error) { + type = 'error'; + this.error = error; } + /// Returns the name of the IQ stanza. @override - IQ error() { - super.error(); - return this; - } + String get name => _name; - /// Overrides [reply] method, instead copies [IQ] with the overrided [copy] - /// method. - IQ replyIQ({bool clear = true}) { - final iq = super.reply(copiedStanza: copy(), clear: clear); - iq['type'] = 'result'; - return iq; - } + @override + bool operator ==(Object other) => + identical(this, other) || + other is IQ && + runtimeType == other.runtimeType && + type == other.type && + id == other.id && + payload == other.payload && + error == other.error && + any == other.any; @override - IQ copy({xml.XmlElement? element, XMLBase? parent, bool receive = false}) => - IQ( - generateID: _generateID, - pluginMultiAttribute: pluginMultiAttribute, - overrides: overrides, - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - subInterfaces: subInterfaces, - boolInterfaces: boolInterfaces, - languageInterfaces: languageInterfaces, - pluginIterables: pluginIterables, - isExtension: isExtension, - includeNamespace: includeNamespace, - getters: getters, - setters: setters, - deleters: deleters, - receive: receive, - element: element, - parent: parent, - ); + int get hashCode => + type.hashCode ^ + id.hashCode ^ + payload.hashCode ^ + error.hashCode ^ + any.hashCode; } From d0d914ef7ce6c5150ca7d190f88de8b99459a78c Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:08:47 +0400 Subject: [PATCH 70/81] refactor: Update _database.dart for improved code organization and functionality --- lib/src/roster/_database.dart | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/src/roster/_database.dart b/lib/src/roster/_database.dart index bb03007..d5be741 100644 --- a/lib/src/roster/_database.dart +++ b/lib/src/roster/_database.dart @@ -1,36 +1,36 @@ -part of 'manager.dart'; +// part of 'manager.dart'; -class _HiveDatabase { - factory _HiveDatabase() => _instance; +// class _HiveDatabase { +// factory _HiveDatabase() => _instance; - _HiveDatabase._(); +// _HiveDatabase._(); - late Box> box; +// late Box> box; - static final _HiveDatabase _instance = _HiveDatabase._(); +// static final _HiveDatabase _instance = _HiveDatabase._(); - Future initialize(String name, [String? path]) async => - box = await Hive.openBox>(name, path: path); +// Future initialize(String name, [String? path]) async => +// box = await Hive.openBox>(name, path: path); - Map? getState(String owner, String jid) { - final data = getJID(owner); +// Map? getState(String owner, String jid) { +// final data = getJID(owner); - return data == null ? null : data[jid] as Map; - } +// return data == null ? null : data[jid] as Map; +// } - Map? getJID(String owner) => box.get(owner); +// Map? getJID(String owner) => box.get(owner); - Stream listenable() => box.watch(); +// Stream listenable() => box.watch(); - Future updateData( - String owner, - String jid, - Map data, - ) { - final existingData = getJID(owner); - if (existingData != null) { - existingData.addAll({jid: data}); - } - return box.put(owner, existingData ?? {jid: data}); - } -} +// Future updateData( +// String owner, +// String jid, +// Map data, +// ) { +// final existingData = getJID(owner); +// if (existingData != null) { +// existingData.addAll({jid: data}); +// } +// return box.put(owner, existingData ?? {jid: data}); +// } +// } From a13ab33a06f9f735e5f404bca17c41ada0933129 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:08:54 +0400 Subject: [PATCH 71/81] refactor: Update StreamFeatures class for improved code organization and functionality --- lib/src/stanza/features.dart | 212 +++++++++++++++++------------------ 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/lib/src/stanza/features.dart b/lib/src/stanza/features.dart index b4325d3..222a7f7 100644 --- a/lib/src/stanza/features.dart +++ b/lib/src/stanza/features.dart @@ -1,109 +1,109 @@ -import 'package:whixp/src/plugins/features.dart'; -import 'package:whixp/src/plugins/plugins.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; +// import 'package:whixp/src/plugins/features.dart'; +// import 'package:whixp/src/plugins/plugins.dart'; +// import 'package:whixp/src/xml/base.dart'; +// import 'package:whixp/src/utils/utils.dart'; -import 'package:xml/xml.dart' as xml; +// import 'package:xml/xml.dart' as xml; -/// Represents available features in an XMPP stream. -/// -/// Designed to handle incoming features list from the server including both -/// __required__ and __optional__ features. -/// -/// ### Example: -/// ```xml -/// -/// -/// urn:ietf:params:xml:ns:xmpp-bind -/// bind -/// bind -/// Support for Resource Binding -/// RFC 6120: XMPP Core -/// -/// -/// urn:ietf:params:xml:ns:xmpp-sasl -/// mechanisms -/// mechanisms -/// Support for Simple Authentication and Security Layer (SASL) -/// RFC 6120: XMPP Core -/// -/// -/// ``` -/// -/// For more information: [Stream Features](https://xmpp.org/registrar/stream-features.html) -class StreamFeatures extends StanzaBase { - /// Accepts optional XML element property. This comes handy when there is a - /// need to parse [XMLBase] from an element or when there is a need to enable - /// any plugin externally. - /// - /// ### Example: - /// ```dart - /// final features = StreamFeatures(); - /// final mechanisms = Mechanisms(); - /// features.registerPlugin(mechanisms); - /// features.enable(mechanisms.name); - /// - /// log(features['mechanisms']); - /// ``` - StreamFeatures({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'features', - namespace: WhixpUtils.getNamespace('JABBER_STREAM'), - interfaces: {'features', 'required', 'optional'}, - subInterfaces: {'features', 'required', 'optional'}, - getters: { - const Symbol('features'): (args, base) { - final features = {}; - for (final plugin in base.plugins.entries) { - features[plugin.key.value1] = plugin.value; - } - return features; - }, - const Symbol('required'): (args, base) { - final features = base['features'] as Map; - return features.entries - .where( - (entry) => (entry.value as XMLBase)['required'] == true, - ) - .map((entry) => entry.value) - .toList(); - }, - const Symbol('optional'): (args, base) { - final features = base['features'] as Map; - return features.entries - .where( - (entry) => (entry.value as XMLBase)['required'] == false, - ) - .map((entry) => entry.value) - .toList(); - }, - }, - ) { - registerPlugin(BindStanza()); - registerPlugin(Session()); - registerPlugin(StartTLS()); - registerPlugin(Mechanisms()); - registerPlugin(RosterVersioning()); - registerPlugin(RegisterFeature()); - registerPlugin(PreApproval()); - registerPlugin(StreamManagementStanza()); - registerPlugin(CompressionStanza()); - } +// /// Represents available features in an XMPP stream. +// /// +// /// Designed to handle incoming features list from the server including both +// /// __required__ and __optional__ features. +// /// +// /// ### Example: +// /// ```xml +// /// +// /// +// /// urn:ietf:params:xml:ns:xmpp-bind +// /// bind +// /// bind +// /// Support for Resource Binding +// /// RFC 6120: XMPP Core +// /// +// /// +// /// urn:ietf:params:xml:ns:xmpp-sasl +// /// mechanisms +// /// mechanisms +// /// Support for Simple Authentication and Security Layer (SASL) +// /// RFC 6120: XMPP Core +// /// +// /// +// /// ``` +// /// +// /// For more information: [Stream Features](https://xmpp.org/registrar/stream-features.html) +// class StreamFeatures extends StanzaBase { +// /// Accepts optional XML element property. This comes handy when there is a +// /// need to parse [XMLBase] from an element or when there is a need to enable +// /// any plugin externally. +// /// +// /// ### Example: +// /// ```dart +// /// final features = StreamFeatures(); +// /// final mechanisms = Mechanisms(); +// /// features.registerPlugin(mechanisms); +// /// features.enable(mechanisms.name); +// /// +// /// log(features['mechanisms']); +// /// ``` +// StreamFeatures({ +// super.pluginTagMapping, +// super.pluginAttributeMapping, +// super.element, +// super.parent, +// }) : super( +// name: 'features', +// namespace: WhixpUtils.getNamespace('JABBER_STREAM'), +// interfaces: {'features', 'required', 'optional'}, +// subInterfaces: {'features', 'required', 'optional'}, +// getters: { +// const Symbol('features'): (args, base) { +// final features = {}; +// for (final plugin in base.plugins.entries) { +// features[plugin.key.value1] = plugin.value; +// } +// return features; +// }, +// const Symbol('required'): (args, base) { +// final features = base['features'] as Map; +// return features.entries +// .where( +// (entry) => (entry.value as XMLBase)['required'] == true, +// ) +// .map((entry) => entry.value) +// .toList(); +// }, +// const Symbol('optional'): (args, base) { +// final features = base['features'] as Map; +// return features.entries +// .where( +// (entry) => (entry.value as XMLBase)['required'] == false, +// ) +// .map((entry) => entry.value) +// .toList(); +// }, +// }, +// ) { +// registerPlugin(BindStanza()); +// registerPlugin(Session()); +// registerPlugin(StartTLS()); +// registerPlugin(Mechanisms()); +// registerPlugin(RosterVersioning()); +// registerPlugin(RegisterFeature()); +// registerPlugin(PreApproval()); +// registerPlugin(StreamManagementStanza()); +// registerPlugin(CompressionStanza()); +// } - @override - StreamFeatures copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - StreamFeatures( - pluginAttributeMapping: pluginAttributeMapping, - pluginTagMapping: pluginTagMapping, - element: element, - parent: parent, - ); -} +// @override +// StreamFeatures copy({ +// xml.XmlElement? element, +// XMLBase? parent, +// bool receive = false, +// }) => +// StreamFeatures( +// pluginAttributeMapping: pluginAttributeMapping, +// pluginTagMapping: pluginTagMapping, +// element: element, +// parent: parent, +// ); +// } From 6a299c561189b43b40b160083bdc5a584301abe8 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:09:01 +0400 Subject: [PATCH 72/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/whixp.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/whixp.dart b/lib/whixp.dart index 7fc8d4e..dcafd19 100644 --- a/lib/whixp.dart +++ b/lib/whixp.dart @@ -2,16 +2,18 @@ /// Messaging and Presence Protocol) connections. library whixp; +export 'src/client.dart'; +export 'src/enums.dart'; export 'src/exception.dart'; export 'src/jid/jid.dart'; export 'src/log/log.dart'; export 'src/plugins/plugins.dart'; +export 'src/reconnection.dart'; export 'src/roster/manager.dart'; export 'src/stanza/atom.dart'; export 'src/stanza/error.dart'; export 'src/stanza/iq.dart'; export 'src/stanza/message.dart'; export 'src/stanza/presence.dart'; -export 'src/stanza/roster.dart' hide RosterItem; +export 'src/stanza/stanza.dart'; export 'src/utils/utils.dart'; -export 'src/whixp.dart'; From a87515a91bfda3535f2cf610675711ed59ea4ee0 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:09:05 +0400 Subject: [PATCH 73/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/client.dart | 383 +++++++++++++++++++++++++------------------- 1 file changed, 220 insertions(+), 163 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 64191b7..7004b7a 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1,4 +1,10 @@ -part of 'whixp.dart'; +import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/handler/handler.dart'; +import 'package:whixp/src/log/log.dart'; +import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/session.dart'; +import 'package:whixp/src/stanza/mixins.dart'; +import 'package:whixp/src/whixp.dart'; class Whixp extends WhixpBase { /// Client class for [Whixp]. @@ -29,9 +35,6 @@ class Whixp extends WhixpBase { /// The default interval between keepalive signals when [whitespaceKeepAlive] /// is enabled. Represents in seconds. Defaults to `300`. /// - /// [maxReconnectionAttempt] is the maximum number of reconnection attempts. - /// This parameter is optional and defaults to `3`. - /// /// If [useIPv6] is set to `true`, attempts to use IPv6 when connecting. /// /// [useTLS] is the DirectTLS activator. Defaults to `false`. @@ -61,132 +64,186 @@ class Whixp extends WhixpBase { /// final whixp = Whixp('vsevex@example.com', 'passwd'); /// whixp.connect(); /// ``` - Whixp( - String jabberID, - String password, { + Whixp({ + super.jabberID, + String? password, super.host, super.port, super.connectionTimeout, - super.whitespaceKeepAliveInterval, - super.maxReconnectionAttempt, super.useIPv6, super.useTLS, super.disableStartTLS, - super.whitespaceKeepAlive, + super.pingKeepAlive, + super.pingKeepAliveInterval, super.logger, super.context, super.onBadCertificateCallback, - super.hivePathName, - super.provideHivePath, + super.internalDatabasePath, + super.reconnectionPolicy, String language = 'en', - }) : super(jabberID: jabberID) { + }) { _language = language; /// Automatically calls the `_setup()` method to configure the XMPP client. _setup(); - credentials.addAll({'password': password}); + if (password?.isNotEmpty ?? false) { + credentials.addAll({'password': password!}); + } } void _setup() { _reset(); + final mechanisms = FeatureMechanisms(this)..pluginInitialize(); + registerFeature( + 'starttls', + (_) async { + await transport.startTLS(); + final result = StartTLS.handleStartTLS(); + return result; + }, + order: 10, + ); + registerFeature( + 'mechanisms', + mechanisms.handleSASLAuth, + restart: true, + order: 50, + ); + + late String host; + + if (transport.boundJID == null) { + host = transport.host; + } else { + host = transport.boundJID!.host; + } + /// Set [streamHeader] of declared transport for initial send. transport ..streamHeader = - "" + "" ..streamFooter = ""; - StanzaBase features = StreamFeatures(); - - /// Register all necessary features. - registerPlugin(FeatureBind()); - registerPlugin(FeatureSession()); - registerPlugin(FeatureStartTLS()); - registerPlugin(FeatureMechanisms()); - registerPlugin(FeatureRosterVersioning()); - registerPlugin(FeaturePreApproval()); + final fullJID = session?.bindJID?.full ?? transport.boundJID?.full ?? ''; transport - ..registerStanza(features) ..registerHandler( - FutureCallbackHandler( - 'Stream Features', - (stanza) async { - features = features.copy(element: stanza.element); - _handleStreamFeatures(features); - return; - }, - matcher: XPathMatcher('{$_streamNamespace}features'), - ), + Handler('Stream Features', _handleStreamFeatures) + ..packet('stream:features'), ) - ..addEventHandler( - 'sessionBind', - (jid) => _handleSessionBind(jid!), + ..registerHandler( + Handler('SM Request', (packet) => session?.sendAnswer()) + ..packet('sm:request'), ) - ..addEventHandler('rosterUpdate', (iq) => _handleRoster(iq!)) ..registerHandler( - CallbackHandler( - 'Roster Update', - (iq) { - final JabberID? from; - try { - from = iq.from; - if (from != null && - from.toString().isNotEmpty && - from.toString() != transport.boundJID.bare) { - final reply = (iq as IQ).replyIQ(); - reply['type'] = 'error'; - final error = reply['error'] as StanzaError; - error['type'] = 'cancel'; - error['code'] = 503; - error['condition'] = 'service-unavailable'; - reply.sendIQ(); - return; - } - transport.emit('rosterUpdate', data: iq as IQ); - } on Exception { - transport.emit('rosterUpdate', data: iq as IQ); - } - }, - matcher: StanzaPathMatcher('iq@type=set/roster'), - ), + Handler('SM Answer', (packet) => session?.handleAnswer(packet, fullJID)) + ..packet('sm:answer'), + ) + ..addEventHandler('startSession', (_) => session?.enabledOut = true) + ..addEventHandler('endSession', (_) => session?.enabledOut = false) + ..addEventHandler( + 'increaseHandled', + (_) => session?.increaseInbound(fullJID), ); + // ..addEventHandler( + // 'sessionBind', + // (jid) => _handleSessionBind(jid!), + // ) + // ..addEventHandler('rosterUpdate', (iq) => _handleRoster(iq!)); + // ..registerHandler( + // CallbackHandler( + // 'Roster Update', + // (iq) { + // final JabberID? from; + // try { + // from = iq.from; + // if (from != null && + // from.toString().isNotEmpty && + // from.toString() != transport.boundJID.bare) { + // final reply = (iq as IQ).replyIQ(); + // reply['type'] = 'error'; + // final error = reply['error'] as StanzaError; + // error['type'] = 'cancel'; + // error['code'] = 503; + // error['condition'] = 'service-unavailable'; + // reply.sendIQ(); + // return; + // } + // transport.emit('rosterUpdate', data: iq as IQ); + // } on Exception { + // transport.emit('rosterUpdate', data: iq as IQ); + // } + // }, + // matcher: StanzaPathMatcher('iq@type=set/roster'), + // ), + // ); + transport.defaultLanguage = _language; } - void _reset() => _streamFeatureHandlers.clear(); + void _reset() => streamFeatureHandlers.clear(); /// Default language to use in stanza communication. late final String _language; - Future _handleStreamFeatures(StanzaBase features) async { - for (final feature in _streamFeatureOrder) { - final name = feature.value2; + /// Callable function that is triggered when stream is enabled. + Future _onStreamEnabled(Packet packet) async { + if (packet is! SMEnabled) return; + await session?.saveSMState( + session?.bindJID?.full ?? transport.boundJID?.full, + SMState(packet.id!, 0, 0, 0), + ); + } + + Future _handleStreamFeatures(Packet features) async { + if (features is! StreamFeatures) return false; + if (transport.disableStartTLS && features.tlsRequired) { + AuthenticationException.requiresTLS(); + } + if (!transport.disableStartTLS && !features.tlsRequired) { + AuthenticationException.disabledTLS(); + } + + /// Attach new [Session] manager for this connection. + session = Session(features); + if (StreamFeatures.supported.contains('mechanisms')) { + /// If sm is not supported by the server, then add binding feature. + if (!features.doesStreamManagement) { + registerFeature('bind', (_) => session!.bind(), order: 150); + } + registerFeature( + 'sm', + (_) => session!.resume( + session?.bindJID?.full ?? transport.boundJID?.full, + onResumeDone: () => transport + ..removeHandler('SM Resume Handler') + ..removeHandler('SM Enable Handler'), + onResumeFailed: () { + Log.instance.warning('Stream resumption failed'); + return session!.enableStreamManagement(_onStreamEnabled); + }, + ), + order: 100, + ); + } - if ((features['features'] as Map).containsKey(name) && - (features['features'] as Map)[name] != null) { - final handler = _streamFeatureHandlers[name]!.value1; - final restart = _streamFeatureHandlers[name]!.value2; + for (final feature in streamFeatureOrder) { + final name = feature.secondValue; - final result = await handler(features); + if (StreamFeatures.list.contains(name)) { + final handler = streamFeatureHandlers[name]!.firstValue; + + final result = await handler.call(features); /// Using delay, 'cause establishing connection may require time, /// and if there is something to do with event handling, we should have /// time to do necessary things. (e.g. registering user before sending /// auth challenge to the server) - await Future.delayed(const Duration(milliseconds: 150)); - - if (result != null && restart) { - if (result is bool) { - if (result) { - return true; - } - } else { - return true; - } - } + // await Future.delayed(const Duration(milliseconds: 150)); + if (result) return true; } } @@ -195,90 +252,90 @@ class Whixp extends WhixpBase { return false; } - void _handleSessionBind(JabberID jid) => - clientRoster = roster[jid.bare] as rost.RosterNode; + // void _handleSessionBind(JabberID jid) => + // clientRoster = roster[jid.bare] as rost.RosterNode; - /// Adds or changes a roster item. - /// - /// [jid] is the entry to modify. - FutureOr updateRoster( - String jid, { - String? name, - String? subscription, - List? groups, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) { - final current = clientRoster[jid] as rost.RosterItem; - - name ??= current['name'] as String; - subscription ??= current['subscription'] as String; - groups ??= (current['groups'] as List).isEmpty - ? null - : current['groups'] as List; - - return clientRoster.update( - jid, - name: name, - groups: groups, - subscription: subscription, - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } + // /// Adds or changes a roster item. + // /// + // /// [jid] is the entry to modify. + // FutureOr updateRoster( + // String jid, { + // String? name, + // String? subscription, + // List? groups, + // FutureOr Function(IQ iq)? callback, + // FutureOr Function(StanzaError error)? failureCallback, + // FutureOr Function()? timeoutCallback, + // int timeout = 10, + // }) { + // final current = clientRoster[jid] as rost.RosterItem; - void _handleRoster(IQ iq) { - if (iq['type'] == 'set') { - final JabberID? from; - try { - from = iq.from; - if (from != null && - from.bare.isNotEmpty && - from.bare != transport.boundJID.bare) { - throw StanzaException.serviceUnavailable(iq); - } - } on Exception { - /// pass; - } - } + // name ??= current['name'] as String; + // subscription ??= current['subscription'] as String; + // groups ??= (current['groups'] as List).isEmpty + // ? null + // : current['groups'] as List; - if (((iq['roster'] as Roster).copy()['ver'] as String).isNotEmpty) { - clientRoster.version = (iq['roster'] as Roster)['ver'] as String; - } - final items = - (iq['roster'] as Roster)['items'] as Map>; - - final validSubscriptions = {'to', 'from', 'both', 'none', 'remove'}; - for (final item in items.entries) { - final value = item.value; - final rosterItem = clientRoster[item.key] as rost.RosterItem; - if (validSubscriptions.contains(value['subscription'])) { - rosterItem['name'] = value['name']; - rosterItem['groups'] = value['groups']; - rosterItem['from'] = - {'from', 'both'}.contains(value['subscription'] as String); - rosterItem['to'] = - {'to', 'both'}.contains(value['subscription'] as String); - rosterItem['pending_out'] = value['ask'] == 'subscribe'; - - rosterItem.save(remove: value['subscription'] == 'remove'); - } - } + // return clientRoster.update( + // jid, + // name: name, + // groups: groups, + // subscription: subscription, + // callback: callback, + // failureCallback: failureCallback, + // timeoutCallback: timeoutCallback, + // timeout: timeout, + // ); + // } - if (iq['type'] == 'set') { - final response = IQ( - stanzaType: 'result', - stanzaTo: iq.from ?? transport.boundJID, - stanzaID: iq['id'] as String, - transport: transport, - )..enable('roster'); - response.sendIQ(); - } - } + // void _handleRoster(IQ iq) { + // if (iq['type'] == 'set') { + // final JabberID? from; + // try { + // from = iq.from; + // if (from != null && + // from.bare.isNotEmpty && + // from.bare != transport.boundJID.bare) { + // throw StanzaException.serviceUnavailable(iq); + // } + // } on Exception { + // /// pass; + // } + // } + + // if (((iq['roster'] as Roster).copy()['ver'] as String).isNotEmpty) { + // clientRoster.version = (iq['roster'] as Roster)['ver'] as String; + // } + // final items = + // (iq['roster'] as Roster)['items'] as Map>; + + // final validSubscriptions = {'to', 'from', 'both', 'none', 'remove'}; + // for (final item in items.entries) { + // final value = item.value; + // final rosterItem = clientRoster[item.key] as rost.RosterItem; + // if (validSubscriptions.contains(value['subscription'])) { + // rosterItem['name'] = value['name']; + // rosterItem['groups'] = value['groups']; + // rosterItem['from'] = + // {'from', 'both'}.contains(value['subscription'] as String); + // rosterItem['to'] = + // {'to', 'both'}.contains(value['subscription'] as String); + // rosterItem['pending_out'] = value['ask'] == 'subscribe'; + + // rosterItem.save(remove: value['subscription'] == 'remove'); + // } + // } + + // if (iq['type'] == 'set') { + // final response = IQ( + // stanzaType: 'result', + // stanzaTo: iq.from ?? transport.boundJID, + // stanzaID: iq['id'] as String, + // transport: transport, + // )..enable('roster'); + // response.sendIQ(); + // } + // } /// Connects to the XMPP server. /// From 222342cffd9c0f697a49cef981f89dfe76a8384b Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:09:13 +0400 Subject: [PATCH 74/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/exception.dart | 93 +++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 309f5ff..f7263dc 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -1,7 +1,5 @@ -import 'package:whixp/src/stanza/error.dart'; import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/presence.dart'; -import 'package:whixp/src/stream/base.dart'; +import 'package:whixp/src/stanza/stanza.dart'; /// The given given class is a custom written [Exception] class for [Whixp]. /// @@ -26,6 +24,72 @@ abstract class WhixpException implements Exception { String toString() => '''Whixp Exception: $message'''; } +/// Represents an exception that occurs during the internal processing of Whixp. +class WhixpInternalException extends WhixpException { + /// Constructor for [WhixpInternalException] that takes a message as a + /// parameter. + const WhixpInternalException(super.message); + + /// Whenever there is an exception there in the setup process of the package, + /// then this exception will be thrown. + factory WhixpInternalException.setup(String message) => + WhixpInternalException(message); + + /// Factory constructor for [WhixpInternalException] that creates an instance + /// of the exception with a message "Invalid XML". + factory WhixpInternalException.invalidXML() => + const WhixpInternalException('Invalid XML'); + + /// Factory constructor for [WhixpInternalException] that creates an instance + /// of the exception with a message "Invalid node". + factory WhixpInternalException.invalidNode(String node, String name) => + WhixpInternalException('Invalid $node, expecting $name'); + + /// Factory constructor for [WhixpInternalException] that creates an instance + /// of the exception with a message "Unexpected XMPP packet". + factory WhixpInternalException.unexpectedPacket( + String? namespace, + String node, + ) => + WhixpInternalException('Unexpected XMPP packet {$namespace} <$node>'); + + /// Factory constructor for [WhixpInternalException] that creates an instance + /// of the exception with a message "Unknown namespace while trying to parse + /// element". + factory WhixpInternalException.unknownNamespace(String namespace) => + WhixpInternalException( + 'Unknown namespace($namespace) while trying to parse element', + ); + + /// Factory constructor for [WhixpInternalException] that creates an instance + /// of the exception with a message "Unable to find stanza for XML Tag". + factory WhixpInternalException.stanzaNotFound( + String stanza, + String tag, + ) => + WhixpInternalException('Unable to find $stanza stanza for XML Tag: $tag'); + + /// Overrides of the `toString` method to return the message of the exception. + @override + String toString() => message; +} + +/// Exception thrown when authentication fails. +class AuthenticationException extends WhixpException { + /// Creates an [AuthenticationException] with the given [message]. + AuthenticationException(super.message); + + /// Creates an [AuthenticationException] indicating that TLS is required by the server. + factory AuthenticationException.requiresTLS() => AuthenticationException( + 'Server requires TLS session. Ensure you either "disableStartTLS" attribute to "false"', + ); + + /// Creates an [AuthenticationException] indicating that TLS is disabled but requested. + factory AuthenticationException.disabledTLS() => AuthenticationException( + "You requested TLS session, but Server doesn't support TLS", + ); +} + /// Represents an exception related to XMPP stanzas within the context of the /// [Whixp]. /// @@ -69,30 +133,28 @@ class StanzaException extends WhixpException { final String errorType; /// The XMPP stanza associated with the exception. - final XMLBase? stanza; + final Stanza? stanza; /// Creates a [StanzaException] for a timed-out response from the server. - factory StanzaException.timeout(StanzaBase stanza) => StanzaException( + factory StanzaException.timeout(Stanza? stanza) => StanzaException( 'Waiting for response from the server is timed out', stanza: stanza, condition: 'remote-server-timeout', ); /// Creates a [StanzaException] for a received service unavailable stanza. - factory StanzaException.serviceUnavailable(StanzaBase stanza) => - StanzaException( + factory StanzaException.serviceUnavailable(Stanza stanza) => StanzaException( 'Received service unavailable stanza', stanza: stanza, - condition: (stanza['error'] as StanzaError)['condition'] as String, ); /// Creates a [StanzaException] for an IQ error with additional details. factory StanzaException.iq(IQ iq) => StanzaException( 'IQ error has occured', stanza: iq, - text: (iq['error'] as StanzaError)['text'] as String, - condition: (iq['error'] as StanzaError)['condition'] as String, - errorType: (iq['error'] as StanzaError)['type'] as String, + text: iq.error?.text ?? '', + condition: iq.error?.reason ?? '', + errorType: iq.error?.type ?? '', ); /// Creates a [StanzaException] for an IQ timeout. @@ -102,15 +164,6 @@ class StanzaException extends WhixpException { condition: 'remote-server-timeout', ); - /// Creates a [StanzaException] for an Presence error. - factory StanzaException.presence(Presence presence) => StanzaException( - 'Presence error has occured', - stanza: presence, - condition: (presence['error'] as StanzaError)['condition'] as String, - text: (presence['error'] as StanzaError)['text'] as String, - errorType: (presence['error'] as StanzaError)['type'] as String, - ); - /// Formats the exception details. String get _format { final text = From 114be93c7778a1e7b3c762fef61ef18ea9cb0735 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:09:32 +0400 Subject: [PATCH 75/81] refactor: Update XMPP classes for improved code organization and functionality --- lib/src/plugins/plugins.dart | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/src/plugins/plugins.dart b/lib/src/plugins/plugins.dart index 08051ae..ca71ccd 100644 --- a/lib/src/plugins/plugins.dart +++ b/lib/src/plugins/plugins.dart @@ -1,15 +1,10 @@ export 'command/command.dart'; -export 'compression/compression.dart'; export 'delay/delay.dart'; -export 'disco/disco.dart'; -export 'form/dataforms.dart'; -export 'pep/pep.dart'; -export 'ping/ping.dart'; +export 'disco/info.dart'; +export 'disco/items.dart'; +export 'form/form.dart'; +export 'id/id.dart'; export 'pubsub/pubsub.dart'; export 'push/push.dart'; -export 'register/register.dart'; -export 'rsm/rsm.dart'; -export 'sm/sm.dart'; export 'time/time.dart'; -export 'tune/tune.dart'; -export 'vcard/vcard.dart'; +export 'vcard/temp.dart'; From 0d158f92446238dca9e5021e76989c768886ebd0 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:09:59 +0400 Subject: [PATCH 76/81] refactor: Remove unused 'vcard/temp.dart' file --- lib/src/plugins/plugins.dart | 1 - lib/src/plugins/vcard/stanza.dart | 282 ------------------------------ 2 files changed, 283 deletions(-) delete mode 100644 lib/src/plugins/vcard/stanza.dart diff --git a/lib/src/plugins/plugins.dart b/lib/src/plugins/plugins.dart index ca71ccd..78e28c5 100644 --- a/lib/src/plugins/plugins.dart +++ b/lib/src/plugins/plugins.dart @@ -7,4 +7,3 @@ export 'id/id.dart'; export 'pubsub/pubsub.dart'; export 'push/push.dart'; export 'time/time.dart'; -export 'vcard/temp.dart'; diff --git a/lib/src/plugins/vcard/stanza.dart b/lib/src/plugins/vcard/stanza.dart deleted file mode 100644 index 80372e7..0000000 --- a/lib/src/plugins/vcard/stanza.dart +++ /dev/null @@ -1,282 +0,0 @@ -part of 'vcard.dart'; - -class VCardTempStanza extends XMLBase { - VCardTempStanza({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.pluginMultiAttribute, - super.element, - super.parent, - }) : super( - name: 'vCard', - namespace: WhixpUtils.getNamespace('VCARD'), - pluginAttribute: 'vcard_temp', - interfaces: {'FN', 'VERSION'}, - subInterfaces: {'FN', 'VERSION'}, - ) { - registerPlugin(Name()); - registerPlugin(Photo(), iterable: true); - registerPlugin(Nickname(), iterable: true); - registerPlugin(UID(), iterable: true); - } - - @override - VCardTempStanza copy({xml.XmlElement? element, XMLBase? parent}) => - VCardTempStanza( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - pluginMultiAttribute: pluginMultiAttribute, - element: element, - parent: parent, - ); -} - -class Name extends XMLBase { - Name({ - super.getters, - super.setters, - super.element, - super.parent, - }) : super( - name: 'N', - namespace: WhixpUtils.getNamespace('VCARD'), - includeNamespace: false, - pluginAttribute: 'N', - interfaces: {'FAMILY', 'GIVEN', 'MIDDLE', 'PREFIX', 'SUFFIX'}, - subInterfaces: { - 'FAMILY', - 'GIVEN', - 'MIDDLE', - 'PREFIX', - 'SUFFIX', - }, - ) { - addGetters({ - const Symbol('family'): (args, base) => family, - const Symbol('given'): (args, base) => given, - const Symbol('middle'): (args, base) => middle, - const Symbol('prefix'): (args, base) => prefix, - const Symbol('suffix'): (args, base) => suffix, - }); - addSetters({ - const Symbol('family'): (value, args, base) => setFamily(value as String), - const Symbol('given'): (value, args, base) => setGiven(value as String), - const Symbol('middle'): (value, args, base) => setMiddle(value as String), - const Symbol('prefix'): (value, args, base) => setPrefix(value as String), - const Symbol('suffix'): (value, args, base) => setSuffix(value as String), - }); - } - - void _setComponent(String name, dynamic value) { - late String result; - if (value is List) { - result = value.join(','); - } else if (value is String) { - result = value; - } - if (value != null) { - setSubText(name, text: result, keep: true); - } else { - deleteSub(name); - } - } - - dynamic _getComponent(String name) { - dynamic value = getSubText(name) as String; - if ((value as String).contains(',')) { - value = value.split(',').map((v) => v.trim()).toList(); - } - return value; - } - - void setFamily(String value) => _setComponent('FAMILY', value); - - dynamic get family => _getComponent('FAMILY'); - - void setGiven(String value) => _setComponent('GIVEN', value); - - dynamic get given => _getComponent('GIVEN'); - - void setMiddle(String value) => _setComponent('MIDDLE', value); - - dynamic get middle => _getComponent('MIDDLE'); - - void setPrefix(String value) => _setComponent('PREFIX', value); - - dynamic get prefix => _getComponent('PREFIX'); - - void setSuffix(String value) => _setComponent('SUFFIX', value); - - dynamic get suffix => _getComponent('SUFFIX'); - - @override - Name copy({xml.XmlElement? element, XMLBase? parent}) => Name( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class Nickname extends XMLBase { - Nickname({super.getters, super.setters, super.element, super.parent}) - : super( - name: 'NICKNAME', - namespace: WhixpUtils.getNamespace('VCARD'), - includeNamespace: false, - pluginAttribute: 'NICKNAME', - pluginMultiAttribute: 'nicknames', - interfaces: {'NICKNAME'}, - isExtension: true, - ) { - addGetters({ - const Symbol('nickname'): (args, base) => nickname, - }); - addSetters({ - const Symbol('nickname'): (value, args, base) => - setNickname(value as String), - }); - } - - void setNickname(String value) => element!.innerText = [value].join(','); - - List get nickname { - if (element!.innerText.isNotEmpty) { - return element!.innerText.split(','); - } - return []; - } - - @override - Nickname copy({xml.XmlElement? element, XMLBase? parent}) => Nickname( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} - -class BinVal extends XMLBase { - BinVal({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'BINVAL', - namespace: WhixpUtils.getNamespace('VCARD'), - includeNamespace: false, - pluginAttribute: 'BINVAL', - interfaces: {'BINVAL'}, - isExtension: true, - ) { - addGetters({ - const Symbol('binval'): (args, base) => binval, - }); - addSetters({ - const Symbol('binval'): (value, args, base) => binval = value as String, - }); - addDeleters({ - const Symbol('binval'): (args, base) => deleteBinval(), - }); - } - - @override - bool setup([xml.XmlElement? element]) { - this.element = WhixpUtils.xmlElement(''); - return super.setup(element); - } - - set binval(String value) { - deleteBinval(); - element!.innerText = WhixpUtils.unicode( - WhixpUtils.base64ToArrayBuffer(WhixpUtils.btoa(value)), - ); - parent!.add(element); - } - - String get binval { - final xml = element!.getElement('BINVAL', namespace: namespace); - if (xml != null) { - return WhixpUtils.atob(xml.innerText); - } - return ''; - } - - void deleteBinval() => parent!.deleteSub('{$namespace}BINVAL'); - - @override - BinVal copy({xml.XmlElement? element, XMLBase? parent}) => BinVal( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -class Photo extends XMLBase { - Photo({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'PHOTO', - namespace: WhixpUtils.getNamespace('VCARD'), - includeNamespace: false, - pluginAttribute: 'PHOTO', - pluginMultiAttribute: 'photos', - interfaces: {'TYPE', 'EXTVAL'}, - subInterfaces: {'TYPE', 'EXTVAL'}, - ) { - registerPlugin(BinVal()); - } - - @override - Photo copy({xml.XmlElement? element, XMLBase? parent}) => Photo( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -class UID extends XMLBase { - UID({super.getters, super.setters, super.element, super.parent}) - : super( - name: 'UID', - namespace: WhixpUtils.getNamespace('VCARD'), - includeNamespace: false, - pluginAttribute: 'UID', - pluginMultiAttribute: 'uids', - interfaces: {'UID'}, - isExtension: true, - ) { - addGetters({ - const Symbol('id'): (args, base) => uid, - }); - addSetters({ - const Symbol('id'): (value, args, base) => uid = value as String, - }); - } - - set uid(String value) => element!.innerText = value; - - String get uid => element!.innerText; - - @override - UID copy({xml.XmlElement? element, XMLBase? parent}) => UID( - getters: getters, - setters: setters, - element: element, - parent: parent, - ); -} From 232b3097329b5e65b8ddd4e619e0e8c583c26117 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:10:38 +0400 Subject: [PATCH 77/81] refactor: Remove unused 'vcard/temp.dart' file --- lib/src/roster/_database.dart | 36 -- lib/src/roster/item.dart | 377 --------------- lib/src/roster/manager.dart | 161 ------- lib/src/roster/node.dart | 248 ---------- lib/src/whixp.dart | 831 ++++++++++++++++------------------ lib/whixp.dart | 1 - 6 files changed, 394 insertions(+), 1260 deletions(-) delete mode 100644 lib/src/roster/_database.dart delete mode 100644 lib/src/roster/item.dart delete mode 100644 lib/src/roster/manager.dart delete mode 100644 lib/src/roster/node.dart diff --git a/lib/src/roster/_database.dart b/lib/src/roster/_database.dart deleted file mode 100644 index d5be741..0000000 --- a/lib/src/roster/_database.dart +++ /dev/null @@ -1,36 +0,0 @@ -// part of 'manager.dart'; - -// class _HiveDatabase { -// factory _HiveDatabase() => _instance; - -// _HiveDatabase._(); - -// late Box> box; - -// static final _HiveDatabase _instance = _HiveDatabase._(); - -// Future initialize(String name, [String? path]) async => -// box = await Hive.openBox>(name, path: path); - -// Map? getState(String owner, String jid) { -// final data = getJID(owner); - -// return data == null ? null : data[jid] as Map; -// } - -// Map? getJID(String owner) => box.get(owner); - -// Stream listenable() => box.watch(); - -// Future updateData( -// String owner, -// String jid, -// Map data, -// ) { -// final existingData = getJID(owner); -// if (existingData != null) { -// existingData.addAll({jid: data}); -// } -// return box.put(owner, existingData ?? {jid: data}); -// } -// } diff --git a/lib/src/roster/item.dart b/lib/src/roster/item.dart deleted file mode 100644 index 4b290d4..0000000 --- a/lib/src/roster/item.dart +++ /dev/null @@ -1,377 +0,0 @@ -part of 'manager.dart'; - -/// It is a single entry in a roster node, and tracks the subscription state -/// and user annotations of a single [JabberID]. -class RosterItem { - /// [RosterItem]s provide methods for handling incoming [Presence] stanzas - /// that ensure that response stanzas are sent. - RosterItem( - /// The [Whixp] instance which is assigned to this roster management - this.whixp, { - /// The JID of the roster item - required this.jid, - this.roster, - - /// The Roster's owner [JabberID] - JabberID? owner, - - /// State fields: - /// - /// "from" indicates if a subscription of type "from" has been authorized - /// "to" indicates if a subcsription of type "to" has been authorized - /// "pending_in" indicates if a subscription request has been received from - /// this JID and it has not been authorized yet - /// "subscription" returns one of: "to", "from", "both", or "none" based on - /// the stanzas of from, to, pending_in, and pending_out. Assignment to this - /// value does not affect the states of other values - /// "whitelisted" indicates if a subscription request from this JID should - /// be automatically accepted - /// "name" is an alias for the JID - /// "groups" is a list of group for the JID - Map? state, - }) { - _owner = owner ?? whixp.transport.boundJID; - _state = state ?? - { - 'from': false, - 'to': false, - 'pending_in': false, - 'pending_out': false, - 'whitelisted': false, - 'subscription': 'none', - 'name': '', - 'groups': [], - }; - - _transport = whixp.transport; - } - - /// The [Whixp] instance which is assigned to this roster management. - final WhixpBase whixp; - - /// The JID of the roster item. - final String jid; - final RosterNode? roster; - late final Map _state; - - /// A [Map] of online resources for this JID. Will contain the fields "show", - /// "status", and "priority". - final resources = {}; - - /// Will be assigned from the [Whixp] instance later. - late final Transport _transport; - - /// The Roster's owner [JabberID]. - late final JabberID _owner; - - /// The last [Presence] sent to this JID. - Presence? lastStatus; - - Future _setBackend() async { - await save(); - _load(); - } - - void _load() { - final items = _HiveDatabase().getState(_owner.bare, jid); - - if (items != null && items.isNotEmpty) { - final item = - json.decode(items['state']! as String) as Map; - - this['name'] = item['name']; - this['groups'] = item['groups']; - this['from'] = item['from']; - this['whitelisted'] = item['whitelisted']; - this['pending_out'] = item['pending_out']; - this['pending_in'] = item['pending_in']; - this['subscription'] = _subscription(); - } - } - - Future save({bool remove = false}) async { - this['subscription'] = _subscription(); - if (remove) { - _state['removed'] = true; - } - - await _HiveDatabase().updateData(_owner.bare, jid, _state); - - if (remove) { - (whixp.roster[_owner.toString()] as RosterNode).delete(jid); - } - } - - /// Send a subscription request to the JID. - void subscribe() { - final presence = Presence(transport: whixp.transport); - presence['to'] = jid; - presence['type'] = 'subscribe'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - this['pending_out'] = true; - presence.send(); - save(); - } - - /// Authorize a received subscription request from the JID. - void authorize() { - this['from'] = true; - this['pending_out'] = false; - save(); - _subscribed(); - sendLastPresence(); - } - - /// Deny a received subscription request from the JID. - void unauthorize() { - this['from'] = false; - this['pending_in'] = false; - save(); - _unsubscribed(); - - final presence = Presence(transport: whixp.transport); - presence['to'] = jid; - presence['type'] = 'unavailable'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - presence.send(); - } - - /// Handle ack a subscription. - void _subscribed() { - final presence = Presence(transport: whixp.transport); - presence['to'] = jid; - presence['type'] = 'subscribed'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - presence.send(); - } - - /// Unsubscribe from the JID. - void unsubscribe() { - final presence = Presence(transport: whixp.transport); - presence['to'] = jid; - presence['type'] = 'unsubscribe'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - save(); - presence.send(); - } - - /// Handle ack an unsubscribe request. - void _unsubscribed() { - final presence = Presence(transport: whixp.transport); - presence['to'] = jid; - presence['type'] = 'unsubscribed'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - presence.send(); - } - - /// Create, initialize, and send a [Presence] stanza. - /// - /// If no recipient is specified, send the presence immediately. Otherwise, - /// forward the send request to the recipient's roster entry for processing. - void sendPresence() { - JabberID? presenceFrom; - JabberID? presenceTo; - if (_transport.isComponent) { - presenceFrom = _owner; - } - presenceTo = JabberID(jid); - whixp.sendPresence(presenceFrom: presenceFrom, presenceTo: presenceTo); - } - - void sendLastPresence() { - if (lastStatus == null) { - final presence = roster!.lastStatus; - if (presence == null) { - sendPresence(); - } else { - presence['to'] = jid; - if (whixp.isComponent) { - presence['from'] = _owner.toString(); - } else { - presence.delete('from'); - } - presence.send(); - } - } else { - lastStatus!.send(); - } - } - - void handleAvailable(Presence presence) { - final resource = JabberID(presence['from'] as String).resource; - final data = { - 'status': presence['status'], - 'show': presence['show'], - 'priority': presence['priority'], - }; - final gotOnline = resources.isEmpty; - if (resources.containsKey(resource)) { - resources[resource] = {}; - } - final oldStatus = (resources[resource] as Map?)?['status'] ?? ''; - final oldShow = (resources[resource] as Map?)?['show']; - resources[resource] = data; - if (gotOnline) { - whixp.transport.emit('gotOnline', data: presence); - } - if (oldShow != presence['show'] || oldStatus != presence['status']) { - whixp.transport.emit('changedStatus', data: presence); - } - } - - void handleUnavailable(Presence presence) { - final resource = JabberID(presence['from'] as String).resource; - if (resources.isEmpty) { - return; - } - if (resources.containsKey(resource)) { - resources.remove(resource); - } - whixp.transport.emit('changedStatus', data: presence); - if (resources.isEmpty) { - whixp.transport.emit('gotOffline', data: presence); - } - } - - void handleSubscribe(Presence presence) { - if (whixp.isComponent) { - if (this['from'] == null && !(this['pending_in'] as bool)) { - this['pending_in'] = true; - whixp.transport - .emit('rosterSubscriptionRequest', data: presence); - } else if (this['from'] != null) { - _subscribed(); - } - save(); - } else { - whixp.transport - .emit('rosterSubscriptionRequest', data: presence); - } - } - - void handleSubscribed(Presence presence) { - if (whixp.isComponent) { - if (this['to'] == null && this['pending_out'] as bool) { - this['pending_out'] = false; - this['to'] = true; - whixp.transport - .emit('rosterSubscriptionAuthorized', data: presence); - } - save(); - } else { - whixp.transport - .emit('rosterSubscriptionAuthorized', data: presence); - } - } - - void handleUnsubscribe(Presence presence) { - if (whixp.isComponent) { - if (this['from'] == null && this['pending_in'] as bool) { - this['pending_in'] = false; - _unsubscribed(); - } else if (this['from'] != null) { - this['from'] = false; - _unsubscribed(); - whixp.transport - .emit('rosterSubscriptionRemove', data: presence); - } - save(); - } else { - whixp.transport - .emit('rosterSubscriptionRemove', data: presence); - } - } - - void handleUnsubscribed(Presence presence) { - if (whixp.isComponent) { - if (this['to'] == null && this['pending_out'] as bool) { - this['pending_out'] = false; - } else if (this['to'] != null && this['pending_out'] as bool) { - this['to'] = false; - whixp.transport - .emit('rosterSubscriptionRemoved', data: presence); - } - save(); - } else { - whixp.transport - .emit('rosterSubscriptionRemoved', data: presence); - } - } - - /// Returns a state field's value. - dynamic operator [](String key) { - if (_state.containsKey(key)) { - if (key == 'subscription') { - return _subscription(); - } - return _state[key]; - } - } - - /// Set the value of a state field. - void operator []=(String attribute, dynamic value) { - if (_state.containsKey(attribute)) { - if ({'name', 'subscription', 'groups'}.contains(attribute)) { - _state[attribute] = value; - } else { - final val = value.toString().toLowerCase(); - _state[attribute] = ['true', '1', 'on', 'yes'].contains(val); - } - } - } - - /// Returns a proper subscription type based on current state. - String _subscription() { - if (this['to'] != null && this['from'] != null) { - return 'both'; - } else if (this['from'] != null) { - return 'from'; - } else if (this['to'] != null) { - return 'to'; - } - return 'none'; - } - - void handleProbe() { - if (this['from'] as bool) { - sendLastPresence(); - } - if (this['pending_out'] as bool) { - subscribe(); - } - if (!(this['from'] as bool)) { - _unsubscribed(); - } - } - - /// Remove a JID's whitelisted status and unsubscribe if a subscription - /// exists. - void remove() { - if (this['to'] != null) { - final presence = Presence(transport: whixp.transport); - presence['to'] = this['to']; - presence['type'] = 'unsubscribe'; - if (_transport.isComponent) { - presence['from'] = _owner.toString(); - } - presence.send(); - this['to'] = false; - } - this['whitelisted'] = false; - save(); - } - - /// Forgot current resource presence information as part of a roster reset - /// request. - void reset() => resources.clear(); -} diff --git a/lib/src/roster/manager.dart b/lib/src/roster/manager.dart deleted file mode 100644 index fca0a1c..0000000 --- a/lib/src/roster/manager.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:hive/hive.dart'; -import 'package:meta/meta.dart'; - -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/presence.dart'; -import 'package:whixp/src/stanza/roster.dart' as roster; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/transport.dart'; -import 'package:whixp/src/whixp.dart'; - -part '_database.dart'; -part 'item.dart'; -part 'node.dart'; - -const _rosterTable = 'roster'; - -/// ## Roster Manager -/// -/// [Whixp] roster manager. -/// -/// The roster is divided into "node"s, where each node is responsible for -/// single JID. -class RosterManager { - /// Creates an instance for [RosterManager] with the provided [Whixp] instance - /// and several optional flags. - RosterManager( - /// The main [Whixp] instance. - this._whixp, { - /// Default autoAuthorize value for the new roster nodes - this.autoAuthorize = true, - - /// Default autoSubscribe value for the new roster nodes - this.autoSubscribe = true, - }) { - _whixp.transport.addFilter( - mode: FilterMode.out, - filter: _saveLastStatus, - ); - - _initializeDatabase(); - } - - final WhixpBase _whixp; - - /// Default autoAuthorize value for the new roster nodes. - final bool autoAuthorize; - - /// Default autoSubscribe value for the new roster nodes. - final bool autoSubscribe; - - /// Keeps [RosterNode] instances for this roster. - final _rosters = {}; - - Future _initializeDatabase() async { - await _HiveDatabase().initialize( - _rosterTable, - _whixp.provideHivePath ? _whixp.hivePathName : null, - ); - - final entries = _HiveDatabase().box.values; - - for (final node in _rosters.keys) { - await _rosters[node]!._setBackend(); - } - for (final entry in entries) { - for (final node - in entry.keys.where((key) => !_rosters.keys.contains(key)).toList()) { - add(node as String); - } - } - } - - /// Listens to the changes which occurs in the roster box. - void listenChanges( - FutureOr Function(BoxEvent event) onData, { - void Function(dynamic error, dynamic trace)? onError, - void Function()? onDone, - }) { - _HiveDatabase().listenable().listen( - onData, - onError: onError, - onDone: onDone, - ); - } - - StanzaBase _saveLastStatus(dynamic stanza) { - if (stanza is Presence) { - String subscribeFrom = (stanza['from'] as String).isEmpty - ? _whixp.transport.boundJID.toString() - : JabberID(stanza['from'] as String).full; - final subscribeTo = stanza['to'] as String; - - if (subscribeFrom.isEmpty) { - subscribeFrom = _whixp.transport.boundJID.toString(); - } - - if (stanza.showtypes.contains(stanza['type']) || - {'unavailable', 'available'}.contains(stanza['type'])) { - if (subscribeTo.isNotEmpty) { - ((this[subscribeFrom] as RosterNode)[JabberID(subscribeTo).full] - as RosterItem) - .lastStatus = stanza; - } else { - (this[subscribeFrom] as RosterNode).lastStatus = stanza; - } - } - } - - return stanza as StanzaBase; - } - - /// Returns the roster node for a JID. - /// - /// A new roster node will be created if one does not already exist. - dynamic operator [](String jid) { - final bare = JabberID(jid).bare; - - if (!_rosters.containsKey(bare)) { - add(bare); - _rosters[bare]!.autoAuthorize = autoAuthorize; - _rosters[bare]!.autoSubscribe = autoSubscribe; - } - - return _rosters[bare]; - } - - /// Returns the JIDs managed by the roster. - Iterable get keys => _rosters.keys; - - /// Adds a new roster node for the given JID. - void add(String node) { - if (!_rosters.containsKey(node)) { - _rosters[node] = RosterNode(_whixp, jid: node); - } - } - - /// Resets the state of the roster to forget any current [Presence] - /// information. - void reset() { - for (final node in _rosters.entries) { - (this[node.key] as RosterNode).reset(); - } - } - - /// Create, initialize, and send a [Presence] stanza. - /// - /// If no recipient is specified, send the presence immediately. Otherwise, - /// forward the send request to the recipient's roster entry for processing. - void sendPresence() { - JabberID? presenceFrom; - if (_whixp.isComponent) { - presenceFrom = _whixp.transport.boundJID; - } - _whixp.sendPresence(presenceFrom: presenceFrom); - } -} diff --git a/lib/src/roster/node.dart b/lib/src/roster/node.dart deleted file mode 100644 index b1f28d8..0000000 --- a/lib/src/roster/node.dart +++ /dev/null @@ -1,248 +0,0 @@ -part of 'manager.dart'; - -/// A roster node is a roster for a single [JabberID]. -class RosterNode { - /// Creates an instance of [RosterNode] with the specified [Whixp] instance - /// and [JabberID] which owns the Roster. - RosterNode(this._whixp, {required this.jid}); - - /// The main [WhixpBase] instance. Can be client or component. - final WhixpBase _whixp; - - /// The JID associated and owns the roster. - final String jid; - - /// The last sent [Presence] status that was broadcast to all contact JIDs. - Presence? lastStatus; - - /// The [RosterItem] items that this roster includes. - final _jids = {}; - - /// Determines how authorizations are handled. - bool autoAuthorize = true; - - /// Determines if bi-directional subscriptions are created after auto - /// authorizing a subscription request. - bool autoSubscribe = true; - - @internal - bool ignoreUpdates = false; - - /// Roster's version ID. - String version = 'ver1'; - - /// Return the roster item for a subscribed [JabberID]. - dynamic operator [](String key) { - final bare = JabberID(key).bare; - if (!_jids.containsKey(bare)) { - add(key, save: true); - } - return _jids[bare]; - } - - Future _setBackend() async { - final existingEntries = _jids; - final newEntries = _HiveDatabase().getJID(jid); - - final newJids = newEntries?.keys.toList(); - - for (final jid in existingEntries.keys) { - await _jids[jid]!._setBackend(); - } - if (newJids != null) { - for (final jid in existingEntries.keys - .where((element) => !newJids.contains(element)) - .toList()) { - add(jid); - } - } - } - - /// Checks rather there is data in the local storage or not. - bool get isLocalEmpty { - final items = _HiveDatabase().getJID(jid); - return items == null || items.isEmpty; - } - - /// Remove a roster item from the local. To remotely remove the item from the - /// roster use [remove] method instead. - void delete(String key) { - final bare = JabberID(key).bare; - if (_jids.containsKey(bare)) _jids.remove(bare); - } - - /// Returns a list of all subscribed JIDs in [String] format. - Iterable get keys => _jids.keys; - - /// Returns whether the roster has a JID. - bool hasJID(String jid) => _jids.containsKey(jid); - - /// Returns a [Map] of group names. - Map groups() { - final result = {}; - - for (final jid in _jids.entries) { - final groups = (_jids[jid.key]!)['groups'] as List; - if (groups.isEmpty) if (!result.containsKey('')) result[''] = jid.key; - for (final group in groups) { - if (result.containsKey(group)) result[group] = []; - result[group] = jid.key; - } - } - - return result; - } - - /// Adds a new [JabberID] to the roster. - void add( - /// The JID for the roster item - String jid, { - /// An alias for the JID - String name = '', - - /// A list of group names - List? groups, - - /// Indicates if the JID has a subscription state of 'from'. Defaults to - /// `false` - bool from = false, - - /// Indicates if the JID has a subscription state of 'to'. Defaults to - /// `false` - bool to = false, - - /// Indicates if the JID has sent a subscription request to this - /// connection's JID. Defaults to `false` - bool pendingIn = false, - - /// Indicates if a subscription request has been send to this JID. Defaults - /// to `false` - bool pendingOut = false, - - /// Indicates if a subscription request from this JID should be - /// automatically authorized. Defaults to `false` - bool whitelisted = false, - - /// Indicates if the item should persisted immediately to an external - /// datastore, if one is used. Defaults to `false` - bool save = false, - }) { - final bare = JabberID(jid).bare; - - final state = { - 'name': name, - 'groups': groups ?? [], - 'from': from, - 'to': to, - 'pending_in': pendingIn, - 'pending_out': pendingOut, - 'whitelisted': whitelisted, - 'subscription': 'none', - }; - - _jids[bare] = RosterItem( - _whixp, - jid: jid, - state: state, - roster: this, - ); - } - - /// Update a [JabberID]'s subscription information. - void subscribe(String jid) => (this[jid] as RosterItem).subscribe(); - - /// Unsubscribe from the [JabberID]. - void unsubscribe(String jid) => (this[jid] as RosterItem).unsubscribe(); - - /// Removes a [JabberID] from the roster (remote). - FutureOr remove(String jid) { - (this[jid] as RosterItem).remove(); - if (!_whixp.isComponent) return update(jid, subscription: 'remove'); - - return null; - } - - /// Update a [JabberID]'s roster information. - FutureOr update( - String jid, { - String? name, - String? subscription, - List? groups, - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) { - (this[jid] as RosterItem)['name'] = name; - (this[jid] as RosterItem)['groups'] = groups ?? []; - - if (!_whixp.isComponent) { - final iq = _whixp.makeIQSet(); - iq.registerPlugin(roster.Roster()); - (iq['roster'] as roster.Roster)['items'] = { - jid: { - 'name': name, - 'subscription': subscription, - 'groups': groups, - }, - }; - - return iq.sendIQ( - callback: callback, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - return Future.value(); - } - - /// Returns [Presence] information for a [JabberID]'s resources. - /// - /// May return either all online resources' status, or a single [resource]'s - /// status. - dynamic presence(String jid, {String? resource}) { - if (resource == null) return (this[jid] as RosterItem).resources; - - final defaultResource = { - 'status': '', - 'priority': 0, - 'show': '', - }; - - return (this[jid] as RosterItem).resources[resource] ?? defaultResource; - } - - /// Reset the state of the roster to forget any current presence information. - void reset() { - for (final jid in _jids.entries) { - (this[jid.key] as RosterItem).reset(); - } - } - - /// Shortcut for sending a [Presence] stanza. - /// - /// Create, initialize, and send a [Presence] stanza. - /// - /// If no recipient is specified, send the presence immediately. Otherwise, - /// forward the send request to the recipient's roster entry for processing. - void sendPresence() { - JabberID? presenceFrom; - if (_whixp.isComponent) presenceFrom = JabberID(jid); - _whixp.sendPresence(presenceFrom: presenceFrom); - } - - void sendLastPresence() { - if (lastStatus == null) { - sendPresence(); - } else { - final presence = lastStatus; - if (_whixp.isComponent) { - presence!['from'] = jid; - } else { - presence!.delete('from'); - } - presence.send(); - } - } -} diff --git a/lib/src/whixp.dart b/lib/src/whixp.dart index 5f3a88b..14fc770 100644 --- a/lib/src/whixp.dart +++ b/lib/src/whixp.dart @@ -1,31 +1,15 @@ import 'dart:async'; import 'dart:io' as io; -import 'package:crypto/crypto.dart'; -import 'package:dartz/dartz.dart'; -import 'package:meta/meta.dart'; - -import 'package:whixp/src/exception.dart'; +import 'package:whixp/src/database/controller.dart'; import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/features.dart'; -import 'package:whixp/src/roster/manager.dart' as rost; -import 'package:whixp/src/stanza/error.dart'; -import 'package:whixp/src/stanza/features.dart'; -import 'package:whixp/src/stanza/handshake.dart'; -import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stanza/message.dart'; -import 'package:whixp/src/stanza/presence.dart'; -import 'package:whixp/src/stanza/roster.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; +import 'package:whixp/src/session.dart'; +import 'package:whixp/src/stanza/mixins.dart'; import 'package:whixp/src/transport.dart'; -import 'package:whixp/src/utils/utils.dart'; +import 'package:whixp/whixp.dart'; -part 'client.dart'; -part 'component.dart'; +part '_extensions.dart'; +// part 'component.dart'; abstract class WhixpBase { /// Adapts the generic [Transport] class for use with XMPP. It also provides @@ -41,7 +25,7 @@ abstract class WhixpBase { int port = 5222, /// Jabber ID associated with the XMPP client - String jabberID = '', + String? jabberID, /// Default XML namespace, defualts to "client" String? defaultNamespace, @@ -63,7 +47,7 @@ abstract class WhixpBase { /// If `true`, periodically send a whitespace character over the wire to /// keep the connection alive - bool whitespaceKeepAlive = true, + bool pingKeepAlive = true, /// Optional [io.SecurityContext] which is going to be used in socket /// connections @@ -73,7 +57,7 @@ abstract class WhixpBase { /// /// Passes [io.X509Certificate] instance when returning boolean value which /// indicates to proceed on bad certificate or not. - bool Function(io.X509Certificate)? onBadCertificateCallback, + bool Function(io.X509Certificate cert)? onBadCertificateCallback, /// Represents the duration in milliseconds for which the system will wait /// for a connection to be established before raising a [TimeoutException]. @@ -81,23 +65,18 @@ abstract class WhixpBase { /// Defaults to 2000 milliseconds int connectionTimeout = 2000, - /// The maximum number of reconnection attempts that the [Transport] will - /// make in case the connection with the server is lost or cannot be - /// established initially. Defaults to 3 - int maxReconnectionAttempt = 3, - /// The maximum number of consecutive `see-other-host` redirections that /// will be followed before quitting int maxRedirects = 5, - /// The default interval between keepalive signals when [whitespaceKeepAlive] - /// is enabled. Represents in seconds. Defaults to `300` - int whitespaceKeepAliveInterval = 300, + /// The default interval between keepalive signals when + /// [pingKeepAliveInterval] is enabled. Represents in seconds. Defaults to `300` + int pingKeepAliveInterval = 300, /// [Log] instance to print out various log messages properly Log? logger, - this.hivePathName = 'whixp', - this.provideHivePath = false, + String internalDatabasePath = '/', + ReconnectionPolicy? reconnectionPolicy, }) { _streamNamespace = WhixpUtils.getNamespace('JABBER_STREAM'); @@ -105,14 +84,14 @@ abstract class WhixpBase { /// be used. _defaultNamespace = defaultNamespace ?? WhixpUtils.getNamespace('CLIENT'); - /// requested [JabberID] from the passed jabber ID. - requestedJID = JabberID(jabberID); + /// Requested [JabberID] from the passed jabber ID. + if (jabberID != null) _requestedJID = JabberID(jabberID); /// [JabberID] from the passed jabber ID. - final boundJID = JabberID(jabberID); - - /// Initialize [PluginManager]. - _pluginManager = PluginManager(); + JabberID? boundJID; + if (jabberID != null) { + boundJID = JabberID(jabberID); + } /// Equals passed maxRedirect count to the local variable. _maxRedirects = maxRedirects; @@ -123,10 +102,16 @@ abstract class WhixpBase { late String address; late String? dnsService; - if (!isComponent) { + if (host == null && boundJID == null) { + throw WhixpInternalException.setup( + 'You need to declare either host or jid to connect to the server.', + ); + } + + if (!_isComponent) { /// Check if this class is not used for component initialization, and try /// to point [host] and [port] properly. - if (host == null) { + if (host == null && boundJID != null) { address = boundJID.host; if (useTLS) { @@ -134,152 +119,143 @@ abstract class WhixpBase { } else { dnsService = 'xmpp-client'; } - } else { + } else if (host != null) { address = host; dnsService = null; } } else { - address = host ?? boundJID.host; + address = host ?? boundJID!.host; dnsService = null; } + /// Initialize internal used database for Whixp. + HiveController.initialize(internalDatabasePath); + /// Declare [Transport] with the passed params. - transport = Transport( + _transport = Transport( address, port: port, useIPv6: useIPv6, disableStartTLS: disableStartTLS, boundJID: boundJID, - isComponent: isComponent, dnsService: dnsService, useTLS: useTLS, context: context, onBadCertificateCallback: onBadCertificateCallback, connectionTimeout: connectionTimeout, - whitespaceKeepAlive: whitespaceKeepAlive, - whitespaceKeepAliveInterval: whitespaceKeepAliveInterval, - maxReconnectionAttempt: maxReconnectionAttempt, + pingKeepAlive: pingKeepAlive, + pingKeepAliveInterval: pingKeepAliveInterval, + reconnectionPolicy: + reconnectionPolicy ?? RandomBackoffReconnectionPolicy(0, 2), ); + /// Initialize PubSub instance + PubSub.initialize(); + /// Set up the transport with XMPP's root stanzas & handlers. - transport - ..startStreamHandler = ([attributes]) { - String streamVersion = ''; + _transport + ..startStreamHandler = (attributes) { + String? streamVersion; - for (final attribute in attributes!) { - if (attribute.localName == 'version') { + for (final attribute in attributes.entries) { + if (attribute.key == 'version') { streamVersion = attribute.value; - } else if (attribute.qualifiedName == 'xml:lang') { - transport.peerDefaultLanguage = attribute.value; + } else if (attribute.value == 'xml:lang') { + _transport.peerDefaultLanguage = attribute.value; } } - if (!isComponent && streamVersion.isEmpty) { - transport.emit('legacyProtocol'); + if (!_isComponent && (streamVersion?.isEmpty ?? true)) { + _transport.emit('legacyProtocol'); } } - ..registerStanza(IQ(generateID: false)) - ..registerStanza(Presence()) - ..registerStanza(Message(includeNamespace: true)) - ..registerStanza(StreamError()) - ..registerHandler( - CallbackHandler( - 'Presence', - _handlePresence, - matcher: XPathMatcher('{$_defaultNamespace}presence'), - ), - ) - ..registerHandler( - CallbackHandler( - 'Presence', - _handlePresence, - matcher: XPathMatcher('{null}presence'), - ), - ) - ..registerHandler( - CallbackHandler( - 'IM', - (stanza) => _handleMessage(stanza as Message), - matcher: XPathMatcher( - '{$_defaultNamespace}message/${_defaultNamespace}body', - ), - ), - ) - ..registerHandler( - CallbackHandler( - 'IMError', - (stanza) => _handleMessageError(stanza as Message), - matcher: XPathMatcher( - '{$defaultNamespace}message/${defaultNamespace}error', - ), - ), - ) + ..registerHandler(Handler('IM', _handleMessage)..packet('message')) + // ..registerHandler( + // Handler('Presence', _handlePresence), + // CallbackHandler( + // 'Presence', + // _handlePresence, + // matcher: XPathMatcher('{$_defaultNamespace}presence'), + // ), + // ) + // ..registerHandler( + // CallbackHandler( + // 'Presence', + // _handlePresence, + // matcher: XPathMatcher('{null}presence'), + // ), + // ) + // ..registerHandler( + // CallbackHandler( + // 'IMError', + // (stanza) => _handleMessageError(stanza as Message), + // matcher: XPathMatcher( + // '{$defaultNamespace}message/${defaultNamespace}error', + // ), + // ), + // ) ..registerHandler( - CallbackHandler( - 'Stream Error', - _handleStreamError, - matcher: XPathMatcher('{$_streamNamespace}error'), - ), + Handler('Stream Error', _handleStreamError)..packet('stream_error'), ); /// Initialize [RosterManager]. - roster = rost.RosterManager(this); + // roster = rost.RosterManager(this); /// Add current user jid to the roster. - roster.add(boundJID.toString()); + // roster.add(boundJID.toString()); /// Get current user's roster from the roster manager. - clientRoster = roster[boundJID.toString()] as rost.RosterNode; - - transport - ..addEventHandler('disconnected', (_) => _handleDisconnected()) - ..addEventHandler( - 'presenceDnd', - (presence) => _handleAvailable(presence!), - ) - ..addEventHandler( - 'presenceXa', - (presence) => _handleAvailable(presence!), - ) - ..addEventHandler( - 'presenceChat', - (presence) => _handleAvailable(presence!), - ) - ..addEventHandler( - 'presenceAway', - (presence) => _handleAvailable(presence!), - ) - ..addEventHandler( - 'presenceAvailable', - (presence) => _handleAvailable(presence!), - ) - ..addEventHandler( - 'presenceUnavailable', - (presence) => _handleUnavailable(presence!), - ) - ..addEventHandler( - 'presenceSubscribe', - (presence) => _handleSubscribe(presence!), - ) - ..addEventHandler( - 'presenceSubscribed', - (presence) => _handleSubscribed(presence!), - ) - ..addEventHandler( - 'presenceUnsubscribe', - (presence) => _handleUnsubscribe(presence!), - ) - ..addEventHandler( - 'presenceUnsubscribed', - (presence) => _handleUnsubscribed(presence!), - ) - ..addEventHandler( - 'rosterSubscriptionRequest', - (presence) => _handleNewSubscription(presence!), - ); + // clientRoster = roster[boundJID.toString()] as rost.RosterNode; + + // transport + // ..addEventHandler('disconnected', (_) => _handleDisconnected()) + // ..addEventHandler( + // 'presenceDnd', + // (presence) => _handleAvailable(presence!), + // ) + // ..addEventHandler( + // 'presenceXa', + // (presence) => _handleAvailable(presence!), + // ) + // ..addEventHandler( + // 'presenceChat', + // (presence) => _handleAvailable(presence!), + // ) + // ..addEventHandler( + // 'presenceAway', + // (presence) => _handleAvailable(presence!), + // ) + // ..addEventHandler( + // 'presenceAvailable', + // (presence) => _handleAvailable(presence!), + // ) + // ..addEventHandler( + // 'presenceUnavailable', + // (presence) => _handleUnavailable(presence!), + // ) + // ..addEventHandler( + // 'presenceSubscribe', + // (presence) => _handleSubscribe(presence!), + // ) + // ..addEventHandler( + // 'presenceSubscribed', + // (presence) => _handleSubscribed(presence!), + // ) + // ..addEventHandler( + // 'presenceUnsubscribe', + // (presence) => _handleUnsubscribe(presence!), + // ) + // ..addEventHandler( + // 'presenceUnsubscribed', + // (presence) => _handleUnsubscribed(presence!), + // ) + // ..addEventHandler( + // 'rosterSubscriptionRequest', + // (presence) => _handleNewSubscription(presence!), + // ); } - @internal - late final Transport transport; + + late final Transport _transport; /// Late final initialization of stream namespace. late final String _streamNamespace; @@ -288,8 +264,7 @@ abstract class WhixpBase { late final String _defaultNamespace; /// The JabberID (JID) requested for this connection. - @internal - late final JabberID requestedJID; + late final JabberID _requestedJID; /// The maximum number of consecutive `see-other-host` redirections that will /// be followed before quitting. @@ -300,119 +275,123 @@ abstract class WhixpBase { /// The sasl data keeper. Works with [SASL] class and keeps various data(s) /// that can be used accross package. - @internal - final saslData = {}; + final _saslData = {}; /// The distinction between clients and components can be important, primarily /// for choosing how to handle the `to` and `from` JIDs of stanzas. - final bool isComponent = false; + final bool _isComponent = false; - /// Hive properties. - final String hivePathName; - final bool provideHivePath; + /// Session initializer. + Session? _session; - @internal - Map credentials = {}; - - late final PluginManager _pluginManager; + /// Map holder for the given user properties for the connection. + Map _credentials = {}; /// [rost.RosterManager] instance to make communication with roster easier. - late final rost.RosterManager roster; - late rost.RosterNode clientRoster; - - final features = {}; + // late final rost.RosterManager roster; + // late rost.RosterNode clientRoster; final _streamFeatureHandlers = - Function(StanzaBase stanza), bool>>{}; + Function(Packet features), bool>>{}; final _streamFeatureOrder = >[]; - /// Register a stream feature handler. - void registerFeature( + /// Registers a stream feature handler. + void _registerFeature( String name, - FutureOr Function(StanzaBase stanza) handler, { + FutureOr Function(Packet features) handler, { bool restart = false, int order = 5000, }) { _streamFeatureHandlers[name] = Tuple2(handler, restart); _streamFeatureOrder.add(Tuple2(order, name)); - _streamFeatureOrder.sort((a, b) => a.value1.compareTo(b.value1)); + _streamFeatureOrder.sort((a, b) => a.firstValue.compareTo(b.firstValue)); } /// Unregisters a stream feature handler. - void unregisterFeature(String name, {int order = 5000}) { - if (_streamFeatureHandlers.containsKey(name)) { - _streamFeatureHandlers.remove(name); - } - _streamFeatureOrder.remove(Tuple2(order, name)); - _streamFeatureOrder.sort((a, b) => a.value1.compareTo(b.value1)); - } + // void _unregisterFeature(String name, {int order = 5000}) { + // if (_streamFeatureHandlers.containsKey(name)) { + // _streamFeatureHandlers.remove(name); + // } + // _streamFeatureOrder.remove(Tuple2(order, name)); + // _streamFeatureOrder.sort((a, b) => a.firstValue.compareTo(b.firstValue)); + // } /// Create, initialize, and send a new [Presence]. void sendPresence({ /// The recipient of a directed presence - JabberID? presenceTo, + JabberID? to, /// The sender of the presence - JabberID? presenceFrom, + JabberID? from, /// The presence's show value - String? presenceShow, + String? show, /// The presence's status message - String? presenceStatus, - - /// The connection's priority - String? presencePriority, + String? status, /// The type of presence, such as 'subscribe' - String? presenceType, + String? type, /// Optional nickname of the presence's sender - String? presenceNick, + String? nick, + + /// The connection's priority + int? priority, }) { - final presence = makePresence( - presenceTo: presenceTo, - presenceFrom: presenceFrom, - presenceShow: presenceShow, - presenceStatus: presenceStatus, - presencePriority: presencePriority, - presenceType: presenceType, - presenceNick: presenceNick, + final presence = _makePresence( + presenceTo: to, + presenceFrom: from, + presenceShow: show, + presenceStatus: status, + presencePriority: priority, + presenceType: type, + presenceNick: nick, ); - return presence.send(); + return Transport.instance().send(presence); } /// Creates, initializes and sends a new [Message]. void sendMessage( /// The recipient of a directed message - JabberID messageTo, { + JabberID to, { /// The contents of the message - String? messageBody, + String? body, /// Optional subject for the message - String? messageSubject, + String? subject, /// The message's type, defaults to [MessageType.chat] - MessageType messageType = MessageType.chat, + MessageType type = MessageType.chat, /// The sender of the presence - JabberID? messageFrom, + JabberID? from, /// Optional nickname of the message's sender - String? messageNick, + String? nick, + + /// List of custom extensions for the message stanza + List? extensions, + + /// List of payloads to be inserted + List? payloads, }) => - makeMessage( - messageTo, - messageBody: messageBody, - messageSubject: messageSubject, - messageType: messageType, - messageFrom: messageFrom, - messageNick: messageNick, - ).send(); + Transport.instance().send( + _makeMessage( + to, + messageBody: body, + messageSubject: subject, + messageType: type, + messageFrom: from, + messageNick: nick, + extensions: extensions, + payloads: payloads, + ), + ); /// Create and initialize a new [Presence] stanza. - Presence makePresence({ + Presence _makePresence({ /// The recipient of a directed presence JabberID? presenceTo, @@ -425,29 +404,26 @@ abstract class WhixpBase { /// The presence's status message String? presenceStatus, - /// The connection's priority - String? presencePriority, - /// The type of presence, such as 'subscribe' String? presenceType, /// Optional nickname of the presence's sender String? presenceNick, + + /// The connection's priority + int? presencePriority, }) { final presence = _presence( presenceType: presenceType, presenceTo: presenceTo, presenceFrom: presenceFrom, + presenceShow: presenceShow, + presencePriority: presencePriority, + presenceStatus: presenceStatus, ); - if (presenceShow != null) { - presence['type'] = presenceShow; - } - if (presenceFrom != null && transport.isComponent) { - presence['from'] = transport.boundJID.full; + if (presenceFrom != null && _isComponent) { + presence.from = _session!.bindJID; } - presence['priority'] = presencePriority; - presence['status'] = presenceStatus; - presence['nick'] = presenceNick; return presence; } @@ -458,25 +434,22 @@ abstract class WhixpBase { String? presenceType, String? presenceShow, String? presenceStatus, - String? presencePriority, String? presenceNick, + int? presencePriority, }) { final presence = Presence( - transport: transport, - stanzaType: presenceType, - stanzaTo: presenceTo, - stanzaFrom: presenceFrom, - ); - if (presenceShow != null) { - presence['type'] = presenceShow; - } - if (presenceFrom != null && isComponent) { - presence['from'] = transport.boundJID.full; + show: presenceShow, + priority: presencePriority, + status: presenceStatus, + nick: presenceNick, + ) + ..to = presenceTo + ..from = presenceFrom + ..type = presenceType; + if (presenceFrom != null && _isComponent) { + presence.from = _session!.bindJID; } - presence['priority'] = presencePriority; - presence['status'] = presenceStatus; - presence['nick'] = presenceNick; - presence['lang'] = transport.defaultLanguage; + return presence; } @@ -487,238 +460,222 @@ abstract class WhixpBase { ///
[messageSubject] is an optional subject for the message. /// Take a look at the [MessageType] enum for the message types. ///
[messageNick] is an optional nickname for the sender. - Message makeMessage( + Message _makeMessage( JabberID messageTo, { String? messageBody, String? messageSubject, MessageType messageType = MessageType.chat, JabberID? messageFrom, String? messageNick, + List? extensions, + List? payloads, }) { - final message = Message( - stanzaTo: messageTo, - stanzaFrom: messageFrom, - stanzaType: messageType.name, - transport: transport, - ); - message['body'] = messageBody; - message['subject'] = messageSubject; - if (messageNick != null) { - message['nick'] = messageNick; + final message = + Message(body: messageBody, subject: messageSubject, nick: messageNick) + ..to = messageTo + ..from = messageFrom + ..type = messageType.name; + + if (extensions?.isNotEmpty ?? false) { + for (final extension in extensions!) { + message.addExtension(extension); + } } - return message; - } - /// Creates a stanza of type `get`. - IQ makeIQGet({ - IQ? iq, - String? queryXMLNS, - JabberID? iqTo, - JabberID? iqFrom, - }) { - iq ??= IQ(transport: transport); - iq['type'] = 'get'; - iq['query'] = queryXMLNS; - if (iqTo != null) { - iq['to'] = iqTo; - } - if (iqFrom != null) { - iq['from'] = iqFrom; + if (payloads?.isNotEmpty ?? false) { + for (final payload in payloads!) { + message.addPayload(payload); + } } - return iq; + return message; } + /// Creates a stanza of type `get`. + // IQ makeIQGet({ + // IQ? iq, + // String? queryXMLNS, + // JabberID? iqTo, + // JabberID? iqFrom, + // }) { + // iq ??= IQ(); + // iq.type = StanzaType.get; + // iq.query = queryXMLNS; + + // if (iqTo != null) iq.to = iqTo; + // if (iqFrom != null) iq.from = iqFrom; + + // return iq; + // } + /// Creates a stanza of type `set`. /// /// Optionally, a substanza may be given to use as the stanza's payload. - IQ makeIQSet({IQ? iq, JabberID? iqTo, JabberID? iqFrom, dynamic sub}) { - iq ??= IQ(transport: transport); - iq['type'] = 'set'; - if (sub != null) { - iq.add(sub); - } - if (iqTo != null) { - iq['to'] = iqTo; - } - if (iqFrom != null) { - iq['from'] = iqFrom; - } + // IQ makeIQSet({IQ? iq, JabberID? iqTo, JabberID? iqFrom, dynamic sub}) { + // iq ??= IQ(); + // iq.type = StanzaType.set; - return iq; - } + // if (sub != null) iq.add(sub); + // if (iqTo != null) iq.to = iqTo; + // if (iqFrom != null) iq.from = iqFrom; - /// Request the roster from the server. - void getRoster({ - FutureOr Function(IQ iq)? callback, - FutureOr Function(StanzaError error)? failureCallback, - FutureOr Function()? timeoutCallback, - int timeout = 10, - }) { - final iq = makeIQGet(); + // return iq; + // } - if (features.contains('rosterver')) { - (iq['roster'] as Roster)['ver'] = clientRoster.version; - } - - iq.sendIQ( - callback: (iq) { - transport.emit('rosterUpdate', data: iq); - callback?.call(iq); - }, - failureCallback: failureCallback, - timeoutCallback: timeoutCallback, - timeout: timeout, - ); - } - - /// Registers and configures a [PluginBase] instance to use in this stream. - void registerPlugin(PluginBase plugin) { - if (!_pluginManager.registered(plugin.name)) { - _pluginManager.register(plugin.name, plugin); - - /// Assign the instance of this class to the [plugin]. - plugin.base = this; - } - _pluginManager.enable(plugin.name, enabled: _pluginManager.enabledPlugins); - } - - /// Responsible for retrieving an instance of a specified type [T] which - /// extends [PluginBase] from the plugin registry. - /// - /// Optionally, it can activate the plugin if it is registered but not yet - /// active. - P? getPluginInstance

(String name, {bool enableIfRegistered = true}) => - _pluginManager.getPluginInstance

(name); + /// Request the roster from the server. + // void getRoster({ + // FutureOr Function(IQ iq)? callback, + // FutureOr Function(StanzaError error)? failureCallback, + // FutureOr Function()? timeoutCallback, + // int timeout = 10, + // }) { + // final iq = makeIQGet(); + + // if (features.contains('rosterver')) { + // (iq['roster'] as Roster)['ver'] = clientRoster.version; + // } + + // iq.sendIQ( + // callback: (iq) { + // transport.emit('rosterUpdate', data: iq); + // callback?.call(iq); + // }, + // failureCallback: failureCallback, + // timeoutCallback: timeoutCallback, + // timeout: timeout, + // ); + // } /// Close the XML stream and wait for ack from the server. /// - /// Calls the primary method from [transport]. - Future disconnect() => transport.disconnect(); + /// Calls the primary method from [Transport]. + Future disconnect({bool consume = true}) => + Transport.instance().disconnect(consume: consume); - /// Process incoming message stanzas. - void _handleMessage(Message message) { + // /// Process incoming message stanzas. + void _handleMessage(Packet message) { + if (message is! Message) return; final to = message.to; - if (!transport.isComponent && (to != null || to!.bare.isEmpty)) { - message['to'] = transport.boundJID.toString(); - } - - transport.emit('message', data: message); - } - - /// Handles error occured while messaging - void _handleMessageError(Message message) { - if (!isComponent && (message.to == null || message.to!.bare.isEmpty)) { - message.setTo(transport.boundJID.toString()); + if (!_isComponent && (to != null || to!.bare.isEmpty)) { + message.to = transport.boundJID; } transport.emit('message', data: message); } - void _handlePresence(StanzaBase stanza) { - final presence = Presence(element: stanza.element); - - if (((roster[presence['from'] as String]) as rost.RosterNode) - .ignoreUpdates) { - return; - } - - if (!isComponent && JabberID(presence['to'] as String).bare.isNotEmpty) { - presence['to'] = transport.boundJID.toString(); - } - - transport.emit('presence', data: presence); - transport.emit( - 'presence${(presence['type'] as String).capitalize()}', - data: presence, - ); - - if ({'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'} - .contains(presence['type'])) { - transport.emit('changedSubscription', data: presence); - return; - } else if (!{'available', 'unavailable'}.contains(presence['type'])) { - return; - } - } - - void _handleDisconnected() { - roster.reset(); - transport.sessionBind = false; - } - - void _handleAvailable(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleAvailable(presence); - } - - void _handleUnavailable(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleUnavailable(presence); - } - - void _handleSubscribe(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleSubscribe(presence); - } - - void _handleSubscribed(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleSubscribed(presence); - } - - void _handleUnsubscribe(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleUnsubscribe(presence); - } - - void _handleUnsubscribed(Presence presence) { - ((roster[presence['to'] as String] - as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) - .handleUnsubscribed(presence); - } - - /// Attempt to automatically handle subscription requests. - /// - /// Subscriptions will be approved if the request is from a whitelisted JID, - /// of `autoAuthorize` is true. - void _handleNewSubscription(Presence presence) { - final roster = this.roster[presence['to'] as String] as rost.RosterNode; - final rosterItem = roster[presence['from'] as String] as rost.RosterItem; - if (rosterItem['whitelisted'] as bool) { - rosterItem.authorize(); - if (roster.autoAuthorize) { - rosterItem.subscribe(); - } - } else if (roster.autoAuthorize) { - rosterItem.authorize(); - if (roster.autoSubscribe) { - rosterItem.subscribe(); - } - } else if (!roster.autoAuthorize) { - rosterItem.unauthorize(); - } - } - - void _handleStreamError(StanzaBase error) { - transport.emit('streamError', data: error as StreamError); - - if (error['condition'] == 'see-other-host') { - final otherHost = error['see-other-host'] as String?; + // /// Handles error occured while messaging + // void _handleMessageError(Message message) { + // if (!isComponent && (message.to == null || message.to!.bare.isEmpty)) { + // message.to = transport.boundJID; + // } + + // transport.emit('message', data: message); + // } + + // void _handlePresence(Stanza stanza) { + // final presence = Presence(xml: stanza.xml); + + // if (((roster[presence.from as String]) as rost.RosterNode).ignoreUpdates) { + // return; + // } + + // if (!isComponent && (presence.to?.bare.isNotEmpty ?? false)) { + // presence.to = transport.boundJID; + // } + + // transport.emit('presence', data: presence); + // transport.emit( + // 'presence${presence.typeRaw.capitalize()}', + // data: presence, + // ); + + // if ({'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'} + // .contains(presence.typeRaw)) { + // transport.emit('changedSubscription', data: presence); + // return; + // } else if (!{'available', 'unavailable'}.contains(presence.typeRaw)) { + // return; + // } + // } + + // void _handleDisconnected() { + // roster.reset(); + // transport.sessionBind = false; + // } + + // void _handleAvailable(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleAvailable(presence); + // } + + // void _handleUnavailable(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleUnavailable(presence); + // } + + // void _handleSubscribe(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleSubscribe(presence); + // } + + // void _handleSubscribed(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleSubscribed(presence); + // } + + // void _handleUnsubscribe(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleUnsubscribe(presence); + // } + + // void _handleUnsubscribed(Presence presence) { + // ((roster[presence['to'] as String] + // as rost.RosterNode)[presence['from'] as String] as rost.RosterItem) + // .handleUnsubscribed(presence); + // } + + // /// Attempt to automatically handle subscription requests. + // /// + // /// Subscriptions will be approved if the request is from a whitelisted JID, + // /// of `autoAuthorize` is true. + // void _handleNewSubscription(Presence presence) { + // final roster = this.roster[presence['to'] as String] as rost.RosterNode; + // final rosterItem = roster[presence['from'] as String] as rost.RosterItem; + // if (rosterItem['whitelisted'] as bool) { + // rosterItem.authorize(); + // if (roster.autoAuthorize) { + // rosterItem.subscribe(); + // } + // } else if (roster.autoAuthorize) { + // rosterItem.authorize(); + // if (roster.autoSubscribe) { + // rosterItem.subscribe(); + // } + // } else if (!roster.autoAuthorize) { + // rosterItem.unauthorize(); + // } + // } + + void _handleStreamError(Packet error) { + if (error is! StreamError) return; + _transport.emit('streamError', data: error); + + if (error.seeOtherHost) { + final otherHost = error.text; if (otherHost == null || otherHost.isEmpty) { _logger.warning('No other host specified'); return; } - transport.handleStreamError(otherHost, maxRedirects: _maxRedirects); + _transport.handleStreamError(otherHost, maxRedirects: _maxRedirects); } else { - transport.disconnect(reason: 'System shutted down', timeout: 0); + _transport.disconnect(consume: false); } } @@ -729,10 +686,10 @@ abstract class WhixpBase { FutureOr Function(B? data) handler, { bool once = false, }) => - transport.addEventHandler(event, handler, once: once); + _transport.addEventHandler(event, handler, once: once); /// Password from credentials. - String get password => credentials['password']!; + String get password => _credentials['password']!; } extension StringExtension on String { diff --git a/lib/whixp.dart b/lib/whixp.dart index dcafd19..1bc9be4 100644 --- a/lib/whixp.dart +++ b/lib/whixp.dart @@ -9,7 +9,6 @@ export 'src/jid/jid.dart'; export 'src/log/log.dart'; export 'src/plugins/plugins.dart'; export 'src/reconnection.dart'; -export 'src/roster/manager.dart'; export 'src/stanza/atom.dart'; export 'src/stanza/error.dart'; export 'src/stanza/iq.dart'; From b34f30ac0322d4265334bfb85733194919b695b7 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:11:19 +0400 Subject: [PATCH 78/81] refactor: Remove unused files --- lib/src/component.dart | 108 ------------------- lib/src/stanza/atom.dart | 63 ----------- lib/src/stanza/handshake.dart | 43 -------- lib/src/stanza/roster.dart | 193 ---------------------------------- 4 files changed, 407 deletions(-) delete mode 100644 lib/src/component.dart delete mode 100644 lib/src/stanza/atom.dart delete mode 100644 lib/src/stanza/handshake.dart delete mode 100644 lib/src/stanza/roster.dart diff --git a/lib/src/component.dart b/lib/src/component.dart deleted file mode 100644 index 86b7791..0000000 --- a/lib/src/component.dart +++ /dev/null @@ -1,108 +0,0 @@ -part of 'whixp.dart'; - -/// The wire component protocol in use today enables an external component to -/// connect to a server (with proper configuration and authentication) and to -/// send and receive XML stanzas through the server. There are two connection -/// methods: "accept" and "connect". When the "accept" method is used, the -/// server waits for connections from components and accepts them when they are -/// initiated by a component. When the "connect" method is used, the server -/// initiates the connection to a component. -/// -/// see -class WhixpComponent extends WhixpBase { - /// Basic XMPP server component. - /// - /// An external component is called "trusted" because it authenticates with a - /// server using authentication credentials that include a shared [secret]. - WhixpComponent( - String jabberID, { - required String secret, - super.host, - super.port, - super.useTLS, - super.disableStartTLS, - super.connectionTimeout, - super.maxReconnectionAttempt, - super.onBadCertificateCallback, - super.context, - super.logger, - super.hivePathName, - super.provideHivePath, - bool useClientNamespace = false, - }) : super(jabberID: jabberID, whitespaceKeepAlive: false) { - if (useClientNamespace) { - transport.defaultNamespace = WhixpUtils.getNamespace('CLIENT'); - } else { - transport.defaultNamespace = WhixpUtils.getNamespace('COMPONENT'); - } - - transport - ..streamHeader = - '' - ..streamFooter = '' - ..sessionStarted = false - ..startStreamHandler = ([attributes]) { - if (attributes == null) return; - for (final attribute in attributes) { - if (attribute.name == 'id') { - final sid = attribute.value; - final prehash = WhixpUtils.stringToArrayBuffer('$sid$secret'); - - final handshake = Handshake(); - handshake['value'] = sha1.convert(prehash).toString().toLowerCase(); - - transport.send(handshake); - break; - } - } - }; - - transport - ..registerHandler( - CallbackHandler( - 'Handshake with Component namespace', - _handleHandshake, - matcher: XPathMatcher( - '{${WhixpUtils.getNamespace('COMPONENT')}}handshake', - ), - ), - ) - ..registerHandler( - CallbackHandler( - 'Handshake with Stream namespace', - _handleHandshake, - matcher: XPathMatcher( - '{${WhixpUtils.getNamespace('JABBER_STREAM')}}handshake', - ), - ), - ) - ..registerHandler( - CallbackHandler( - 'Handshake without namespace', - _handleHandshake, - matcher: XPathMatcher('handshake'), - ), - ); - transport.addEventHandler('presenceProbe', _handleProbe); - } - - @override - bool get isComponent => true; - - /// Connects to the server. - void connect() => transport.connect(); - - /// The Handshake has been accepted. - void _handleHandshake(StanzaBase? stanza) => transport - ..sessionBind = true - ..sessionStarted = true - ..emit('sessionBind', data: transport.boundJID) - ..emit('sessionStart'); - - void _handleProbe(Presence? presence) { - if (presence == null) return; - return ((roster[presence.to.toString()] - as rost.RosterNode)[presence.from.toString()] as rost.RosterItem) - .handleProbe(); - } -} diff --git a/lib/src/stanza/atom.dart b/lib/src/stanza/atom.dart deleted file mode 100644 index a985f01..0000000 --- a/lib/src/stanza/atom.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -/// A simple Atom feed entry. -/// -/// Atom syndication format: -///
-class AtomEntry extends XMLBase { - AtomEntry({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.element, - super.parent, - }) : super( - name: 'entry', - namespace: WhixpUtils.getNamespace('ATOM'), - pluginAttribute: 'entry', - interfaces: { - 'title', - 'summary', - 'id', - 'published', - 'updated', - }, - subInterfaces: { - 'title', - 'summary', - 'id', - 'published', - 'updated', - }, - ) { - registerPlugin(AtomAuthor()); - } - - @override - AtomEntry copy({xml.XmlElement? element, XMLBase? parent}) => AtomEntry( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - element: element, - parent: parent, - ); -} - -/// An Atom author. -class AtomAuthor extends XMLBase { - AtomAuthor({super.element, super.parent}) - : super( - name: 'author', - includeNamespace: false, - pluginAttribute: 'author', - interfaces: {'name', 'uri'}, - subInterfaces: {'name', 'uri'}, - ); - - @override - AtomAuthor copy({xml.XmlElement? element, XMLBase? parent}) => AtomAuthor( - element: element, - parent: parent, - ); -} diff --git a/lib/src/stanza/handshake.dart b/lib/src/stanza/handshake.dart deleted file mode 100644 index 05f7fb4..0000000 --- a/lib/src/stanza/handshake.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart'; - -/// External source for expanded version of the given stanza's purpose: -/// https://xmpp.org/extensions/xep-0114.html -/// -/// The main difference between the **jabber:component:** namespaces and the -/// **jabber:client** or **jabber:server** `namespace` is authentication. -/// -/// This stanza uses element to specify credentials for the -/// component's session with the server. -/// -/// Component sends this stanza: -/// ```xml -/// -/// ``` -/// -/// `to` identifier in this case refers to component name, not the server name. -class Handshake extends StanzaBase { - Handshake() - : super( - name: 'handshake', - namespace: WhixpUtils.getNamespace('COMPONENT'), - interfaces: { - 'value', - }, - setters: { - const Symbol('value'): (value, args, base) => - base.element?.innerText = value as String, - }, - getters: { - const Symbol('value'): (args, base) => base.element?.innerText, - }, - deleters: { - const Symbol('value'): (args, base) => base.element?.innerText = '', - }, - ); -} diff --git a/lib/src/stanza/roster.dart b/lib/src/stanza/roster.dart deleted file mode 100644 index 208cc4d..0000000 --- a/lib/src/stanza/roster.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/utils/utils.dart'; - -import 'package:xml/xml.dart' as xml; - -/// ### Example: -/// ```xml -/// -/// -/// -/// hokkabazlar -/// -/// -/// -/// ``` -class Roster extends XMLBase { - /// The [Roster] class provides functionality for handling XMPP roster-related - /// queries. - /// - /// ### Example: - /// ```dart - /// final iq = IQ(); - /// final roster = Roster(); - /// iq.registerPlugin(roster); - /// (iq['roster'] as XMLBase)['items'] = { - /// 'vsevex@example.com': { - /// 'name': 'Vsevolod', - /// 'subscription': 'both', - /// 'groups': ['cart', 'hella'], - /// }, - /// 'alyosha@example.com': { - /// 'name': 'Alyosha', - /// 'subscription': 'both', - /// 'groups': ['gup'], - /// }, - /// }; /// ...sets items of the [Roster] stanza in the IQ stanza - /// ``` - Roster({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'query', - namespace: WhixpUtils.getNamespace('ROSTER'), - pluginAttribute: 'roster', - interfaces: {'items', 'ver'}, - ) { - addGetters( - { - const Symbol('ver'): (args, base) => base.getAttribute('ver'), - const Symbol('items'): (args, base) { - final items = >{}; - for (final item in base['substanzas'] as List) { - if (item is RosterItem) { - items[item['jid'] as String] = item.values; - } - } - return items; - }, - }, - ); - - addSetters({ - const Symbol('ver'): (value, args, base) { - if (value != null) { - base.element!.setAttribute('ver', value as String); - } - }, - const Symbol('items'): (value, args, base) { - delete('items'); - for (final jid in (value as Map).entries) { - final item = RosterItem(); - item.values = value[jid.key] as Map; - item['jid'] = jid.key; - base.add(item); - } - return; - }, - }); - - addDeleters( - { - const Symbol('items'): (args, base) { - for (final item in base['substanzas'] as List) { - if (item is RosterItem) { - base.element!.children.remove(item.element); - } - } - }, - }, - ); - - registerPlugin(RosterItem(), iterable: true); - } - - @override - Roster copy({xml.XmlElement? element, XMLBase? parent}) => Roster( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} - -/// Represents an individual roster item within the roster query. -class RosterItem extends XMLBase { - /// ### Example: - /// ```dart - /// final roster = Roster(); - /// final item = RosterItem(); - /// roster.registerPlugin(item); - /// ``` - RosterItem({ - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'item', - namespace: WhixpUtils.getNamespace('ROSTER'), - includeNamespace: false, - pluginAttribute: 'item', - interfaces: { - 'jid', - 'name', - 'subscription', - 'ask', - 'approved', - 'groups', - }, - ) { - addGetters({ - const Symbol('jid'): (args, base) => base.getAttribute('jid'), - const Symbol('groups'): (args, base) { - final groups = []; - for (final group in base.element!.findAllElements('group')) { - if (group.innerText.isNotEmpty) { - groups.add(group.innerText); - } - } - - return groups; - }, - }); - - addSetters({ - const Symbol('jid'): (value, args, base) => - base.setAttribute('jid', JabberID(value as String).toString()), - const Symbol('groups'): (value, args, base) { - base.delete('groups'); - for (final groupName in value as List) { - final group = WhixpUtils.xmlElement('group'); - group.innerText = groupName as String; - base.element!.children.add(group); - } - }, - }); - - addDeleters( - { - const Symbol('groups'): (args, base) { - for (final group - in base.element!.findAllElements('group', namespace: namespace)) { - base.element!.children.remove(group); - } - return; - }, - }, - ); - } - - @override - RosterItem copy({xml.XmlElement? element, XMLBase? parent}) => RosterItem( - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); -} From 904a175a6c39d266ca0ae44c31b791d877004ade Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:12:05 +0400 Subject: [PATCH 79/81] refactor: Remove unused 'compression.dart' and 'stanza.dart' files --- lib/src/plugins/compression/compression.dart | 75 ----------------- lib/src/plugins/compression/stanza.dart | 84 -------------------- 2 files changed, 159 deletions(-) delete mode 100644 lib/src/plugins/compression/compression.dart delete mode 100644 lib/src/plugins/compression/stanza.dart diff --git a/lib/src/plugins/compression/compression.dart b/lib/src/plugins/compression/compression.dart deleted file mode 100644 index d475ea3..0000000 --- a/lib/src/plugins/compression/compression.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:whixp/src/handler/handler.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/stanza/features.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; - -import 'package:xml/xml.dart' as xml; - -part 'stanza.dart'; - -class Compression extends PluginBase { - Compression() - : super( - 'compression', - description: 'XEP-0138: Compression', - dependencies: {'disco'}, - ); - - late final Map _compressionMethods; - - @override - void pluginInitialize() { - _compressionMethods = {'zlib': true}; - - base.transport - ..registerStanza(Compress()) - ..registerStanza(Compressed()) - ..registerHandler( - CallbackHandler( - 'Compressed', - _handleCompressed, - matcher: - XPathMatcher('{http://jabber.org/protocol/compress}compressed'), - ), - ); - - base.registerFeature( - 'compression', - (stanza) => _handleCompression(stanza as StreamFeatures), - restart: true, - order: 101, - ); - } - - bool _handleCompression(StreamFeatures features) { - for (final method in (features['compression'] - as CompressionStanza)['methods'] as Set) { - if (_compressionMethods.containsKey(method)) { - Log.instance.info('Attempting to use $method compression'); - final compress = Compress(); - compress.transport = base.transport; - compress['method'] = method; - compress.send(); - return true; - } - } - return false; - } - - void _handleCompressed(StanzaBase stanza) { - base.features.add('compression'); - Log.instance.info('Stream Compressed!!!'); - base.transport.streamCompressed = true; - base.transport.sendRaw(base.transport.streamHeader); - } - - /// Do not implement. - @override - void sessionBind(String? jid) {} - - /// Do not implement. - @override - void pluginEnd() {} -} diff --git a/lib/src/plugins/compression/stanza.dart b/lib/src/plugins/compression/stanza.dart deleted file mode 100644 index ab66f76..0000000 --- a/lib/src/plugins/compression/stanza.dart +++ /dev/null @@ -1,84 +0,0 @@ -part of 'compression.dart'; - -const _$namespace = 'http://jabber.org/features/compress'; -const _$protocolNamespace = 'http://jabber.org/protocol/compress'; - -class CompressionStanza extends XMLBase { - CompressionStanza({super.getters, super.element, super.parent}) - : super( - name: 'compression', - namespace: _$namespace, - interfaces: {'methods'}, - pluginAttribute: 'compression', - pluginTagMapping: {}, - pluginAttributeMapping: {}, - ) { - addGetters({ - const Symbol('methods'): (args, base) => methods, - }); - } - - Set get methods { - late final methods = {}; - for (final method - in element!.findAllElements('method', namespace: namespace)) { - methods.add(method.innerText); - } - - return methods; - } - - @override - CompressionStanza copy({xml.XmlElement? element, XMLBase? parent}) => - CompressionStanza( - getters: getters, - element: element, - parent: parent, - ); -} - -class Compress extends StanzaBase { - Compress({super.element, super.parent}) - : super( - name: 'compress', - namespace: _$protocolNamespace, - interfaces: {'method'}, - subInterfaces: {'method'}, - pluginAttribute: 'compress', - pluginTagMapping: {}, - pluginAttributeMapping: {}, - ); - - @override - Compress copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Compress( - element: element, - parent: parent, - ); -} - -class Compressed extends StanzaBase { - Compressed({super.element, super.parent}) - : super( - name: 'compressed', - namespace: _$namespace, - interfaces: {}, - pluginTagMapping: {}, - pluginAttributeMapping: {}, - ); - - @override - Compressed copy({ - xml.XmlElement? element, - XMLBase? parent, - bool receive = false, - }) => - Compressed( - element: element, - parent: parent, - ); -} From b5afcbac3ac74c25cf21b9c6d1072fae8f2adaea Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:12:38 +0400 Subject: [PATCH 80/81] chore: update ad-hoc commands with specific methods, not ready to be used --- lib/src/plugins/command/command.dart | 596 ++++----------------------- lib/src/plugins/command/stanza.dart | 240 +++++------ 2 files changed, 173 insertions(+), 663 deletions(-) diff --git a/lib/src/plugins/command/command.dart b/lib/src/plugins/command/command.dart index e10f3ee..1f95604 100644 --- a/lib/src/plugins/command/command.dart +++ b/lib/src/plugins/command/command.dart @@ -1,500 +1,80 @@ -import 'dart:async'; +import 'dart:async' as async; -import 'package:dartz/dartz.dart'; -import 'package:hive/hive.dart'; - -import 'package:whixp/src/exception.dart'; -import 'package:whixp/src/handler/handler.dart'; +import 'package:whixp/src/_static.dart'; import 'package:whixp/src/jid/jid.dart'; -import 'package:whixp/src/log/log.dart'; -import 'package:whixp/src/plugins/base.dart'; -import 'package:whixp/src/plugins/plugins.dart'; +import 'package:whixp/src/stanza/error.dart'; import 'package:whixp/src/stanza/iq.dart'; -import 'package:whixp/src/stream/base.dart'; -import 'package:whixp/src/stream/matcher/matcher.dart'; +import 'package:whixp/src/stanza/stanza.dart'; import 'package:whixp/src/utils/utils.dart'; -import 'package:whixp/src/whixp.dart'; import 'package:xml/xml.dart' as xml; -part '_database.dart'; +// part '_database.dart'; part 'stanza.dart'; -const _commandTable = 'commands'; +// const _commandTable = 'commands'; -typedef _Handler = FutureOr Function( - IQ iq, - Map? session, [ - dynamic results, -]); +// typedef _Handler = FutureOr Function( +// IQ iq, +// Map? session, [ +// dynamic results, +// ]); +// ignore: avoid_classes_with_only_static_members /// XMPP's Adhoc Commands provides a generic workflow mechanism for interacting /// with applications. /// /// see -class AdHocCommands extends PluginBase { - /// Events: - /// - /// * `execute`: received a command with action "execute" - /// * `next`: received a command with action "next" - /// * `complete`: received a command with action "complete" - /// * `cancel`: received a command with action "cancel" - AdHocCommands() - : super( - 'commands', - description: 'XEP-0050: Ad-Hoc Commands', - dependencies: {'disco', 'forms'}, - ); - - late final Map> _sessions; - late final Map, Tuple2> _commands; - final _commandNamespace = Command().namespace; - - @override - void pluginInitialize() { - _setBackend(); - - _sessions = >{}; - _commands = , Tuple2>{}; - - base.transport - ..registerHandler( - CallbackHandler( - 'Ad-Hoc Execute', - (stanza) => _handleCommand(stanza as IQ), - matcher: StanzaPathMatcher('iq@type=set/command'), - ), - ) - ..registerHandler( - CallbackHandler( - 'Ad-Hoc Result', - (stanza) => _handleCommandResult(stanza as IQ), - matcher: StanzaPathMatcher('iq@type=result/command'), - ), - ) - ..addEventHandler('command', _handleAllCommands); - } - - Future _setBackend() async { - await _HiveDatabase().initialize( - _commandTable, - base.provideHivePath ? base.hivePathName : null, - ); - } +class AdHocCommands { + const AdHocCommands(); - /// Makes a command available to external entities. - /// - /// Access control may be implemented in the provided handler. - void addCommand({ - JabberID? jid, - String? node, - String name = '', - _Handler? handler, - }) { - jid ??= base.transport.boundJID; - final itemJid = jid.full; - - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco - ..addIdentity( - category: 'automation', - type: 'command-list', - name: 'Ad-Hoc commands', - node: _commandNamespace, - jid: jid, - ) - ..addItem( - jid: itemJid, - name: name, - node: _commandNamespace, - subnode: node, - itemJid: jid, - ) - ..addIdentity( - category: 'automation', - type: 'command-node', - name: name, - node: node, - jid: jid, - ) - ..addFeature(_commandNamespace, jid: jid); - } - - _commands[Tuple2(itemJid, node)] = Tuple2(name, handler); - } - - /// Emits command events based on the command action. - void _handleCommand(IQ iq) => base.transport - ..emit('command', data: iq) - ..emit( - 'command${((iq['command'] as Command)['action'] as String).capitalize()}', - data: iq, - ); - - Future _handleAllCommands(IQ? iq) async { - if (iq == null) return; - final command = iq['command'] as Command; - final action = command['action'] as String; - final sessionID = command['sessionid'] as String; - final session = _HiveDatabase().getSession(sessionID); - - if (session == null) { - return _handleCommandStart(iq); - } - - if ({'next', 'execute'}.contains(action)) { - return _handleCommandNext(iq); - } - if (action == 'prev') { - return _handleCommandPrev(iq); - } - if (action == 'complete') { - return _handleCommandComplete(iq); - } - if (action == 'cancel') { - return _handleCommandCancel(iq); - } - } - - /// Generates a command reply stanza based on the provided session data. - void _processCommandResponse(IQ iq, Map session) { - final sessionID = session['id'] as String; - - final payloads = session['payload'] as List? ?? []; - final interfaces = session['interfaces'] as Set; - final payloadTypes = session['payloadTypes'] as Set; - - if (payloads.isNotEmpty) { - for (final payload in payloads) { - interfaces.add(payload.pluginAttribute); - payloadTypes.add(payload.runtimeType); - } - } - - session['interfaces'] = interfaces; - session['payloadTypes'] = payloadTypes; - - _sessions[sessionID] = session; - - for (final item in payloads) { - registerStanzaPlugin(Command(), item, iterable: true); - } - - final reply = iq.replyIQ(); - reply.transport = base.transport; - final command = reply['command'] as Command; - command['node'] = session['node']; - command['sessionid'] = session['id']; - - if (command['node'] == null) { - command['actions'] = []; - command['status'] = 'completed'; - } else if (session['hasNext'] as bool) { - final actions = ['next']; - if (session['allowComplete'] as bool) { - actions.add('complete'); - } else if (session['allowPrev'] as bool) { - actions.add('prev'); - } - command['actions'] = actions; - command['status'] = 'executing'; - } else { - command['actions'] = ['complete']; - command['status'] = 'executing'; - } - command['notes'] = session['notes']; - - for (final item in payloads) { - command.add(item); - } - - reply.sendIQ(); - } - - /// Processes an initial request to execute a command. - Future _handleCommandStart(IQ iq) async { - final sessionID = WhixpUtils.getUniqueId(); - final node = (iq['command'] as Command)['node'] as String; - final key = Tuple2(iq.to != null ? iq.to!.full : '', node); - final command = _commands[key]; - if (command == null || command.value2 == null) { - Log.instance.info('Command not found: $key, $command'); - throw StanzaException( - 'Command start exception', - condition: 'item-not-found', - ); - } - - final payload = []; - for (final stanza - in (iq['command'] as Command)['substanzas'] as List) { - payload.add(stanza); - } - - final interfaces = - Set.from(payload.map((item) => item.pluginAttribute)); - final payloadTypes = - Set.from(payload.map((item) => item.runtimeType)); - - final initialSession = { - 'id': sessionID, - 'from': iq.from, - 'to': iq.to, - 'node': node, - 'payload': payload, - 'interfaces': interfaces, - 'payloadTypes': payloadTypes, - 'notes': null, - 'hasNext': false, - 'allowComplete': false, - 'allowPrev': false, - 'past': [], - 'next': null, - 'prev': null, - 'cancel': null, - }; - - final handler = command.value2!; - final session = - await handler.call(iq, initialSession) as Map; - _processCommandResponse(iq, session); - } - - /// Processes the results of a command request. - void _handleCommandResult(IQ iq) { - String sessionID = 'client:${(iq['command'] as Command)['sessionid']}'; - late String pendingID; - bool pending = false; - - if (!_sessions.containsKey(sessionID) || _sessions[sessionID] == null) { - pending = true; - if ((iq['id'] as String).isEmpty) { - pendingID = _sessions.entries.last.key; - } else { - pendingID = 'client:pending_${iq['id']}'; - } - if (!_sessions.containsKey(pendingID)) { - return; - } - - sessionID = pendingID; - } - final command = iq['command'] as Command; - - final session = _sessions[sessionID]; - sessionID = 'client:${command['sessionid']}'; - session!['id'] = command['sessionid']; - - _sessions[sessionID] = session; - - if (pending) { - _sessions.remove(pendingID); - } - - String handlerType = 'next'; - if (iq['type'] == 'error') { - handlerType = 'error'; - } - final handler = session[handlerType] as _Handler?; - if (handler != null) { - handler.call(iq, session); - } else if (iq['type'] == 'error') { - terminateCommand(session); - } - - if (command['status'] == 'completed') { - terminateCommand(session); - } - } - - /// Process a request for the next step in the workflow for a command with - /// multiple steps. - Future _handleCommandNext(IQ iq) async { - final command = iq['command'] as Command; - final sessionID = command['sessionid'] as String; - final session = _sessions[sessionID]; - - if (session != null) { - final handler = session['next'] as _Handler; - final interfaces = session['interfaces'] as Set; - final results = []; - - for (final stanza in command['substanzas'] as List) { - if (interfaces.contains(stanza.pluginAttribute)) { - results.add(stanza); - } - } - - final newSession = - await handler.call(iq, session, results) as Map; - - _processCommandResponse(iq, newSession); - } else { - throw StanzaException( - 'Command start exception', - condition: 'item-not-found', - ); - } - } - - /// Processes a request for the previous step in the workflow for a command - /// with multiople steps. - Future _handleCommandPrev(IQ iq) async { - final command = iq['command'] as Command; - final sessionID = command['sessionid'] as String; - Map? session = _sessions[sessionID]; - - if (session != null) { - final handler = session['prev'] as _Handler?; - final interfaces = session['interfaces'] as Set; - final results = []; - for (final stanza in command['substanzas'] as List) { - if (interfaces.contains(stanza.pluginAttribute)) { - results.add(stanza); - } - } - - session = - await handler?.call(iq, session, results) as Map; - - _processCommandResponse(iq, session); - } else { - throw StanzaException( - 'Command start exception', - condition: 'item-not-found', - ); - } - } - - /// Processes a request to finish the execution of command and terminate the - /// workflow. - Future _handleCommandComplete(IQ iq) async { - final command = iq['command'] as Command; - final node = command['node'] as String; - final sessionID = command['sessionid'] as String; - final session = _sessions[sessionID]; - - if (session != null) { - final handler = session['prev'] as _Handler?; - final interfaces = session['interfaces'] as Set; - final results = []; - for (final stanza in command['substanzas'] as List) { - if (interfaces.contains(stanza.pluginAttribute)) { - results.add(stanza); - } - } - - if (handler != null) { - await handler(iq, session, results); - } - - _sessions.remove(sessionID); - - final payloads = session['payload'] as List? ?? []; - - for (final payload in payloads) { - registerStanzaPlugin(Command(), payload); - } - - final reply = iq.replyIQ() - ..transport = base.transport - ..enable('command'); - - final replyCommand = reply['command'] as Command; - replyCommand['node'] = node; - replyCommand['sessionid'] = sessionID; - replyCommand['actions'] = []; - replyCommand['status'] = 'completed'; - replyCommand['notes'] = session['notes']; - - for (final payload in payloads) { - replyCommand.add(payload); - } - - reply.sendIQ(); - } else { - throw StanzaException( - 'Command start exception', - condition: 'item-not-found', - ); - } - } - - /// Processes a request to cancel a command's execution. - Future _handleCommandCancel(IQ iq) async { - final command = iq['command'] as Command; - final node = command['node'] as String; - final sessionID = command['sessiondid']; - final session = _sessions[sessionID]; - - if (session != null) { - final handler = session['cancel'] as _Handler?; - if (handler != null) { - await handler.call(iq, session); - } - _sessions.remove(sessionID); - - final reply = iq.replyIQ() - ..transport = base.transport - ..enable('command'); - - final replyCommand = reply['command'] as Command; - replyCommand['node'] = node; - replyCommand['sessionid'] = sessionID; - replyCommand['status'] = 'canceled'; - replyCommand['notes'] = session['notes']; - - reply.sendIQ(); - } else { - throw StanzaException( - 'Command start exception', - condition: 'item-not-found', - ); - } - } + static final Map> _sessions = + >{}; /// Creates and sends a command stanza. /// /// If [flow] is true, the process the Iq result using the command workflow /// methods contained in the session instead of returning the response stanza /// itself. Defaults to `false`. - FutureOr sendCommand( + static async.FutureOr sendCommand( JabberID jid, String node, { JabberID? iqFrom, String? sessionID, - /// Must be in XMLBase or XMl element type. - List? payloads, + /// Must be in XMLBase or XML element type. + List? payloads, String action = 'execute', + async.FutureOr Function(IQ result)? callback, + async.FutureOr Function(ErrorStanza error)? failureCallback, + async.FutureOr Function()? timeoutCallback, + int timeout = 10, }) { - final iq = base.makeIQSet()..enable('command'); - iq['to'] = jid; - if (iqFrom != null) { - iq['from'] = iqFrom; - } - final command = iq['command'] as Command; - command['node'] = node; - command['action'] = action; - if (sessionID != null) { - command['sessionid'] = sessionID; - } - if (payloads != null) { - for (final payload in payloads) { - command.add(payload); - } - } + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..to = jid; + + if (iqFrom != null) iq.from = iqFrom; + + final command = + Command(node, action: action, sessionID: sessionID, payloads: payloads); - return iq.sendIQ(); + iq.payload = command; + + return iq.send( + callback: callback, + failureCallback: failureCallback, + timeoutCallback: timeoutCallback, + timeout: timeout, + ); } /// Initiate executing a command provided by a remote agent. - FutureOr startCommand( + static async.FutureOr startCommand( JabberID jid, String node, Map session, { - JabberID? iqFrom, + JabberID? from, }) { session['jid'] = jid; session['node'] = node; @@ -502,76 +82,42 @@ class AdHocCommands extends PluginBase { if (!session.containsKey('payload')) { session['payload'] = null; } - final iq = base.makeIQSet(iqTo: jid, iqFrom: iqFrom); - session['from'] = iqFrom; - (iq['command'] as Command)['node'] = node; - (iq['command'] as Command)['action'] = 'execute'; - if (session['payload'] != null) { - /// Although it accepts dynamic, the list must contain either XMLBase or - /// XML element (from xml package). - final payload = session['payload'] as List; - for (final stanza in payload) { - (iq['command'] as Command).add(stanza); + final iq = IQ(generateID: true) + ..to = jid + ..from = from + ..type = iqTypeSet; + if (from != null) session['from'] = from; + bool includePayloads = false; + late final List payloads; + + if ((session['payload'] as List?)?.isNotEmpty ?? false) { + includePayloads = true; + if (includePayloads) payloads = []; + for (final payload in session['payload'] as List) { + /// Parse an element from the saved payload in the session. + final elementFromString = xml.XmlDocument.parse(payload).rootElement; + + payloads.add( + Stanza.payloadFromXML( + WhixpUtils.generateNamespacedElement(elementFromString), + elementFromString, + ), + ); } } - final sessionID = 'client:pending_${iq['id']}'; - session['id'] = sessionID; - _sessions[sessionID] = session; - return iq.sendIQ(); - } - - FutureOr continueCommand( - Map session, { - String direction = 'next', - }) { - final sessionID = 'client:${session['id']}'; - _sessions[sessionID] = session; - - return sendCommand( - session['jid'] as JabberID, - session['node'] as String, - iqFrom: session['from'] as JabberID?, - sessionID: session['id'] as String, - action: direction, - payloads: (session['payload'] is List) - ? session['payload'] as List? - : [session['payload'] as XMLBase], + final command = Command( + node, + action: 'execute', + payloads: includePayloads ? payloads : null, ); - } - /// Deletes a command's session after a command has completed or an error has - /// occured. - void terminateCommand(Map session) { - final sessionID = 'client:${session['id']}'; - _sessions.remove(sessionID); - } - - @override - void sessionBind(String? jid) { - final disco = base.getPluginInstance('disco'); - if (disco != null) { - disco - ..addFeature(_commandNamespace) - ..setItems(items: {}); - } - } + final sessionID = 'client:pending_${iq.id}'; + session['id'] = sessionID; + _sessions[sessionID] = session; - @override - void pluginEnd() { - base.transport - ..removeEventHandler('command', handler: _handleAllCommands) - ..removeHandler('Ad-Hoc Execute') - ..removeHandler('Ad-Hoc Result'); + iq.payload = command; - final disco = base.getPluginInstance( - 'disco', - enableIfRegistered: false, - ); - if (disco != null) { - disco - ..removeFeature(_commandNamespace) - ..setItems(items: {}); - } + return iq.send(); } } diff --git a/lib/src/plugins/command/stanza.dart b/lib/src/plugins/command/stanza.dart index 9053cca..56a1105 100644 --- a/lib/src/plugins/command/stanza.dart +++ b/lib/src/plugins/command/stanza.dart @@ -9,161 +9,125 @@ part of 'command.dart'; /// commands are used primarily for human interaction. /// /// see -class Command extends XMLBase { - /// Example: - /// ```xml - /// - /// - /// - /// ``` - Command({ - super.pluginTagMapping, - super.pluginAttributeMapping, - super.pluginIterables, - super.getters, - super.setters, - super.deleters, - super.element, - super.parent, - }) : super( - name: 'command', - namespace: 'http://jabber.org/protocol/commands', - pluginAttribute: 'command', - interfaces: { - 'action', - 'sessionid', - 'node', - 'status', - 'actions', - 'notes', - }, - includeNamespace: true, - ) { - addGetters({ - const Symbol('action'): (args, base) => action, - const Symbol('actions'): (args, base) => actions, - const Symbol('notes'): (args, base) => notes, - }); - - addSetters({ - const Symbol('actions'): (value, args, base) => - setActions(value as List), - const Symbol('notes'): (value, args, base) => - setNotes(value as Map), - }); - - addDeleters({ - const Symbol('actions'): (args, base) => deleteActions(), - const Symbol('notes'): (args, base) => deleteNotes(), - }); - - registerPlugin(Form(), iterable: true); - } +class Command extends IQStanza { + /// Creates an instance of `Command`. + const Command( + this.node, { + this.action, + this.sessionID, + this.status, + this.payloads, + this.resultActions, + }); - /// Returns the value of the `action` attribute. - String get action { - if (parent!['type'] == 'set') { - return getAttribute('action', 'execute'); - } - return getAttribute('action'); - } + /// The node identifier of the command. + final String? node; - /// Assign the set of allowable next actions. - void setActions(List values) { - delete('actions'); - if (values.isNotEmpty) { - setSubText('{$namespace}actions', text: '', keep: true); - final actions = element!.getElement('actions', namespace: namespace); - for (final value in values) { - if (_nextActions.contains(value)) { - final action = WhixpUtils.xmlElement(value); - element!.childElements - .firstWhere((element) => element == actions) - .children - .add(action); - } + /// The action to be performed by the command. + final String? action; + + /// The list of action(s) from the `result` stanza. + final List? resultActions; + + /// The session ID of the command. + final String? sessionID; + + /// The status of the command. + final String? status; + + /// The Command form payload. + final List? payloads; + + /// Creates a `Command` instance from an XML element. + /// + /// - [node]: An XML element representing an Adhoc Command. + factory Command.fromXML(xml.XmlElement node) { + String? action; + String? status; + String? sessionID; + final actions = []; + final payloads = []; + + // Iterate over the child elements of the node to extract vCard information + for (final attribute in node.attributes) { + switch (attribute.localName) { + case 'status': + status = attribute.innerText; + case 'sessionid': + sessionID = attribute.innerText; } } - } - /// Returns the [Iterable] of the allowable next actions. - Iterable get actions { - final actions = []; - final actionElements = element!.getElement('actions', namespace: namespace); - if (actionElements != null) { - for (final action in _nextActions) { - final actionElement = - actionElements.getElement(action, namespace: namespace); - if (actionElement != null) { - actions.add(action); - } + for (final child in node.children.whereType()) { + switch (child.localName) { + case 'actions': + for (final child in child.children.whereType()) { + actions.add(child.localName); + } } + payloads.add( + Stanza.payloadFromXML( + WhixpUtils.generateNamespacedElement(child), + child, + ), + ); } - return actions; + + return Command( + node.getAttribute('node'), + action: action, + status: status, + sessionID: sessionID, + payloads: payloads, + resultActions: actions, + ); } - /// Remove all allowable next actions. - void deleteActions() => deleteSub('{$namespace}actions'); + /// Converts the `VCard4` instance to an XML element. + @override + xml.XmlElement toXML() { + final builder = WhixpUtils.makeGenerator(); + final attributes = {}; - /// Returns a [Map] of note information. - Map get notes { - final notes = {}; - final xml = element!.findAllElements('note', namespace: namespace); - for (final note in xml) { - notes.addAll({note.getAttribute('type') ?? 'info': note.innerText}); + if (sessionID?.isNotEmpty ?? false) { + attributes['sessionid'] = sessionID!; } - return notes; - } - - /// Adds multiple notes to the command result. - /// - /// [Map] representation the notes, with the key being of "info", "warning", - /// or "error", and the value of [notes] being any human readable message. - /// - /// ### Example: - /// ```dart - /// final notes = { - /// 'info': 'salam, blyat!', - /// 'warning': 'do not go gentle into that good night', - /// }; - /// ``` - void setNotes(Map notes) { - delete('notes'); - for (final note in notes.entries) { - addNote(note.value, note.key); + if (node?.isNotEmpty ?? false) { + attributes['node'] = node!; + } + if (action?.isNotEmpty ?? false) { + attributes['action'] = action!; } - } - /// Removes all note associated with the command result. - void deleteNotes() { - final notes = element!.findAllElements('note', namespace: namespace); - for (final note in notes) { - element!.children.remove(note); + builder.element( + name, + attributes: {'xmlns': namespace}..addAll(attributes), + nest: () { + if (status?.isNotEmpty ?? false) { + builder.element('status', nest: () => builder.text(status!)); + } + }, + ); + + final element = builder.buildDocument().rootElement; + if (payloads?.isNotEmpty ?? false) { + for (final payload in payloads!) { + element.children.add(payload.toXML().copy()); + } } - } - /// Adds a single [note] annotation to the command. - void addNote(String note, String type) { - final xml = WhixpUtils.xmlElement('note'); - xml.setAttribute('type', type); - xml.innerText = note; - element!.children.add(xml); + return element; } - final _nextActions = {'prev', 'next', 'complete'}; + /// The name of the XML element representing the vCard. + @override + String get name => 'command'; + /// The XML namespace for the vCard 4.0. @override - Command copy({xml.XmlElement? element, XMLBase? parent}) => Command( - pluginTagMapping: pluginTagMapping, - pluginAttributeMapping: pluginAttributeMapping, - pluginIterables: pluginIterables, - getters: getters, - setters: setters, - deleters: deleters, - element: element, - parent: parent, - ); + String get namespace => 'http://jabber.org/protocol/commands'; + + /// A tag used to identify the vCard element. + @override + String get tag => adhocCommandTag; } From 9d64315cfcd4c045bfa541194f160b7d13acf258 Mon Sep 17 00:00:00 2001 From: vsevex Date: Sun, 18 Aug 2024 00:12:42 +0400 Subject: [PATCH 81/81] refactor: Remove unused 'src/stanza/atom.dart' file --- lib/whixp.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/whixp.dart b/lib/whixp.dart index 1bc9be4..6980369 100644 --- a/lib/whixp.dart +++ b/lib/whixp.dart @@ -9,7 +9,6 @@ export 'src/jid/jid.dart'; export 'src/log/log.dart'; export 'src/plugins/plugins.dart'; export 'src/reconnection.dart'; -export 'src/stanza/atom.dart'; export 'src/stanza/error.dart'; export 'src/stanza/iq.dart'; export 'src/stanza/message.dart';