diff --git a/example/inbox.dart b/example/inbox.dart new file mode 100644 index 0000000..15866bb --- /dev/null +++ b/example/inbox.dart @@ -0,0 +1,76 @@ +import 'package:whixp/src/plugins/inbox/inbox.dart'; +import 'package:whixp/whixp.dart'; + +void main() { + final whixp = Whixp( + jabberID: 'asdf@localhost', + password: 'passwd', + logger: Log( + enableWarning: true, + enableError: true, + includeTimestamp: true, + ), + internalDatabasePath: 'whixp', + ); + + whixp + ..addEventHandler('streamNegotiated', (_) { + return getInbox(); + }) + ..addEventHandler('message', (message) { + final result = message?.get(); + if (result?.isNotEmpty ?? false) { + for (final stanza in result!) { + final forwarded = stanza.forwarded; + if (forwarded?.delay?.stamp != null) { + Log.instance.info(forwarded!.delay!.stamp!); + } + Log.instance.info("marked: ${forwarded?.actual?.isMarked}"); + Log.instance.info( + "box: ${stanza.box} \t archive: ${stanza.archive} \t mute: ${stanza.mute}"); + Log.instance.info( + "from: ${forwarded?.actual?.from?.username} \t to: ${forwarded?.actual?.to?.username}", + ); + + Log.instance.info("unread: ${stanza.unread}"); + + Log.instance.info( + "type: ${forwarded?.actual?.subject} \t value: ${forwarded?.actual?.body}", + ); + } + } + }); + whixp.connect(); +} + +String? globalLast; + +Future getInbox({ + String? lastItem, +}) async { + globalLast = null; + final result = await Inbox.queryInbox( + pagination: RSMSet( + max: 25, + after: lastItem, + ), + ); + + final fin = result.payload as InboxFin?; + final last = fin?.last?.lastItem; + Log.instance.warning( + "active-conversations: ${fin?.activeConversation}", + ); + Log.instance.warning( + "unread: ${fin?.unreadMessages}", + ); + Log.instance.warning( + "cursor: $last", + ); + + if (last != null && last != globalLast) { + getInbox( + lastItem: last, + ); + } +} diff --git a/example/mam.dart b/example/mam.dart index 8ba55e8..cea3afa 100644 --- a/example/mam.dart +++ b/example/mam.dart @@ -2,28 +2,46 @@ import 'package:whixp/whixp.dart'; void main() { final whixp = Whixp( - jabberID: 'vsevex@localhost', + jabberID: 'asdf@localhost', password: 'passwd', - logger: Log(enableWarning: true, enableError: true, includeTimestamp: true), + logger: Log( + enableWarning: true, + enableError: true, + includeTimestamp: true, + ), internalDatabasePath: 'whixp', ); whixp ..addEventHandler('streamNegotiated', (_) { - for (int i = 1; i <= 100; i++) { - whixp.sendMessage(JabberID('alyosha@loalhost'), body: 'Message no: $i'); - } + // for (int i = 1; i <= 2; i++) { + // final message = Message( + // subject: "normal", + // body: "hmm trying something heehe * $i", + // )..to = JabberID("asdfasdf@localhost"); + // + // whixp.send(message.makeMarkable); + // } return paginationRequest(); }) ..addEventHandler('message', (message) { final result = message?.get(); + if (result?.isNotEmpty ?? false) { for (final stanza in result!) { final forwarded = stanza.forwarded; if (forwarded?.delay?.stamp != null) { Log.instance.info(forwarded!.delay!.stamp!); } + Log.instance.info("marked: ${forwarded?.actual?.isMarked}"); + Log.instance.info( + "from: ${forwarded?.actual?.from?.username} \t to: ${forwarded?.actual?.to?.username}", + ); + + Log.instance.info( + "type: ${forwarded?.actual?.subject} \t value: ${forwarded?.actual?.body}", + ); } } }); @@ -32,11 +50,30 @@ void main() { /// Recursively request messages from the archive. Future paginationRequest({String? lastItem}) async { - final result = - await MAM.queryArchive(pagination: RSMSet(max: 20, after: lastItem)); + const mam = MAM(); + final result = await MAM.queryArchive( + pagination: RSMSet( + max: 25, + // after: lastItem, + before: lastItem ?? "", + ), + filter: mam.createFilter( + wth: "asdfasdf@localhost", + ), + flipPage: true, + ); final fin = result.payload as MAMFin?; final last = fin?.last?.lastItem; + Log.instance.warning( + "complete: ${fin?.complete}", + ); + Log.instance.info("first cursor: $last"); if (last?.isEmpty ?? true) return; - return paginationRequest(lastItem: last); + if (fin != null && !fin.complete && last != null) { + return paginationRequest( + lastItem: last, + ); + } + // return paginationRequest(lastItem: last); } diff --git a/lib/src/_static.dart b/lib/src/_static.dart index 3ba6268..576c55f 100644 --- a/lib/src/_static.dart +++ b/lib/src/_static.dart @@ -66,6 +66,9 @@ String get mamFinTag => '{urn:xmpp:mam:2}fin'; String get mamResultTag => '{urn:xmpp:mam:2}result'; String get mamMetadataTag => '{urn:xmpp:mam:2}metadata'; String get forwardedTag => '{urn:xmpp:forward:0}forwarded'; +String get inboxQueryTag => '{erlang-solutions.com:xmpp:inbox:0}inbox'; +String get inboxFinTag => '{erlang-solutions.com:xmpp:inbox:0}fin'; +String get inboxResultTag => '{erlang-solutions.com:xmpp:inbox:0}result'; Set get presenceTypes => { 'subscribe', diff --git a/lib/src/plugins/inbox/inbox.dart b/lib/src/plugins/inbox/inbox.dart new file mode 100644 index 0000000..3b9deba --- /dev/null +++ b/lib/src/plugins/inbox/inbox.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:whixp/src/_static.dart'; +import 'package:whixp/src/plugins/plugins.dart'; +import 'package:whixp/src/stanza/forwarded.dart'; +import 'package:whixp/src/stanza/iq.dart'; +import 'package:whixp/src/stanza/stanza.dart'; +import 'package:whixp/src/utils/src/utils.dart'; +import 'package:xml/xml.dart'; + +part 'stanza.dart'; + +class Inbox { + const Inbox(); + + /// read here for all the options for the inbox querying + /// https://esl.github.io/MongooseDocs/latest/open-extensions/inbox/ + static FutureOr queryInbox({ + RSMSet? pagination, + int timeout = 5, + }) { + final query = InboxQuery( + rsm: pagination, + ); + + final iq = IQ(generateID: true) + ..type = iqTypeSet + ..payload = query; + + return iq.send( + timeout: timeout, + ); + } +} diff --git a/lib/src/plugins/inbox/stanza.dart b/lib/src/plugins/inbox/stanza.dart new file mode 100644 index 0000000..ccef0be --- /dev/null +++ b/lib/src/plugins/inbox/stanza.dart @@ -0,0 +1,211 @@ +part of 'inbox.dart'; + +class InboxQuery extends IQStanza { + const InboxQuery({ + this.rsm, + }); + + final RSMSet? rsm; + + @override + XmlElement toXML() { + final element = WhixpUtils.xmlElement( + name, + namespace: namespace, + ); + + if (rsm != null) element.children.add(rsm!.toXML().copy()); + + return element; + } + + factory InboxQuery.fromXML(XmlElement node) { + RSMSet? rsm; + + for (final child in node.children.whereType()) { + if (WhixpUtils.generateNamespacedElement(child) == rsmSetTag) { + rsm = RSMSet.fromXML(child); + } + } + + return InboxQuery( + rsm: rsm, + ); + } + + @override + String get name => "inbox"; + + @override + String get namespace => WhixpUtils.getNamespace('INBOX'); + + @override + String get tag => inboxQueryTag; +} + +class InboxFin extends IQStanza { + /// iq-fin stanza marks the end of the inbox query, Inbox query result IQ stanza returns the following values: + /// count: the total number of conversations (if hidden_read value was set to true, this value will be equal to active_conversations) + /// unread-messages: total number of unread messages from all conversations + /// active-conversations: the number of conversations with unread message(s) + const InboxFin({ + this.last, + required this.count, + required this.unreadMessages, + required this.activeConversation, + }); + + final RSMSet? last; + final int activeConversation; + final int count; + final int unreadMessages; + + @override + XmlElement toXML() { + final element = WhixpUtils.xmlElement( + name, + namespace: namespace, + ); + + if (last != null) element.children.add(last!.toXML().copy()); + + element.children.add( + WhixpUtils.xmlElement( + "active-conversations", + text: activeConversation.toString(), + ), + ); + + element.children.add( + WhixpUtils.xmlElement( + "count", + text: count.toString(), + ), + ); + + element.children.add( + WhixpUtils.xmlElement( + "unread-messages", + text: unreadMessages.toString(), + ), + ); + + return element; + } + + factory InboxFin.fromXML(XmlElement node) { + RSMSet? last; + int activeConversation = 0; + int count = 0; + int unreadMessages = 0; + + for (final child in node.children.whereType()) { + if (WhixpUtils.generateNamespacedElement(child) == rsmSetTag) { + last = RSMSet.fromXML(child); + } else if (child.name.toString() == "active-conversations") { + activeConversation = int.parse(child.innerText); + } else if (child.name.toString() == "count") { + count = int.parse(child.innerText); + } else if (child.name.toString() == "unread-messages") { + unreadMessages = int.parse(child.innerText); + } + } + + return InboxFin( + last: last, + count: count, + unreadMessages: unreadMessages, + activeConversation: activeConversation, + ); + } + + @override + String get name => "fin"; + + @override + String get namespace => WhixpUtils.getNamespace('INBOX'); + + @override + String get tag => inboxFinTag; +} + +/// none-or-many message stanzas are sent to the requesting resource describing each inbox entry +class InboxResult extends MessageStanza { + const InboxResult({ + this.queryID, + this.unread, + this.forwarded, + this.box, + this.archive, + this.mute, + }); + + final String? queryID; + final int? unread; + final Forwarded? forwarded; + final String? box; + final bool? archive; + final bool? mute; + + @override + XmlElement toXML() { + final attributes = { + 'xmlns': WhixpUtils.getNamespace('INBOX'), + }; + + if (queryID?.isNotEmpty ?? false) attributes['queryID'] = queryID!; + if (unread != null) attributes['unread'] = unread.toString(); + + final element = WhixpUtils.xmlElement( + name, + namespace: WhixpUtils.getNamespace('INBOX'), + attributes: attributes, + ); + if (forwarded != null) element.children.add(forwarded!.toXML().copy()); + return element; + } + + factory InboxResult.fromXML(XmlElement node) { + int? unread; + String? queryID; + Forwarded? forwarded; + String? box; + bool? archive; + bool? mute; + + for (final attribute in node.attributes) { + if (attribute.localName == 'queryid') { + queryID = attribute.value; + } else if (attribute.localName == "unread") { + unread = int.parse(attribute.value); + } + } + + for (final child in node.children.whereType()) { + if (WhixpUtils.generateNamespacedElement(child) == forwardedTag) { + forwarded = Forwarded.fromXML(child); + } else if (child.name.toString() == "box") { + box = child.innerText; + } else if (child.name.toString() == "archive") { + archive = child.innerText == "true"; + } else if (child.name.toString() == "mute") { + mute = int.parse(child.innerText) == 1; + } + } + + return InboxResult( + queryID: queryID, + forwarded: forwarded, + unread: unread, + box: box, + archive: archive, + mute: mute, + ); + } + + @override + String get name => 'result'; + + @override + String get tag => inboxResultTag; +} diff --git a/lib/src/plugins/plugins.dart b/lib/src/plugins/plugins.dart index b353456..4791099 100644 --- a/lib/src/plugins/plugins.dart +++ b/lib/src/plugins/plugins.dart @@ -5,7 +5,9 @@ export 'disco/info.dart'; export 'disco/items.dart'; export 'form/form.dart'; export 'id/id.dart'; +export 'inbox/inbox.dart'; export 'mam/mam.dart'; +export 'markers/markers.dart'; export 'pubsub/pubsub.dart'; export 'push/push.dart'; export 'rsm/rsm.dart'; diff --git a/lib/src/stanza/stanza.dart b/lib/src/stanza/stanza.dart index 5f6c817..b36171e 100644 --- a/lib/src/stanza/stanza.dart +++ b/lib/src/stanza/stanza.dart @@ -1,12 +1,12 @@ import 'package:whixp/src/_static.dart'; import 'package:whixp/src/exception.dart'; import 'package:whixp/src/plugins/features.dart'; +import 'package:whixp/src/plugins/inbox/inbox.dart'; import 'package:whixp/src/plugins/plugins.dart'; import 'package:whixp/src/plugins/version.dart'; import 'package:whixp/src/stanza/forwarded.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`. @@ -63,6 +63,12 @@ abstract class Stanza with Packet { return MAMResult.fromXML(node); } else if (tag == forwardedTag) { return Forwarded.fromXML(node); + } else if (tag == inboxQueryTag) { + return InboxQuery.fromXML(node); + } else if (tag == inboxFinTag) { + return InboxFin.fromXML(node); + } else if (tag == inboxResultTag) { + return InboxResult.fromXML(node); } else { throw WhixpInternalException.stanzaNotFound( node.localName, diff --git a/lib/src/utils/src/_constants.dart b/lib/src/utils/src/_constants.dart index c22a0b2..ec7b2ff 100644 --- a/lib/src/utils/src/_constants.dart +++ b/lib/src/utils/src/_constants.dart @@ -53,4 +53,5 @@ final _namespace = { 'VERSION': "jabber:iq:version", 'STANZAS': "urn:ietf:params:xml:ns:xmpp-stanzas", 'XML': "http://www.w3.org/XML/1998/namespace", + 'INBOX': "erlang-solutions.com:xmpp:inbox:0" };