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