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 * .withJid(jid) 118 * .setResultPageSize(10) 119 * .queryRecentPage() 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 public Builder queryLastPage() { 439 return beforeUid(""); 440 } 441 442 public MamQueryArgs build() { 443 return new MamQueryArgs(this); 444 } 445 } 446 } 447 448 /** 449 * Query archive with a maximum amount of results. 450 * 451 * @param max 452 * @return the MAM query result 453 * @throws NoResponseException 454 * @throws XMPPErrorException 455 * @throws NotConnectedException 456 * @throws InterruptedException 457 * @throws NotLoggedInException 458 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 459 */ 460 @Deprecated 461 // TODO Remove in Smack 4.4 462 public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException, 463 NotConnectedException, InterruptedException, NotLoggedInException { 464 return queryArchive(null, max, null, null, null, null); 465 } 466 467 /** 468 * Query archive with a JID (only messages from/to the JID). 469 * 470 * @param withJid 471 * @return the MAM query result 472 * @throws NoResponseException 473 * @throws XMPPErrorException 474 * @throws NotConnectedException 475 * @throws InterruptedException 476 * @throws NotLoggedInException 477 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 478 */ 479 @Deprecated 480 // TODO Remove in Smack 4.4 481 public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException, 482 NotConnectedException, InterruptedException, NotLoggedInException { 483 return queryArchive(null, null, null, null, withJid, null); 484 } 485 486 /** 487 * Query archive filtering by start and/or end date. If start == null, the 488 * value of 'start' will be equal to the date/time of the earliest message 489 * stored in the archive. If end == null, the value of 'end' will be equal 490 * to the date/time of the most recent message stored in the archive. 491 * 492 * @param start 493 * @param end 494 * @return the MAM query result 495 * @throws NoResponseException 496 * @throws XMPPErrorException 497 * @throws NotConnectedException 498 * @throws InterruptedException 499 * @throws NotLoggedInException 500 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 501 */ 502 @Deprecated 503 // TODO Remove in Smack 4.4 504 public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException, 505 NotConnectedException, InterruptedException, NotLoggedInException { 506 return queryArchive(null, null, start, end, null, null); 507 } 508 509 /** 510 * Query Archive adding filters with additional fields. 511 * 512 * @param additionalFields 513 * @return the MAM query result 514 * @throws NoResponseException 515 * @throws XMPPErrorException 516 * @throws NotConnectedException 517 * @throws InterruptedException 518 * @throws NotLoggedInException 519 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 520 */ 521 @Deprecated 522 // TODO Remove in Smack 4.4 523 public MamQueryResult queryArchive(List<FormField> additionalFields) throws NoResponseException, XMPPErrorException, 524 NotConnectedException, InterruptedException, NotLoggedInException { 525 return queryArchive(null, null, null, null, null, additionalFields); 526 } 527 528 /** 529 * Query archive filtering by start date. The value of 'end' will be equal 530 * to the date/time of the most recent message stored in the archive. 531 * 532 * @param start 533 * @return the MAM query result 534 * @throws NoResponseException 535 * @throws XMPPErrorException 536 * @throws NotConnectedException 537 * @throws InterruptedException 538 * @throws NotLoggedInException 539 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 540 */ 541 @Deprecated 542 // TODO Remove in Smack 4.4 543 public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException, 544 NotConnectedException, InterruptedException, NotLoggedInException { 545 return queryArchive(null, null, start, null, null, null); 546 } 547 548 /** 549 * Query archive filtering by end date. The value of 'start' will be equal 550 * to the date/time of the earliest message stored in the archive. 551 * 552 * @param end 553 * @return the MAM query result 554 * @throws NoResponseException 555 * @throws XMPPErrorException 556 * @throws NotConnectedException 557 * @throws InterruptedException 558 * @throws NotLoggedInException 559 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 560 */ 561 @Deprecated 562 // TODO Remove in Smack 4.4 563 public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException, 564 NotConnectedException, InterruptedException, NotLoggedInException { 565 return queryArchive(null, null, null, end, null, null); 566 } 567 568 569 /** 570 * Query archive applying filters: max count, start date, end date, from/to 571 * JID and with additional fields. 572 * 573 * @param max 574 * @param start 575 * @param end 576 * @param withJid 577 * @param additionalFields 578 * @return the MAM query result 579 * @throws NoResponseException 580 * @throws XMPPErrorException 581 * @throws NotConnectedException 582 * @throws InterruptedException 583 * @throws NotLoggedInException 584 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 585 */ 586 @Deprecated 587 // TODO Remove in Smack 4.4 588 public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List<FormField> additionalFields) 589 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, 590 NotLoggedInException { 591 return queryArchive(null, max, start, end, withJid, additionalFields); 592 } 593 594 595 /** 596 * Query an message archive like a MUC archive or a PubSub node archive, addressed by an archiveAddress, applying 597 * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the 598 * default, the server will be requested. 599 * 600 * @param node The PubSub node name, can be null 601 * @param max 602 * @param start 603 * @param end 604 * @param withJid 605 * @param additionalFields 606 * @return the MAM query result 607 * @throws NoResponseException 608 * @throws XMPPErrorException 609 * @throws NotConnectedException 610 * @throws InterruptedException 611 * @throws NotLoggedInException 612 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 613 */ 614 @Deprecated 615 // TODO Remove in Smack 4.4 616 public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid, 617 List<FormField> additionalFields) 618 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, 619 NotLoggedInException { 620 MamQueryArgs mamQueryArgs = MamQueryArgs.builder() 621 .queryNode(node) 622 .setResultPageSize(max) 623 .limitResultsSince(start) 624 .limitResultsBefore(end) 625 .limitResultsToJid(withJid) 626 .withAdditionalFormFields(additionalFields) 627 .build(); 628 629 MamQuery mamQuery = queryArchive(mamQueryArgs); 630 return new MamQueryResult(mamQuery); 631 } 632 633 public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException, 634 NotConnectedException, NotLoggedInException, InterruptedException { 635 String queryId = UUID.randomUUID().toString(); 636 String node = mamQueryArgs.node; 637 DataForm dataForm = mamQueryArgs.getDataForm(); 638 639 MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm); 640 mamQueryIQ.setType(IQ.Type.set); 641 mamQueryIQ.setTo(archiveAddress); 642 643 mamQueryArgs.maybeAddRsmSet(mamQueryIQ); 644 645 return queryArchive(mamQueryIQ); 646 } 647 648 private static FormField getWithFormField(Jid withJid) { 649 FormField formField = new FormField(FORM_FIELD_WITH); 650 formField.addValue(withJid.toString()); 651 return formField; 652 } 653 654 private static void addWithJid(Jid withJid, DataForm dataForm) { 655 if (withJid == null) { 656 return; 657 } 658 FormField formField = getWithFormField(withJid); 659 dataForm.addField(formField); 660 } 661 662 /** 663 * Returns a page of the archive. 664 * 665 * @param dataForm 666 * @param rsmSet 667 * @return the MAM query result 668 * @throws NoResponseException 669 * @throws XMPPErrorException 670 * @throws NotConnectedException 671 * @throws InterruptedException 672 * @throws NotLoggedInException 673 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 674 */ 675 @Deprecated 676 // TODO Remove in Smack 4.4 677 public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException, 678 NotConnectedException, InterruptedException, NotLoggedInException { 679 return page(null, dataForm, rsmSet); 680 } 681 682 /** 683 * Returns a page of the archive. This is a low-level method, you possibly do not want to use it directly unless you 684 * know what you are doing. 685 * 686 * @param node The PubSub node name, can be null 687 * @param dataForm 688 * @param rsmSet 689 * @return the MAM query result 690 * @throws NoResponseException 691 * @throws XMPPErrorException 692 * @throws NotConnectedException 693 * @throws InterruptedException 694 * @throws NotLoggedInException 695 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 696 */ 697 @Deprecated 698 // TODO Remove in Smack 4.4 699 public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet) 700 throws NoResponseException, XMPPErrorException, 701 NotConnectedException, InterruptedException, NotLoggedInException { 702 MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm); 703 mamQueryIQ.setType(IQ.Type.set); 704 mamQueryIQ.setTo(archiveAddress); 705 mamQueryIQ.addExtension(rsmSet); 706 MamQuery mamQuery = queryArchive(mamQueryIQ); 707 return new MamQueryResult(mamQuery); 708 } 709 710 /** 711 * Returns the next page of the archive. 712 * 713 * @param mamQueryResult 714 * is the previous query result 715 * @param count 716 * is the amount of messages that a page contains 717 * @return the MAM query result 718 * @throws NoResponseException 719 * @throws XMPPErrorException 720 * @throws NotConnectedException 721 * @throws InterruptedException 722 * @throws NotLoggedInException 723 * @deprecated use {@link MamQuery#pageNext(int)} instead. 724 */ 725 @Deprecated 726 // TODO Remove in Smack 4.4 727 public MamQueryResult pageNext(MamQueryResult mamQueryResult, int count) throws NoResponseException, 728 XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { 729 RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); 730 RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after); 731 return page(mamQueryResult, requestRsmSet); 732 } 733 734 /** 735 * Returns the previous page of the archive. 736 * 737 * @param mamQueryResult 738 * is the previous query result 739 * @param count 740 * is the amount of messages that a page contains 741 * @return the MAM query result 742 * @throws NoResponseException 743 * @throws XMPPErrorException 744 * @throws NotConnectedException 745 * @throws InterruptedException 746 * @throws NotLoggedInException 747 * @deprecated use {@link MamQuery#pagePrevious(int)} instead. 748 */ 749 @Deprecated 750 // TODO Remove in Smack 4.4 751 public MamQueryResult pagePrevious(MamQueryResult mamQueryResult, int count) throws NoResponseException, 752 XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { 753 RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); 754 RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before); 755 return page(mamQueryResult, requestRsmSet); 756 } 757 758 private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException, 759 XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { 760 ensureMamQueryResultMatchesThisManager(mamQueryResult); 761 762 return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet); 763 } 764 765 /** 766 * Obtain page before the first message saved (specific chat). 767 * <p> 768 * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message. 769 * </p> 770 * 771 * @param chatJid 772 * @param messageUid the UID of the message of which messages before should be received. 773 * @param max 774 * @return the MAM query result 775 * @throws XMPPErrorException 776 * @throws NotLoggedInException 777 * @throws NotConnectedException 778 * @throws InterruptedException 779 * @throws NoResponseException 780 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 781 */ 782 @Deprecated 783 // TODO Remove in Smack 4.4 784 public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException, 785 NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { 786 RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1); 787 DataForm dataForm = getNewMamForm(); 788 addWithJid(chatJid, dataForm); 789 return page(null, dataForm, rsmSet); 790 } 791 792 /** 793 * Obtain page after the last message saved (specific chat). 794 * <p> 795 * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message. 796 * </p> 797 * 798 * @param chatJid 799 * @param messageUid the UID of the message of which messages after should be received. 800 * @param max 801 * @return the MAM query result 802 * @throws XMPPErrorException 803 * @throws NotLoggedInException 804 * @throws NotConnectedException 805 * @throws InterruptedException 806 * @throws NoResponseException 807 * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. 808 */ 809 @Deprecated 810 // TODO Remove in Smack 4.4 811 public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException, 812 NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { 813 RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1); 814 DataForm dataForm = getNewMamForm(); 815 addWithJid(chatJid, dataForm); 816 return page(null, dataForm, rsmSet); 817 } 818 819 /** 820 * Obtain the most recent page of a chat. 821 * 822 * @param chatJid 823 * @param max 824 * @return the MAM query result 825 * @throws XMPPErrorException 826 * @throws NotLoggedInException 827 * @throws NotConnectedException 828 * @throws InterruptedException 829 * @throws NoResponseException 830 * @deprecated use {@link #queryMostRecentPage(Jid, int)} instead. 831 */ 832 @Deprecated 833 // TODO Remove in Smack 4.4 834 public MamQueryResult mostRecentPage(Jid chatJid, int max) throws XMPPErrorException, NotLoggedInException, 835 NotConnectedException, InterruptedException, NoResponseException { 836 return pageBefore(chatJid, "", max); 837 } 838 839 public MamQuery queryMostRecentPage(Jid jid, int max) throws NoResponseException, XMPPErrorException, 840 NotConnectedException, NotLoggedInException, InterruptedException { 841 MamQueryArgs mamQueryArgs = MamQueryArgs.builder() 842 // Produces an empty <before/> element for XEP-0059 § 2.5 843 .queryLastPage() 844 .limitResultsToJid(jid) 845 .setResultPageSize(max) 846 .build(); 847 return queryArchive(mamQueryArgs); 848 } 849 850 /** 851 * Get the form fields supported by the server. 852 * 853 * @return the list of form fields. 854 * @throws NoResponseException 855 * @throws XMPPErrorException 856 * @throws NotConnectedException 857 * @throws InterruptedException 858 * @throws NotLoggedInException 859 */ 860 public List<FormField> retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException, 861 InterruptedException, NotLoggedInException { 862 return retrieveFormFields(null); 863 } 864 865 /** 866 * Get the form fields supported by the server. 867 * 868 * @param node The PubSub node name, can be null 869 * @return the list of form fields. 870 * @throws NoResponseException 871 * @throws XMPPErrorException 872 * @throws NotConnectedException 873 * @throws InterruptedException 874 * @throws NotLoggedInException 875 */ 876 public List<FormField> retrieveFormFields(String node) 877 throws NoResponseException, XMPPErrorException, NotConnectedException, 878 InterruptedException, NotLoggedInException { 879 String queryId = UUID.randomUUID().toString(); 880 MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null); 881 mamQueryIq.setTo(archiveAddress); 882 883 MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow(); 884 885 return mamResponseQueryIq.getDataForm().getFields(); 886 } 887 888 private MamQuery queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException, 889 NotConnectedException, InterruptedException, NotLoggedInException { 890 MamQueryPage mamQueryPage = queryArchivePage(mamQueryIq); 891 return new MamQuery(mamQueryPage, mamQueryIq.getNode(), DataForm.from(mamQueryIq)); 892 } 893 894 private MamQueryPage queryArchivePage(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException, 895 NotConnectedException, InterruptedException, NotLoggedInException { 896 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 897 MamFinIQ mamFinIQ; 898 899 StanzaCollector mamFinIQCollector = connection.createStanzaCollector(new IQReplyFilter(mamQueryIq, connection)); 900 901 StanzaCollector.Configuration resultCollectorConfiguration = StanzaCollector.newConfiguration() 902 .setStanzaFilter(new MamResultFilter(mamQueryIq)).setCollectorToReset(mamFinIQCollector); 903 StanzaCollector resultCollector = connection.createStanzaCollector(resultCollectorConfiguration); 904 905 try { 906 connection.sendStanza(mamQueryIq); 907 mamFinIQ = mamFinIQCollector.nextResultOrThrow(); 908 } finally { 909 mamFinIQCollector.cancel(); 910 resultCollector.cancel(); 911 } 912 913 return new MamQueryPage(resultCollector, mamFinIQ); 914 } 915 916 /** 917 * MAM query result class. 918 * 919 */ 920 @Deprecated 921 public static final class MamQueryResult { 922 public final List<Forwarded> forwardedMessages; 923 public final MamFinIQ mamFin; 924 private final String node; 925 private final DataForm form; 926 927 private MamQueryResult(MamQuery mamQuery) { 928 this(mamQuery.mamQueryPage.forwardedMessages, mamQuery.mamQueryPage.mamFin, mamQuery.node, mamQuery.form); 929 } 930 931 private MamQueryResult(List<Forwarded> forwardedMessages, MamFinIQ mamFin, String node, DataForm form) { 932 this.forwardedMessages = forwardedMessages; 933 this.mamFin = mamFin; 934 this.node = node; 935 this.form = form; 936 } 937 } 938 939 public final class MamQuery { 940 private final String node; 941 private final DataForm form; 942 943 private MamQueryPage mamQueryPage; 944 945 private MamQuery(MamQueryPage mamQueryPage, String node, DataForm form) { 946 this.node = node; 947 this.form = form; 948 949 this.mamQueryPage = mamQueryPage; 950 } 951 952 public boolean isComplete() { 953 return mamQueryPage.getMamFinIq().isComplete(); 954 } 955 956 public List<Message> getMessages() { 957 return mamQueryPage.messages; 958 } 959 960 public List<MamResultExtension> getMamResultExtensions() { 961 return mamQueryPage.mamResultExtensions; 962 } 963 964 private List<Message> page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException, 965 NotConnectedException, NotLoggedInException, InterruptedException { 966 MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, form); 967 mamQueryIQ.setType(IQ.Type.set); 968 mamQueryIQ.setTo(archiveAddress); 969 mamQueryIQ.addExtension(requestRsmSet); 970 971 mamQueryPage = queryArchivePage(mamQueryIQ); 972 973 return mamQueryPage.messages; 974 } 975 976 private RSMSet getPreviousRsmSet() { 977 return mamQueryPage.getMamFinIq().getRSMSet(); 978 } 979 980 public List<Message> pageNext(int count) throws NoResponseException, XMPPErrorException, NotConnectedException, 981 NotLoggedInException, InterruptedException { 982 RSMSet previousResultRsmSet = getPreviousRsmSet(); 983 RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after); 984 return page(requestRsmSet); 985 } 986 987 public List<Message> pagePrevious(int count) throws NoResponseException, XMPPErrorException, 988 NotConnectedException, NotLoggedInException, InterruptedException { 989 RSMSet previousResultRsmSet = getPreviousRsmSet(); 990 RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.before); 991 return page(requestRsmSet); 992 } 993 994 public int getMessageCount() { 995 return getMessages().size(); 996 } 997 998 public MamQueryPage getPage() { 999 return mamQueryPage; 1000 } 1001 } 1002 1003 public static final class MamQueryPage { 1004 private final MamFinIQ mamFin; 1005 private final List<Message> mamResultCarrierMessages; 1006 private final List<MamResultExtension> mamResultExtensions; 1007 private final List<Forwarded> forwardedMessages; 1008 private final List<Message> messages; 1009 1010 private MamQueryPage(StanzaCollector stanzaCollector, MamFinIQ mamFin) { 1011 this.mamFin = mamFin; 1012 1013 List<Stanza> mamResultCarrierStanzas = stanzaCollector.getCollectedStanzasAfterCancelled(); 1014 1015 List<Message> mamResultCarrierMessages = new ArrayList<>(mamResultCarrierStanzas.size()); 1016 List<MamResultExtension> mamResultExtensions = new ArrayList<>(mamResultCarrierStanzas.size()); 1017 List<Forwarded> forwardedMessages = new ArrayList<>(mamResultCarrierStanzas.size()); 1018 1019 for (Stanza mamResultStanza : mamResultCarrierStanzas) { 1020 Message resultMessage = (Message) mamResultStanza; 1021 1022 mamResultCarrierMessages.add(resultMessage); 1023 1024 MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage); 1025 mamResultExtensions.add(mamResultExtension); 1026 1027 forwardedMessages.add(mamResultExtension.getForwarded()); 1028 } 1029 1030 this.mamResultCarrierMessages = Collections.unmodifiableList(mamResultCarrierMessages); 1031 this.mamResultExtensions = Collections.unmodifiableList(mamResultExtensions); 1032 this.forwardedMessages = Collections.unmodifiableList(forwardedMessages); 1033 this.messages = Collections.unmodifiableList(Forwarded.extractMessagesFrom(forwardedMessages)); 1034 } 1035 1036 public List<Message> getMessages() { 1037 return messages; 1038 } 1039 1040 public List<Forwarded> getForwarded() { 1041 return forwardedMessages; 1042 } 1043 1044 public List<MamResultExtension> getMamResultExtensions() { 1045 return mamResultExtensions; 1046 } 1047 1048 public List<Message> getMamResultCarrierMessages() { 1049 return mamResultCarrierMessages; 1050 } 1051 1052 public MamFinIQ getMamFinIq() { 1053 return mamFin; 1054 } 1055 } 1056 1057 private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) { 1058 EntityFullJid localAddress = connection().getUser(); 1059 EntityBareJid localBareAddress = null; 1060 if (localAddress != null) { 1061 localBareAddress = localAddress.asEntityBareJid(); 1062 } 1063 boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress); 1064 1065 Jid finIqFrom = mamQueryResult.mamFin.getFrom(); 1066 1067 if (finIqFrom != null) { 1068 if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) { 1069 return; 1070 } 1071 throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom 1072 + "' whereas this MamManager is responsible for '" + archiveAddress + '\''); 1073 } 1074 else if (!isLocalUserArchive) { 1075 throw new IllegalArgumentException( 1076 "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '" 1077 + archiveAddress + '\''); 1078 } 1079 } 1080 1081 /** 1082 * Check if this MamManager's archive address supports MAM. 1083 * 1084 * @return true if MAM is supported, <code>false</code>otherwise. 1085 * 1086 * @throws NoResponseException 1087 * @throws XMPPErrorException 1088 * @throws NotConnectedException 1089 * @throws InterruptedException 1090 * @since 4.2.1 1091 * @see <a href="https://xmpp.org/extensions/xep-0313.html#support">XEP-0313 § 7. Determining support</a> 1092 */ 1093 public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1094 // Note that this may return 'null' but SDM's supportsFeature() does the right thing™ then. 1095 Jid archiveAddress = getArchiveAddress(); 1096 return serviceDiscoveryManager.supportsFeature(archiveAddress, MamElements.NAMESPACE); 1097 } 1098 1099 private static DataForm getNewMamForm() { 1100 FormField field = new FormField(FormField.FORM_TYPE); 1101 field.setType(FormField.Type.hidden); 1102 field.addValue(MamElements.NAMESPACE); 1103 DataForm form = new DataForm(DataForm.Type.submit); 1104 form.addField(field); 1105 return form; 1106 } 1107 1108 /** 1109 * Lookup the archive's message ID of the latest message in the archive. Returns {@code null} if the archive is 1110 * empty. 1111 * 1112 * @return the ID of the lastest message or {@code null}. 1113 * @throws NoResponseException 1114 * @throws XMPPErrorException 1115 * @throws NotConnectedException 1116 * @throws NotLoggedInException 1117 * @throws InterruptedException 1118 * @since 4.3.0 1119 */ 1120 public String getMessageUidOfLatestMessage() throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { 1121 MamQueryArgs mamQueryArgs = MamQueryArgs.builder() 1122 .setResultPageSize(1) 1123 .queryLastPage() 1124 .build(); 1125 1126 MamQuery mamQuery = queryArchive(mamQueryArgs); 1127 if (mamQuery.getMessages().isEmpty()) { 1128 return null; 1129 } 1130 1131 return mamQuery.getMamResultExtensions().get(0).getId(); 1132 } 1133 1134 /** 1135 * Get the preferences stored in the server. 1136 * 1137 * @return the MAM preferences result 1138 * @throws NoResponseException 1139 * @throws XMPPErrorException 1140 * @throws NotConnectedException 1141 * @throws InterruptedException 1142 * @throws NotLoggedInException 1143 */ 1144 public MamPrefsResult retrieveArchivingPreferences() throws NoResponseException, XMPPErrorException, 1145 NotConnectedException, InterruptedException, NotLoggedInException { 1146 MamPrefsIQ mamPrefIQ = new MamPrefsIQ(); 1147 return queryMamPrefs(mamPrefIQ); 1148 } 1149 1150 /** 1151 * Update the preferences in the server. 1152 * 1153 * @param alwaysJids 1154 * is the list of JIDs that should always have messages to/from 1155 * archived in the user's store 1156 * @param neverJids 1157 * is the list of JIDs that should never have messages to/from 1158 * archived in the user's store 1159 * @param defaultBehavior 1160 * can be "roster", "always", "never" (see XEP-0313) 1161 * @return the MAM preferences result 1162 * @throws NoResponseException 1163 * @throws XMPPErrorException 1164 * @throws NotConnectedException 1165 * @throws InterruptedException 1166 * @throws NotLoggedInException 1167 * @deprecated use {@link #updateArchivingPreferences(MamPrefs)} instead. 1168 */ 1169 @Deprecated 1170 public MamPrefsResult updateArchivingPreferences(List<Jid> alwaysJids, List<Jid> neverJids, DefaultBehavior defaultBehavior) 1171 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, 1172 NotLoggedInException { 1173 Objects.requireNonNull(defaultBehavior, "Default behavior must be set"); 1174 MamPrefsIQ mamPrefIQ = new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior); 1175 return queryMamPrefs(mamPrefIQ); 1176 } 1177 1178 /** 1179 * Update the preferences in the server. 1180 * 1181 * @param mamPrefs 1182 * @return the currently active preferences after the operation. 1183 * @throws NoResponseException 1184 * @throws XMPPErrorException 1185 * @throws NotConnectedException 1186 * @throws InterruptedException 1187 * @throws NotLoggedInException 1188 * @since 4.3.0 1189 */ 1190 public MamPrefsResult updateArchivingPreferences(MamPrefs mamPrefs) throws NoResponseException, XMPPErrorException, 1191 NotConnectedException, InterruptedException, NotLoggedInException { 1192 MamPrefsIQ mamPrefIQ = mamPrefs.constructMamPrefsIq(); 1193 return queryMamPrefs(mamPrefIQ); 1194 } 1195 1196 public MamPrefsResult enableMamForAllMessages() throws NoResponseException, XMPPErrorException, 1197 NotConnectedException, NotLoggedInException, InterruptedException { 1198 return setDefaultBehavior(DefaultBehavior.always); 1199 } 1200 1201 public MamPrefsResult enableMamForRosterMessages() throws NoResponseException, XMPPErrorException, 1202 NotConnectedException, NotLoggedInException, InterruptedException { 1203 return setDefaultBehavior(DefaultBehavior.roster); 1204 } 1205 1206 public MamPrefsResult setDefaultBehavior(DefaultBehavior desiredDefaultBehavior) throws NoResponseException, 1207 XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { 1208 MamPrefsResult mamPrefsResult = retrieveArchivingPreferences(); 1209 if (mamPrefsResult.mamPrefs.getDefault() == desiredDefaultBehavior) { 1210 return mamPrefsResult; 1211 } 1212 1213 MamPrefs mamPrefs = mamPrefsResult.asMamPrefs(); 1214 mamPrefs.setDefaultBehavior(desiredDefaultBehavior); 1215 return updateArchivingPreferences(mamPrefs); 1216 } 1217 1218 /** 1219 * MAM preferences result class. 1220 * 1221 */ 1222 public static final class MamPrefsResult { 1223 public final MamPrefsIQ mamPrefs; 1224 public final DataForm form; 1225 1226 private MamPrefsResult(MamPrefsIQ mamPrefs, DataForm form) { 1227 this.mamPrefs = mamPrefs; 1228 this.form = form; 1229 } 1230 1231 public MamPrefs asMamPrefs() { 1232 return new MamPrefs(this); 1233 } 1234 } 1235 1236 public static final class MamPrefs { 1237 private final List<Jid> alwaysJids; 1238 private final List<Jid> neverJids; 1239 private DefaultBehavior defaultBehavior; 1240 1241 private MamPrefs(MamPrefsResult mamPrefsResult) { 1242 MamPrefsIQ mamPrefsIq = mamPrefsResult.mamPrefs; 1243 this.alwaysJids = new ArrayList<>(mamPrefsIq.getAlwaysJids()); 1244 this.neverJids = new ArrayList<>(mamPrefsIq.getNeverJids()); 1245 this.defaultBehavior = mamPrefsIq.getDefault(); 1246 } 1247 1248 public void setDefaultBehavior(DefaultBehavior defaultBehavior) { 1249 this.defaultBehavior = Objects.requireNonNull(defaultBehavior, "defaultBehavior must not be null"); 1250 } 1251 1252 public DefaultBehavior getDefaultBehavior() { 1253 return defaultBehavior; 1254 } 1255 1256 public List<Jid> getAlwaysJids() { 1257 return alwaysJids; 1258 } 1259 1260 public List<Jid> getNeverJids() { 1261 return neverJids; 1262 } 1263 1264 private MamPrefsIQ constructMamPrefsIq() { 1265 return new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior); 1266 } 1267 } 1268 1269 private MamPrefsResult queryMamPrefs(MamPrefsIQ mamPrefsIQ) throws NoResponseException, XMPPErrorException, 1270 NotConnectedException, InterruptedException, NotLoggedInException { 1271 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 1272 1273 MamPrefsIQ mamPrefsResultIQ = connection.createStanzaCollectorAndSend(mamPrefsIQ).nextResultOrThrow(); 1274 1275 return new MamPrefsResult(mamPrefsResultIQ, DataForm.from(mamPrefsIQ)); 1276 } 1277 1278}