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}