001/**
002 *
003 * Copyright © 2017-2018 Florian Schmaus, 2016-2017 Fernando Ramirez
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.mam;
018
019import java.text.ParseException;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.UUID;
027import java.util.WeakHashMap;
028
029import org.jivesoftware.smack.ConnectionCreationListener;
030import org.jivesoftware.smack.Manager;
031import org.jivesoftware.smack.SmackException.NoResponseException;
032import org.jivesoftware.smack.SmackException.NotConnectedException;
033import org.jivesoftware.smack.SmackException.NotLoggedInException;
034import org.jivesoftware.smack.StanzaCollector;
035import org.jivesoftware.smack.XMPPConnection;
036import org.jivesoftware.smack.XMPPConnectionRegistry;
037import org.jivesoftware.smack.XMPPException.XMPPErrorException;
038import org.jivesoftware.smack.filter.IQReplyFilter;
039import org.jivesoftware.smack.packet.IQ;
040import org.jivesoftware.smack.packet.Message;
041import org.jivesoftware.smack.packet.Stanza;
042import org.jivesoftware.smack.util.Objects;
043import org.jivesoftware.smack.util.StringUtils;
044
045import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
046import org.jivesoftware.smackx.forward.packet.Forwarded;
047import org.jivesoftware.smackx.mam.element.MamElements;
048import org.jivesoftware.smackx.mam.element.MamElements.MamResultExtension;
049import org.jivesoftware.smackx.mam.element.MamFinIQ;
050import org.jivesoftware.smackx.mam.element.MamPrefsIQ;
051import org.jivesoftware.smackx.mam.element.MamPrefsIQ.DefaultBehavior;
052import org.jivesoftware.smackx.mam.element.MamQueryIQ;
053import org.jivesoftware.smackx.mam.filter.MamResultFilter;
054import org.jivesoftware.smackx.muc.MultiUserChat;
055import org.jivesoftware.smackx.rsm.packet.RSMSet;
056import org.jivesoftware.smackx.xdata.FormField;
057import org.jivesoftware.smackx.xdata.packet.DataForm;
058
059import org.jxmpp.jid.EntityBareJid;
060import org.jxmpp.jid.EntityFullJid;
061import org.jxmpp.jid.Jid;
062
063/**
064 * A Manager for Message Archive Management (MAM, <a href="http://xmpp.org/extensions/xep-0313.html">XEP-0313</a>).
065 *
066 * <h2>Get an instance of a manager for a message archive</h2>
067 *
068 * In order to work with {@link MamManager} you need to obtain an instance for a particular archive.
069 * To get the instance for the default archive on the user's server, use the {@link #getInstanceFor(XMPPConnection)} method.
070 *
071 * <pre>
072 * {@code
073 * XMPPConnection connection = ...
074 * MamManager mamManager = MamManager.getInstanceFor(connection);
075 * }
076 * </pre>
077 *
078 * If you want to retrieve a manager for a different archive use {@link #getInstanceFor(XMPPConnection, Jid)}, which takes the archive's XMPP address as second argument.
079 *
080 * <h2>Check if MAM is supported</h2>
081 *
082 * After you got your manager instance, you probably first want to check if MAM is supported.
083 * Simply use {@link #isSupported()} to check if there is a MAM archive available.
084 *
085 * <pre>
086 * {@code
087 * boolean isSupported = mamManager.isSupported();
088 * }
089 * </pre>
090 *
091 * <h2>Message Archive Preferences</h2>
092 *
093 * After you have verified that the MAM is supported, you probably want to configure the archive first before using it.
094 * One of the most important preference is to enable MAM for your account.
095 * Some servers set up new accounts with MAM disabled by default.
096 * You can do so by calling {@link #enableMamForAllMessages()}.
097 *
098 * <h3>Retrieve current preferences</h3>
099 *
100 * The archive's preferences can be retrieved using {@link #retrieveArchivingPreferences()}.
101 *
102 * <h3>Update preferences</h3>
103 *
104 * Use {@link MamPrefsResult#asMamPrefs()} to get a modifiable {@link MamPrefs} instance.
105 * After performing the desired changes, use {@link #updateArchivingPreferences(MamPrefs)} to update the preferences.
106 *
107 * <h2>Query the message archive</h2>
108 *
109 * Querying a message archive involves a two step process. First you need specify the query's arguments, for example a date range.
110 * The query arguments of a particular query are represented by a {@link MamQueryArgs} instance, which can be build using {@link MamQueryArgs.Builder}.
111 *
112 * After you have build such an instance, use {@link #queryArchive(MamQueryArgs)} to issue the query.
113 *
114 * <pre>
115 * {@code
116 * MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
117 *                                 .limitResultsToJid(jid)
118 *                                 .setResultPageSizeTo(10)
119 *                                 .queryLastPage()
120 *                                 .build();
121 * MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs);
122 * }
123 * </pre>
124 *
125 * On success {@link #queryArchive(MamQueryArgs)} returns a {@link MamQuery} instance.
126 * The instance will hold one page of the queries result set.
127 * Use {@link MamQuery#getMessages()} to retrieve the messages of the archive belonging to the page.
128 *
129 * You can get the whole page including all metadata using {@link MamQuery#getPage()}.
130 *
131 * <h2>Paging through the results</h2>
132 *
133 * Because the matching result set could be potentially very big, a MAM service will probably not return all matching messages.
134 * Instead the results are possibly send in multiple pages.
135 * To check if the result was complete or if there are further pages, use {@link MamQuery#isComplete()}.
136 * If this method returns {@code false}, then you may want to page through the archive.
137 *
138 * {@link MamQuery} provides convince methods to do so: {@link MamQuery#pageNext(int)} and {@link MamQuery#pagePrevious(int)}.
139 *
140 * <pre>
141 * {@code
142 * MamQuery nextPageMamQuery = mamQuery.pageNext(10);
143 * }
144 * </pre>
145 *
146 * <h2>Get the supported form fields</h2>
147 *
148 * You can use {@link #retrieveFormFields()} to retrieve a list of the supported additional form fields by this archive.
149 * Those fields can be used for further restrict a query.
150 *
151 *
152 * @see <a href="http://xmpp.org/extensions/xep-0313.html">XEP-0313: Message
153 *      Archive Management</a>
154 * @author Florian Schmaus
155 * @author Fernando Ramirez
156 *
157 */
158public final class MamManager extends Manager {
159
160    static {
161        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
162            @Override
163            public void connectionCreated(XMPPConnection connection) {
164                getInstanceFor(connection);
165            }
166        });
167    }
168
169    private static final String FORM_FIELD_WITH = "with";
170    private static final String FORM_FIELD_START = "start";
171    private static final String FORM_FIELD_END = "end";
172
173    private static final Map<XMPPConnection, Map<Jid, MamManager>> INSTANCES = new WeakHashMap<>();
174
175    /**
176     * Get a MamManager for the MAM archive of the local entity (the "user") of the given connection.
177     *
178     * @param connection the XMPP connection to get the archive for.
179     * @return the instance of MamManager.
180     */
181    // CHECKSTYLE:OFF:RegexpSingleline
182    public static MamManager getInstanceFor(XMPPConnection connection) {
183    // CHECKSTYLE:ON:RegexpSingleline
184        return getInstanceFor(connection, (Jid) null);
185    }
186
187    /**
188     * Get a MamManager for the MAM archive of the given {@code MultiUserChat}. Note that not all MUCs support MAM,
189     * hence it is recommended to use {@link #isSupported()} to check if MAM is supported by the MUC.
190     *
191     * @param multiUserChat the MultiUserChat to retrieve the MamManager for.
192     * @return the MamManager for the given MultiUserChat.
193     * @since 4.3.0
194     */
195    public static MamManager getInstanceFor(MultiUserChat multiUserChat) {
196        XMPPConnection connection = multiUserChat.getXmppConnection();
197        Jid archiveAddress = multiUserChat.getRoom();
198        return getInstanceFor(connection, archiveAddress);
199    }
200
201    public static synchronized MamManager getInstanceFor(XMPPConnection connection, Jid archiveAddress) {
202        Map<Jid, MamManager> managers = INSTANCES.get(connection);
203        if (managers == null) {
204            managers = new HashMap<>();
205            INSTANCES.put(connection, managers);
206        }
207        MamManager mamManager = managers.get(archiveAddress);
208        if (mamManager == null) {
209            mamManager = new MamManager(connection, archiveAddress);
210            managers.put(archiveAddress, mamManager);
211        }
212        return mamManager;
213    }
214
215    private final Jid archiveAddress;
216
217    private final ServiceDiscoveryManager serviceDiscoveryManager;
218
219    private MamManager(XMPPConnection connection, Jid archiveAddress) {
220        super(connection);
221        this.archiveAddress = archiveAddress;
222        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
223    }
224
225    /**
226     * The the XMPP address of this MAM archive. Note that this method may return {@code null} if this MamManager
227     * handles the local entity's archive and if the connection has never been authenticated at least once.
228     *
229     * @return the XMPP address of this MAM archive or {@code null}.
230     * @since 4.3.0
231     */
232    public Jid getArchiveAddress() {
233        if (archiveAddress == null) {
234            EntityFullJid localJid = connection().getUser();
235            if (localJid == null) {
236                return null;
237            }
238            return localJid.asBareJid();
239        }
240        return archiveAddress;
241    }
242
243    public static final class MamQueryArgs {
244        private final String node;
245
246        private final Map<String, FormField> formFields;
247
248        private final Integer maxResults;
249
250        private final String afterUid;
251
252        private final String beforeUid;
253
254        private MamQueryArgs(Builder builder) {
255            node = builder.node;
256            formFields = builder.formFields;
257            if (builder.maxResults > 0) {
258                maxResults = builder.maxResults;
259            } else {
260                maxResults = null;
261            }
262            afterUid = builder.afterUid;
263            beforeUid = builder.beforeUid;
264        }
265
266        private DataForm dataForm;
267
268        DataForm getDataForm() {
269            if (dataForm != null) {
270                return dataForm;
271            }
272            dataForm = getNewMamForm();
273            dataForm.addFields(formFields.values());
274            return dataForm;
275        }
276
277        void maybeAddRsmSet(MamQueryIQ mamQueryIQ) {
278            if (maxResults == null && afterUid == null && beforeUid == null) {
279                return;
280            }
281
282            int max;
283            if (maxResults != null) {
284                max = maxResults;
285            } else {
286                max = -1;
287            }
288
289            RSMSet rsmSet = new RSMSet(afterUid, beforeUid, -1, -1, null, max, null, -1);
290            mamQueryIQ.addExtension(rsmSet);
291        }
292
293        public static Builder builder() {
294            return new Builder();
295        }
296
297        public static final class Builder {
298            private String node;
299
300            private final Map<String, FormField> formFields = new HashMap<>(8);
301
302            private int maxResults = -1;
303
304            private String afterUid;
305
306            private String beforeUid;
307
308            public Builder queryNode(String node) {
309                if (node == null) {
310                    return this;
311                }
312
313                this.node = node;
314
315                return this;
316            }
317
318            public Builder limitResultsToJid(Jid withJid) {
319                if (withJid == null) {
320                    return this;
321                }
322
323                FormField formField = getWithFormField(withJid);
324                formFields.put(formField.getVariable(), formField);
325
326                return this;
327            }
328
329            public Builder limitResultsSince(Date start) {
330                if (start == null) {
331                    return this;
332                }
333
334                FormField formField = new FormField(FORM_FIELD_START);
335                formField.addValue(start);
336                formFields.put(formField.getVariable(), formField);
337
338                FormField endFormField = formFields.get(FORM_FIELD_END);
339                if (endFormField != null) {
340                    Date end;
341                    try {
342                        end = endFormField.getFirstValueAsDate();
343                    }
344                    catch (ParseException e) {
345                        throw new IllegalStateException(e);
346                    }
347                    if (end.getTime() <= start.getTime()) {
348                        throw new IllegalArgumentException("Given start date (" + start
349                                        + ") is after the existing end date (" + end + ')');
350                    }
351                }
352
353                return this;
354            }
355
356            public Builder limitResultsBefore(Date end) {
357                if (end == null) {
358                    return this;
359                }
360
361                FormField formField = new FormField(FORM_FIELD_END);
362                formField.addValue(end);
363                formFields.put(formField.getVariable(), formField);
364
365                FormField startFormField = formFields.get(FORM_FIELD_START);
366                if (startFormField != null) {
367                    Date start;
368                    try {
369                        start = startFormField.getFirstValueAsDate();
370                    } catch (ParseException e) {
371                        throw new IllegalStateException(e);
372                    }
373                    if (end.getTime() <= start.getTime()) {
374                        throw new IllegalArgumentException("Given end date (" + end
375                                        + ") is before the existing start date (" + start + ')');
376                    }
377                }
378
379                return this;
380            }
381
382            public Builder setResultPageSize(Integer max) {
383                if (max == null) {
384                    maxResults = -1;
385                    return this;
386                }
387                return setResultPageSizeTo(max.intValue());
388            }
389
390            public Builder setResultPageSizeTo(int max) {
391                if (max < 0) {
392                    throw new IllegalArgumentException();
393                }
394                this.maxResults = max;
395                return this;
396            }
397
398            /**
399             * Only return the count of messages the query yields, not the actual messages. Note that not all services
400             * return a correct count, some return an approximate count.
401             *
402             * @return an reference to this builder.
403             * @see <a href="https://xmpp.org/extensions/xep-0059.html#count">XEP-0059 § 2.7</a>
404             */
405            public Builder onlyReturnMessageCount() {
406                return setResultPageSizeTo(0);
407            }
408
409            public Builder withAdditionalFormField(FormField formField) {
410                formFields.put(formField.getVariable(), formField);
411                return this;
412            }
413
414            public Builder withAdditionalFormFields(List<FormField> additionalFields) {
415                for (FormField formField : additionalFields) {
416                    withAdditionalFormField(formField);
417                }
418                return this;
419            }
420
421            public Builder afterUid(String afterUid) {
422                this.afterUid = StringUtils.requireNullOrNotEmpty(afterUid, "afterUid must not be empty");
423                return this;
424            }
425
426            /**
427             * Specifies a message UID as 'before' anchor for the query. Note that unlike {@link #afterUid(String)} this
428             * method also accepts the empty String to query the last page of an archive (c.f. XEP-0059 § 2.5).
429             *
430             * @param beforeUid a message UID acting as 'before' query anchor.
431             * @return an instance to this builder.
432             */
433            public Builder beforeUid(String beforeUid) {
434                // We don't perform any argument validation, since every possible argument (null, empty string,
435                // non-empty string) is valid.
436                this.beforeUid = beforeUid;
437                return this;
438            }
439
440            /**
441             * Query from the last, i.e. most recent, page of the archive. This will return the very last page of the
442             * archive holding the most recent matching messages. You usually would page backwards from there on.
443             *
444             * @return a reference to this builder.
445             * @see <a href="https://xmpp.org/extensions/xep-0059.html#last">XEP-0059 § 2.5. Requesting the Last Page in
446             *      a Result Set</a>
447             */
448            public Builder queryLastPage() {
449                return beforeUid("");
450            }
451
452            public MamQueryArgs build() {
453                return new MamQueryArgs(this);
454            }
455        }
456    }
457
458    /**
459     * Query archive with a maximum amount of results.
460     *
461     * @param max
462     * @return the MAM query result
463     * @throws NoResponseException
464     * @throws XMPPErrorException
465     * @throws NotConnectedException
466     * @throws InterruptedException
467     * @throws NotLoggedInException
468     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
469     */
470    @Deprecated
471    // TODO Remove in Smack 4.4
472    public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException,
473            NotConnectedException, InterruptedException, NotLoggedInException {
474        return queryArchive(null, max, null, null, null, null);
475    }
476
477    /**
478     * Query archive with a JID (only messages from/to the JID).
479     *
480     * @param withJid
481     * @return the MAM query result
482     * @throws NoResponseException
483     * @throws XMPPErrorException
484     * @throws NotConnectedException
485     * @throws InterruptedException
486     * @throws NotLoggedInException
487     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
488     */
489    @Deprecated
490    // TODO Remove in Smack 4.4
491    public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException,
492            NotConnectedException, InterruptedException, NotLoggedInException {
493        return queryArchive(null, null, null, null, withJid, null);
494    }
495
496    /**
497     * Query archive filtering by start and/or end date. If start == null, the
498     * value of 'start' will be equal to the date/time of the earliest message
499     * stored in the archive. If end == null, the value of 'end' will be equal
500     * to the date/time of the most recent message stored in the archive.
501     *
502     * @param start
503     * @param end
504     * @return the MAM query result
505     * @throws NoResponseException
506     * @throws XMPPErrorException
507     * @throws NotConnectedException
508     * @throws InterruptedException
509     * @throws NotLoggedInException
510     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
511     */
512    @Deprecated
513    // TODO Remove in Smack 4.4
514    public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException,
515            NotConnectedException, InterruptedException, NotLoggedInException {
516        return queryArchive(null, null, start, end, null, null);
517    }
518
519    /**
520     * Query Archive adding filters with additional fields.
521     *
522     * @param additionalFields
523     * @return the MAM query result
524     * @throws NoResponseException
525     * @throws XMPPErrorException
526     * @throws NotConnectedException
527     * @throws InterruptedException
528     * @throws NotLoggedInException
529     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
530     */
531    @Deprecated
532    // TODO Remove in Smack 4.4
533    public MamQueryResult queryArchive(List<FormField> additionalFields) throws NoResponseException, XMPPErrorException,
534            NotConnectedException, InterruptedException, NotLoggedInException {
535        return queryArchive(null, null, null, null, null, additionalFields);
536    }
537
538    /**
539     * Query archive filtering by start date. The value of 'end' will be equal
540     * to the date/time of the most recent message stored in the archive.
541     *
542     * @param start
543     * @return the MAM query result
544     * @throws NoResponseException
545     * @throws XMPPErrorException
546     * @throws NotConnectedException
547     * @throws InterruptedException
548     * @throws NotLoggedInException
549     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
550     */
551    @Deprecated
552    // TODO Remove in Smack 4.4
553    public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException,
554            NotConnectedException, InterruptedException, NotLoggedInException {
555        return queryArchive(null, null, start, null, null, null);
556    }
557
558    /**
559     * Query archive filtering by end date. The value of 'start' will be equal
560     * to the date/time of the earliest message stored in the archive.
561     *
562     * @param end
563     * @return the MAM query result
564     * @throws NoResponseException
565     * @throws XMPPErrorException
566     * @throws NotConnectedException
567     * @throws InterruptedException
568     * @throws NotLoggedInException
569     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
570     */
571    @Deprecated
572    // TODO Remove in Smack 4.4
573    public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException,
574            NotConnectedException, InterruptedException, NotLoggedInException {
575        return queryArchive(null, null, null, end, null, null);
576    }
577
578
579    /**
580     * Query archive applying filters: max count, start date, end date, from/to
581     * JID and with additional fields.
582     *
583     * @param max
584     * @param start
585     * @param end
586     * @param withJid
587     * @param additionalFields
588     * @return the MAM query result
589     * @throws NoResponseException
590     * @throws XMPPErrorException
591     * @throws NotConnectedException
592     * @throws InterruptedException
593     * @throws NotLoggedInException
594     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
595     */
596    @Deprecated
597    // TODO Remove in Smack 4.4
598    public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List<FormField> additionalFields)
599            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
600            NotLoggedInException {
601      return queryArchive(null, max, start, end, withJid, additionalFields);
602    }
603
604
605    /**
606     * Query an message archive like a MUC archive or a PubSub node archive, addressed by an archiveAddress, applying
607     * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the
608     * default, the server will be requested.
609     *
610     * @param node The PubSub node name, can be null
611     * @param max
612     * @param start
613     * @param end
614     * @param withJid
615     * @param additionalFields
616     * @return the MAM query result
617     * @throws NoResponseException
618     * @throws XMPPErrorException
619     * @throws NotConnectedException
620     * @throws InterruptedException
621     * @throws NotLoggedInException
622     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
623     */
624    @Deprecated
625    // TODO Remove in Smack 4.4
626    public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid,
627                    List<FormField> additionalFields)
628            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
629            NotLoggedInException {
630        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
631                        .queryNode(node)
632                        .setResultPageSize(max)
633                        .limitResultsSince(start)
634                        .limitResultsBefore(end)
635                        .limitResultsToJid(withJid)
636                        .withAdditionalFormFields(additionalFields)
637                        .build();
638
639        MamQuery mamQuery = queryArchive(mamQueryArgs);
640        return new MamQueryResult(mamQuery);
641    }
642
643    public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException,
644                    NotConnectedException, NotLoggedInException, InterruptedException {
645        String queryId = UUID.randomUUID().toString();
646        String node = mamQueryArgs.node;
647        DataForm dataForm = mamQueryArgs.getDataForm();
648
649        MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm);
650        mamQueryIQ.setType(IQ.Type.set);
651        mamQueryIQ.setTo(archiveAddress);
652
653        mamQueryArgs.maybeAddRsmSet(mamQueryIQ);
654
655        return queryArchive(mamQueryIQ);
656    }
657
658    private static FormField getWithFormField(Jid withJid) {
659        FormField formField = new FormField(FORM_FIELD_WITH);
660        formField.addValue(withJid.toString());
661        return formField;
662    }
663
664    private static void addWithJid(Jid withJid, DataForm dataForm) {
665        if (withJid == null) {
666            return;
667        }
668        FormField formField = getWithFormField(withJid);
669        dataForm.addField(formField);
670    }
671
672    /**
673     * Returns a page of the archive.
674     *
675     * @param dataForm
676     * @param rsmSet
677     * @return the MAM query result
678     * @throws NoResponseException
679     * @throws XMPPErrorException
680     * @throws NotConnectedException
681     * @throws InterruptedException
682     * @throws NotLoggedInException
683     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
684     */
685    @Deprecated
686    // TODO Remove in Smack 4.4
687    public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException,
688                    NotConnectedException, InterruptedException, NotLoggedInException {
689        return page(null, dataForm, rsmSet);
690    }
691
692    /**
693     * Returns a page of the archive. This is a low-level method, you possibly do not want to use it directly unless you
694     * know what you are doing.
695     *
696     * @param node The PubSub node name, can be null
697     * @param dataForm
698     * @param rsmSet
699     * @return the MAM query result
700     * @throws NoResponseException
701     * @throws XMPPErrorException
702     * @throws NotConnectedException
703     * @throws InterruptedException
704     * @throws NotLoggedInException
705     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
706     */
707    @Deprecated
708    // TODO Remove in Smack 4.4
709    public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet)
710                    throws NoResponseException, XMPPErrorException,
711            NotConnectedException, InterruptedException, NotLoggedInException {
712        MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm);
713        mamQueryIQ.setType(IQ.Type.set);
714        mamQueryIQ.setTo(archiveAddress);
715        mamQueryIQ.addExtension(rsmSet);
716        MamQuery mamQuery = queryArchive(mamQueryIQ);
717        return new MamQueryResult(mamQuery);
718    }
719
720    /**
721     * Returns the next page of the archive.
722     *
723     * @param mamQueryResult
724     *            is the previous query result
725     * @param count
726     *            is the amount of messages that a page contains
727     * @return the MAM query result
728     * @throws NoResponseException
729     * @throws XMPPErrorException
730     * @throws NotConnectedException
731     * @throws InterruptedException
732     * @throws NotLoggedInException
733     * @deprecated use {@link MamQuery#pageNext(int)} instead.
734     */
735    @Deprecated
736    // TODO Remove in Smack 4.4
737    public MamQueryResult pageNext(MamQueryResult mamQueryResult, int count) throws NoResponseException,
738            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
739        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
740        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after);
741        return page(mamQueryResult, requestRsmSet);
742    }
743
744    /**
745     * Returns the previous page of the archive.
746     *
747     * @param mamQueryResult
748     *            is the previous query result
749     * @param count
750     *            is the amount of messages that a page contains
751     * @return the MAM query result
752     * @throws NoResponseException
753     * @throws XMPPErrorException
754     * @throws NotConnectedException
755     * @throws InterruptedException
756     * @throws NotLoggedInException
757     * @deprecated use {@link MamQuery#pagePrevious(int)} instead.
758     */
759    @Deprecated
760    // TODO Remove in Smack 4.4
761    public MamQueryResult pagePrevious(MamQueryResult mamQueryResult, int count) throws NoResponseException,
762            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
763        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
764        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before);
765        return page(mamQueryResult, requestRsmSet);
766    }
767
768    private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException,
769                    XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
770        ensureMamQueryResultMatchesThisManager(mamQueryResult);
771
772        return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet);
773    }
774
775    /**
776     * Obtain page before the first message saved (specific chat).
777     * <p>
778     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
779     * </p>
780     *
781     * @param chatJid
782     * @param messageUid the UID of the message of which messages before should be received.
783     * @param max
784     * @return the MAM query result
785     * @throws XMPPErrorException
786     * @throws NotLoggedInException
787     * @throws NotConnectedException
788     * @throws InterruptedException
789     * @throws NoResponseException
790     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
791     */
792    @Deprecated
793    // TODO Remove in Smack 4.4
794    public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
795            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
796        RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1);
797        DataForm dataForm = getNewMamForm();
798        addWithJid(chatJid, dataForm);
799        return page(null, dataForm, rsmSet);
800    }
801
802    /**
803     * Obtain page after the last message saved (specific chat).
804     * <p>
805     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
806     * </p>
807     *
808     * @param chatJid
809     * @param messageUid the UID of the message of which messages after should be received.
810     * @param max
811     * @return the MAM query result
812     * @throws XMPPErrorException
813     * @throws NotLoggedInException
814     * @throws NotConnectedException
815     * @throws InterruptedException
816     * @throws NoResponseException
817     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
818     */
819    @Deprecated
820    // TODO Remove in Smack 4.4
821    public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
822            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
823        RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1);
824        DataForm dataForm = getNewMamForm();
825        addWithJid(chatJid, dataForm);
826        return page(null, dataForm, rsmSet);
827    }
828
829    /**
830     * Obtain the most recent page of a chat.
831     *
832     * @param chatJid
833     * @param max
834     * @return the MAM query result
835     * @throws XMPPErrorException
836     * @throws NotLoggedInException
837     * @throws NotConnectedException
838     * @throws InterruptedException
839     * @throws NoResponseException
840     * @deprecated use {@link #queryMostRecentPage(Jid, int)} instead.
841     */
842    @Deprecated
843    // TODO Remove in Smack 4.4
844    public MamQueryResult mostRecentPage(Jid chatJid, int max) throws XMPPErrorException, NotLoggedInException,
845            NotConnectedException, InterruptedException, NoResponseException {
846        return pageBefore(chatJid, "", max);
847    }
848
849    public MamQuery queryMostRecentPage(Jid jid, int max) throws NoResponseException, XMPPErrorException,
850                    NotConnectedException, NotLoggedInException, InterruptedException {
851        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
852                        // Produces an empty <before/> element for XEP-0059 § 2.5
853                        .queryLastPage()
854                        .limitResultsToJid(jid)
855                        .setResultPageSize(max)
856                        .build();
857        return queryArchive(mamQueryArgs);
858    }
859
860    /**
861     * Get the form fields supported by the server.
862     *
863     * @return the list of form fields.
864     * @throws NoResponseException
865     * @throws XMPPErrorException
866     * @throws NotConnectedException
867     * @throws InterruptedException
868     * @throws NotLoggedInException
869     */
870    public List<FormField> retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException,
871                    InterruptedException, NotLoggedInException {
872        return retrieveFormFields(null);
873    }
874
875    /**
876     * Get the form fields supported by the server.
877     *
878     * @param node The PubSub node name, can be null
879     * @return the list of form fields.
880     * @throws NoResponseException
881     * @throws XMPPErrorException
882     * @throws NotConnectedException
883     * @throws InterruptedException
884     * @throws NotLoggedInException
885     */
886    public List<FormField> retrieveFormFields(String node)
887                    throws NoResponseException, XMPPErrorException, NotConnectedException,
888            InterruptedException, NotLoggedInException {
889        String queryId = UUID.randomUUID().toString();
890        MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null);
891        mamQueryIq.setTo(archiveAddress);
892
893        MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow();
894
895        return mamResponseQueryIq.getDataForm().getFields();
896    }
897
898    private MamQuery queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException,
899                    NotConnectedException, InterruptedException, NotLoggedInException {
900        MamQueryPage mamQueryPage = queryArchivePage(mamQueryIq);
901        return new MamQuery(mamQueryPage, mamQueryIq.getNode(), DataForm.from(mamQueryIq));
902    }
903
904    private MamQueryPage queryArchivePage(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException,
905                    NotConnectedException, InterruptedException, NotLoggedInException {
906        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
907        MamFinIQ mamFinIQ;
908
909        StanzaCollector mamFinIQCollector = connection.createStanzaCollector(new IQReplyFilter(mamQueryIq, connection));
910
911        StanzaCollector.Configuration resultCollectorConfiguration = StanzaCollector.newConfiguration()
912                .setStanzaFilter(new MamResultFilter(mamQueryIq)).setCollectorToReset(mamFinIQCollector);
913        StanzaCollector resultCollector = connection.createStanzaCollector(resultCollectorConfiguration);
914
915        try {
916            connection.sendStanza(mamQueryIq);
917            mamFinIQ = mamFinIQCollector.nextResultOrThrow();
918        } finally {
919            mamFinIQCollector.cancel();
920            resultCollector.cancel();
921        }
922
923        return new MamQueryPage(resultCollector, mamFinIQ);
924    }
925
926    /**
927     * MAM query result class.
928     *
929     */
930    @Deprecated
931    public static final class MamQueryResult {
932        public final List<Forwarded> forwardedMessages;
933        public final MamFinIQ mamFin;
934        private final String node;
935        private final DataForm form;
936
937        private MamQueryResult(MamQuery mamQuery) {
938            this(mamQuery.mamQueryPage.forwardedMessages, mamQuery.mamQueryPage.mamFin, mamQuery.node, mamQuery.form);
939        }
940
941        private MamQueryResult(List<Forwarded> forwardedMessages, MamFinIQ mamFin, String node, DataForm form) {
942            this.forwardedMessages = forwardedMessages;
943            this.mamFin = mamFin;
944            this.node = node;
945            this.form = form;
946        }
947    }
948
949    public final class MamQuery {
950        private final String node;
951        private final DataForm form;
952
953        private MamQueryPage mamQueryPage;
954
955        private MamQuery(MamQueryPage mamQueryPage, String node, DataForm form) {
956            this.node = node;
957            this.form = form;
958
959            this.mamQueryPage = mamQueryPage;
960        }
961
962        public boolean isComplete() {
963            return mamQueryPage.getMamFinIq().isComplete();
964        }
965
966        public List<Message> getMessages() {
967            return mamQueryPage.messages;
968        }
969
970        public List<MamResultExtension> getMamResultExtensions() {
971            return mamQueryPage.mamResultExtensions;
972        }
973
974        private List<Message> page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException,
975                        NotConnectedException, NotLoggedInException, InterruptedException {
976            MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, form);
977            mamQueryIQ.setType(IQ.Type.set);
978            mamQueryIQ.setTo(archiveAddress);
979            mamQueryIQ.addExtension(requestRsmSet);
980
981            mamQueryPage = queryArchivePage(mamQueryIQ);
982
983            return mamQueryPage.messages;
984        }
985
986        private RSMSet getPreviousRsmSet() {
987            return mamQueryPage.getMamFinIq().getRSMSet();
988        }
989
990        public List<Message> pageNext(int count) throws NoResponseException, XMPPErrorException, NotConnectedException,
991                        NotLoggedInException, InterruptedException {
992            RSMSet previousResultRsmSet = getPreviousRsmSet();
993            RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after);
994            return page(requestRsmSet);
995        }
996
997        public List<Message> pagePrevious(int count) throws NoResponseException, XMPPErrorException,
998                        NotConnectedException, NotLoggedInException, InterruptedException {
999            RSMSet previousResultRsmSet = getPreviousRsmSet();
1000            RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before);
1001            return page(requestRsmSet);
1002        }
1003
1004        public int getMessageCount() {
1005            return getMessages().size();
1006        }
1007
1008        public MamQueryPage getPage() {
1009            return mamQueryPage;
1010        }
1011    }
1012
1013    public static final class MamQueryPage {
1014        private final MamFinIQ mamFin;
1015        private final List<Message> mamResultCarrierMessages;
1016        private final List<MamResultExtension> mamResultExtensions;
1017        private final List<Forwarded> forwardedMessages;
1018        private final List<Message> messages;
1019
1020        private MamQueryPage(StanzaCollector stanzaCollector, MamFinIQ mamFin) {
1021            this.mamFin = mamFin;
1022
1023            List<Stanza> mamResultCarrierStanzas = stanzaCollector.getCollectedStanzasAfterCancelled();
1024
1025            List<Message> mamResultCarrierMessages = new ArrayList<>(mamResultCarrierStanzas.size());
1026            List<MamResultExtension> mamResultExtensions = new ArrayList<>(mamResultCarrierStanzas.size());
1027            List<Forwarded> forwardedMessages = new ArrayList<>(mamResultCarrierStanzas.size());
1028
1029            for (Stanza mamResultStanza : mamResultCarrierStanzas) {
1030                Message resultMessage = (Message) mamResultStanza;
1031
1032                mamResultCarrierMessages.add(resultMessage);
1033
1034                MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage);
1035                mamResultExtensions.add(mamResultExtension);
1036
1037                forwardedMessages.add(mamResultExtension.getForwarded());
1038            }
1039
1040            this.mamResultCarrierMessages = Collections.unmodifiableList(mamResultCarrierMessages);
1041            this.mamResultExtensions = Collections.unmodifiableList(mamResultExtensions);
1042            this.forwardedMessages = Collections.unmodifiableList(forwardedMessages);
1043            this.messages = Collections.unmodifiableList(Forwarded.extractMessagesFrom(forwardedMessages));
1044        }
1045
1046        public List<Message> getMessages() {
1047            return messages;
1048        }
1049
1050        public List<Forwarded> getForwarded() {
1051            return forwardedMessages;
1052        }
1053
1054        public List<MamResultExtension> getMamResultExtensions() {
1055            return mamResultExtensions;
1056        }
1057
1058        public List<Message> getMamResultCarrierMessages() {
1059            return mamResultCarrierMessages;
1060        }
1061
1062        public MamFinIQ getMamFinIq() {
1063            return mamFin;
1064        }
1065    }
1066
1067    private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) {
1068        EntityFullJid localAddress = connection().getUser();
1069        EntityBareJid localBareAddress = null;
1070        if (localAddress != null) {
1071            localBareAddress = localAddress.asEntityBareJid();
1072        }
1073        boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress);
1074
1075        Jid finIqFrom = mamQueryResult.mamFin.getFrom();
1076
1077        if (finIqFrom != null) {
1078            if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) {
1079                return;
1080            }
1081            throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom
1082                            + "' whereas this MamManager is responsible for '" + archiveAddress + '\'');
1083        }
1084        else if (!isLocalUserArchive) {
1085            throw new IllegalArgumentException(
1086                            "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '"
1087                                            + archiveAddress + '\'');
1088        }
1089    }
1090
1091    /**
1092     * Check if this MamManager's archive address supports MAM.
1093     *
1094     * @return true if MAM is supported, <code>false</code>otherwise.
1095     *
1096     * @throws NoResponseException
1097     * @throws XMPPErrorException
1098     * @throws NotConnectedException
1099     * @throws InterruptedException
1100     * @since 4.2.1
1101     * @see <a href="https://xmpp.org/extensions/xep-0313.html#support">XEP-0313 § 7. Determining support</a>
1102     */
1103    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1104        // Note that this may return 'null' but SDM's supportsFeature() does the right thing™ then.
1105        Jid archiveAddress = getArchiveAddress();
1106        return serviceDiscoveryManager.supportsFeature(archiveAddress, MamElements.NAMESPACE);
1107    }
1108
1109    private static DataForm getNewMamForm() {
1110        FormField field = new FormField(FormField.FORM_TYPE);
1111        field.setType(FormField.Type.hidden);
1112        field.addValue(MamElements.NAMESPACE);
1113        DataForm form = new DataForm(DataForm.Type.submit);
1114        form.addField(field);
1115        return form;
1116    }
1117
1118    /**
1119     * Lookup the archive's message ID of the latest message in the archive. Returns {@code null} if the archive is
1120     * empty.
1121     *
1122     * @return the ID of the lastest message or {@code null}.
1123     * @throws NoResponseException
1124     * @throws XMPPErrorException
1125     * @throws NotConnectedException
1126     * @throws NotLoggedInException
1127     * @throws InterruptedException
1128     * @since 4.3.0
1129     */
1130    public String getMessageUidOfLatestMessage() throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
1131        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
1132                .setResultPageSize(1)
1133                .queryLastPage()
1134                .build();
1135
1136        MamQuery mamQuery = queryArchive(mamQueryArgs);
1137        if (mamQuery.getMessages().isEmpty()) {
1138            return null;
1139        }
1140
1141        return mamQuery.getMamResultExtensions().get(0).getId();
1142    }
1143
1144    /**
1145     * Get the preferences stored in the server.
1146     *
1147     * @return the MAM preferences result
1148     * @throws NoResponseException
1149     * @throws XMPPErrorException
1150     * @throws NotConnectedException
1151     * @throws InterruptedException
1152     * @throws NotLoggedInException
1153     */
1154    public MamPrefsResult retrieveArchivingPreferences() throws NoResponseException, XMPPErrorException,
1155            NotConnectedException, InterruptedException, NotLoggedInException {
1156        MamPrefsIQ mamPrefIQ = new MamPrefsIQ();
1157        return queryMamPrefs(mamPrefIQ);
1158    }
1159
1160    /**
1161     * Update the preferences in the server.
1162     *
1163     * @param alwaysJids
1164     *            is the list of JIDs that should always have messages to/from
1165     *            archived in the user's store
1166     * @param neverJids
1167     *            is the list of JIDs that should never have messages to/from
1168     *            archived in the user's store
1169     * @param defaultBehavior
1170     *            can be "roster", "always", "never" (see XEP-0313)
1171     * @return the MAM preferences result
1172     * @throws NoResponseException
1173     * @throws XMPPErrorException
1174     * @throws NotConnectedException
1175     * @throws InterruptedException
1176     * @throws NotLoggedInException
1177     * @deprecated use {@link #updateArchivingPreferences(MamPrefs)} instead.
1178     */
1179    @Deprecated
1180    public MamPrefsResult updateArchivingPreferences(List<Jid> alwaysJids, List<Jid> neverJids, DefaultBehavior defaultBehavior)
1181            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
1182            NotLoggedInException {
1183        Objects.requireNonNull(defaultBehavior, "Default behavior must be set");
1184        MamPrefsIQ mamPrefIQ = new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior);
1185        return queryMamPrefs(mamPrefIQ);
1186    }
1187
1188    /**
1189     * Update the preferences in the server.
1190     *
1191     * @param mamPrefs
1192     * @return the currently active preferences after the operation.
1193     * @throws NoResponseException
1194     * @throws XMPPErrorException
1195     * @throws NotConnectedException
1196     * @throws InterruptedException
1197     * @throws NotLoggedInException
1198     * @since 4.3.0
1199     */
1200    public MamPrefsResult updateArchivingPreferences(MamPrefs mamPrefs) throws NoResponseException, XMPPErrorException,
1201                    NotConnectedException, InterruptedException, NotLoggedInException {
1202        MamPrefsIQ mamPrefIQ = mamPrefs.constructMamPrefsIq();
1203        return queryMamPrefs(mamPrefIQ);
1204    }
1205
1206    public MamPrefsResult enableMamForAllMessages() throws NoResponseException, XMPPErrorException,
1207                    NotConnectedException, NotLoggedInException, InterruptedException {
1208        return setDefaultBehavior(DefaultBehavior.always);
1209    }
1210
1211    public MamPrefsResult enableMamForRosterMessages() throws NoResponseException, XMPPErrorException,
1212                    NotConnectedException, NotLoggedInException, InterruptedException {
1213        return setDefaultBehavior(DefaultBehavior.roster);
1214    }
1215
1216    public MamPrefsResult setDefaultBehavior(DefaultBehavior desiredDefaultBehavior) throws NoResponseException,
1217                    XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
1218        MamPrefsResult mamPrefsResult = retrieveArchivingPreferences();
1219        if (mamPrefsResult.mamPrefs.getDefault() == desiredDefaultBehavior) {
1220            return mamPrefsResult;
1221        }
1222
1223        MamPrefs mamPrefs = mamPrefsResult.asMamPrefs();
1224        mamPrefs.setDefaultBehavior(desiredDefaultBehavior);
1225        return updateArchivingPreferences(mamPrefs);
1226    }
1227
1228    /**
1229     * MAM preferences result class.
1230     *
1231     */
1232    public static final class MamPrefsResult {
1233        public final MamPrefsIQ mamPrefs;
1234        public final DataForm form;
1235
1236        private MamPrefsResult(MamPrefsIQ mamPrefs, DataForm form) {
1237            this.mamPrefs = mamPrefs;
1238            this.form = form;
1239        }
1240
1241        public MamPrefs asMamPrefs() {
1242            return new MamPrefs(this);
1243        }
1244    }
1245
1246    public static final class MamPrefs {
1247        private final List<Jid> alwaysJids;
1248        private final List<Jid> neverJids;
1249        private DefaultBehavior defaultBehavior;
1250
1251        private MamPrefs(MamPrefsResult mamPrefsResult) {
1252            MamPrefsIQ mamPrefsIq = mamPrefsResult.mamPrefs;
1253            this.alwaysJids = new ArrayList<>(mamPrefsIq.getAlwaysJids());
1254            this.neverJids = new ArrayList<>(mamPrefsIq.getNeverJids());
1255            this.defaultBehavior = mamPrefsIq.getDefault();
1256        }
1257
1258        public void setDefaultBehavior(DefaultBehavior defaultBehavior) {
1259            this.defaultBehavior = Objects.requireNonNull(defaultBehavior, "defaultBehavior must not be null");
1260        }
1261
1262        public DefaultBehavior getDefaultBehavior() {
1263            return defaultBehavior;
1264        }
1265
1266        public List<Jid> getAlwaysJids() {
1267            return alwaysJids;
1268        }
1269
1270        public List<Jid> getNeverJids() {
1271            return neverJids;
1272        }
1273
1274        private MamPrefsIQ constructMamPrefsIq() {
1275            return new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior);
1276        }
1277    }
1278
1279    private MamPrefsResult queryMamPrefs(MamPrefsIQ mamPrefsIQ) throws NoResponseException, XMPPErrorException,
1280            NotConnectedException, InterruptedException, NotLoggedInException {
1281        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
1282
1283        MamPrefsIQ mamPrefsResultIQ = connection.createStanzaCollectorAndSend(mamPrefsIQ).nextResultOrThrow();
1284
1285        return new MamPrefsResult(mamPrefsResultIQ, DataForm.from(mamPrefsIQ));
1286    }
1287
1288}