diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index b1cd4e395137fb0a34bfc6cf612f5dd452a09b84..043b88d2005a3624c21b26dc5c475934389295b0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -20,7 +20,6 @@ import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.http.ServiceOutageStatus; import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; @@ -96,8 +95,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable protected boolean online = false; private String rosterVersion; private String displayName = null; - private AxolotlService axolotlService = null; - private PgpDecryptionService pgpDecryptionService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private final Map bookmarks = new HashMap<>(); @@ -233,11 +230,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public boolean hasPendingPgpIntent(Conversation conversation) { - return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation); + return getPgpDecryptionService().hasPendingIntent(conversation); } public boolean isPgpDecryptionServiceConnected() { - return pgpDecryptionService != null && pgpDecryptionService.isConnected(); + return getPgpDecryptionService().isConnected(); } public boolean setShowErrorNotification(boolean newValue) { @@ -285,11 +282,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable final Jid prev = this.jid != null ? this.jid.asBareJid() : null; final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid())); if (changed) { - final AxolotlService oldAxolotlService = this.axolotlService; + final AxolotlService oldAxolotlService = xmppConnection.getAxolotlService(); + // TODO check that changing JID and recreating the AxolotlService still works if (oldAxolotlService != null) { oldAxolotlService.destroy(); this.jid = next; - this.axolotlService = oldAxolotlService.makeNew(); + xmppConnection.setAxolotlService(oldAxolotlService.makeNew()); } } this.jid = next; @@ -545,18 +543,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public AxolotlService getAxolotlService() { - return axolotlService; - } - - public void initAccountServices(final XmppConnectionService context) { - this.xmppConnection = new XmppConnection(this, context); - this.axolotlService = new AxolotlService(this, context); - this.pgpDecryptionService = new PgpDecryptionService(context); - this.xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + return this.xmppConnection.getAxolotlService(); } public PgpDecryptionService getPgpDecryptionService() { - return this.pgpDecryptionService; + return this.xmppConnection.getPgpDecryptionService(); } public XmppConnection getXmppConnection() { @@ -739,9 +730,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private List getFingerprints() { ArrayList fingerprints = new ArrayList<>(); - if (axolotlService == null) { - return fingerprints; - } + final var axolotlService = getAxolotlService(); fingerprints.add( new XmppUri.Fingerprint( XmppUri.FingerprintType.OMEMO, @@ -811,6 +800,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return false; } + public void setXmppConnection(final XmppConnection connection) { + this.xmppConnection = connection; + } + public enum State { DISABLED(false, false), LOGGED_OUT(false, false), diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 41bd8c978a85bd32a32546f55f88a67d26310ae7..3245da705a2beb0b8f3e1b5a52f86d963ddef8e2 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -12,7 +12,6 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.model.bookmark.Storage; import im.conversations.android.xmpp.model.bookmark2.Conference; -import im.conversations.android.xmpp.model.pubsub.PubSub; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; @@ -41,6 +40,7 @@ public class Bookmark extends Element implements ListItem { public static Map parseFromStorage( final Storage storage, final Account account) { + // TODO refactor to use extensions. get rid of the 'old' handling if (storage == null) { return Collections.emptyMap(); } @@ -61,26 +61,6 @@ public class Bookmark extends Element implements ListItem { return bookmarks; } - public static Map parseFromPubSub(final PubSub pubSub, final Account account) { - if (pubSub == null) { - return Collections.emptyMap(); - } - final var items = pubSub.getItems(); - if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) { - return Collections.emptyMap(); - } - final Map bookmarks = new HashMap<>(); - for (final var item : items.getItemMap(Conference.class).entrySet()) { - final Bookmark bookmark = - Bookmark.parseFromItem(item.getKey(), item.getValue(), account); - if (bookmark == null) { - continue; - } - bookmarks.put(bookmark.jid, bookmark); - } - return bookmarks; - } - public static Bookmark parse(Element element, Account account) { Bookmark bookmark = new Bookmark(account); bookmark.setAttributes(element.getAttributes()); @@ -103,6 +83,9 @@ public class Bookmark extends Element implements ListItem { if (bookmark.jid == null) { return null; } + + // TODO use proper API + bookmark.setBookmarkName(conference.getAttribute("name")); bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); bookmark.setNick(conference.findChildContent("nick")); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 35725518c712f3f97ef0ed4f206e65f9cba67f74..77da8fd1eac11d6ba3d4aaf44e061dcf4f9475e9 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -37,6 +37,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; @@ -321,7 +322,7 @@ public class MessageParser extends AbstractParser } final var storage = items.getFirstItem(Storage.class); final Map bookmarks = Bookmark.parseFromStorage(storage, account); - mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); + // mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event"); @@ -351,7 +352,7 @@ public class MessageParser extends AbstractParser Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); - mXmppConnectionService.processDeletedBookmark(account, id); + // mXmppConnectionService.processDeletedBookmark(account, id); mXmppConnectionService.updateConversationUi(); } } @@ -398,7 +399,7 @@ public class MessageParser extends AbstractParser private void deleteAllBookmarks(final Account account) { final var previous = account.getBookmarkedJids(); account.setBookmarks(Collections.emptyMap()); - mXmppConnectionService.processDeletedBookmarks(account, previous); + // mXmppConnectionService.processDeletedBookmarks(account, previous); } private void setNick(final Account account, final Jid user, final String nick) { @@ -1410,6 +1411,9 @@ public class MessageParser extends AbstractParser // end no body } + if (original.hasExtension(Event.class)) { + getManager(PubSubManager.class).handleEvent(original); + } final var event = original.getExtension(Event.class); if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { final var action = event.getAction(); diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index eb58377ff8e650605eb7587f48be5a4018e46b1d..95f232d2f244ef0aa1305665de484fc58b210543 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -50,6 +50,7 @@ public class PresenceParser extends AbstractParser ? null : mXmppConnectionService.find(account, packet.getFrom().asBareJid()); if (conversation == null) { + Log.d(Config.LOGTAG, "conversation not found for parsing conference presence"); return; } final MucOptions mucOptions = conversation.getMucOptions(); @@ -490,6 +491,7 @@ public class PresenceParser extends AbstractParser @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { + // Log.d(Config.LOGTAG,"<--"+packet); if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 34323ed4c61b7575f45aaf97b39668820c5f8aa8..0a37de478760c9524c0985e914cc18bb6c50a9e6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -121,7 +121,6 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.IqErrorResponseException; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -144,13 +143,12 @@ import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.avatar.Metadata; -import im.conversations.android.xmpp.model.bookmark.Storage; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; -import im.conversations.android.xmpp.model.storage.PrivateStorage; import im.conversations.android.xmpp.model.up.Push; import java.io.File; import java.security.Security; @@ -366,6 +364,7 @@ public class XmppConnectionService extends Service { @Override public void onStatusChanged(final Account account) { + Log.d(Config.LOGTAG, "begin onStatusChanged()"); final var status = account.getStatus(); if (ServiceOutageStatus.isPossibleOutage(status)) { fetchServiceOutageStatus(account); @@ -499,6 +498,7 @@ public class XmppConnectionService extends Service { } } getNotificationService().updateErrorNotification(); + Log.d(Config.LOGTAG, "end onStatusChanged()"); } }; @@ -1843,15 +1843,13 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); + // TODO move status listener into final variable in XmppConnection connection.setOnStatusChangedListener(this.statusListener); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); + // TODO move MessageAck into final Processor into XmppConnection connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } return connection; } @@ -2137,44 +2135,6 @@ public class XmppConnectionService extends Service { }); } - public void fetchBookmarks(final Account account) { - final Iq iqPacket = new Iq(Iq.Type.GET); - iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage()); - final Consumer callback = - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final var privateStorage = response.getExtension(PrivateStorage.class); - if (privateStorage == null) { - return; - } - final var bookmarkStorage = privateStorage.getExtension(Storage.class); - Map bookmarks = - Bookmark.parseFromStorage(bookmarkStorage, account); - processBookmarksInitial(account, bookmarks, false); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not fetch bookmarks"); - } - }; - sendIqPacket(account, iqPacket, callback); - } - - public void fetchBookmarks2(final Account account) { - final Iq retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket( - account, - retrieve, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final var pubsub = response.getExtension(PubSub.class); - final Map bookmarks = - Bookmark.parseFromPubSub(pubsub, account); - processBookmarksInitial(account, bookmarks, true); - } - }); - } - public void fetchMessageDisplayedSynchronization(final Account account) { Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); final var retrieve = mIqGenerator.retrieveMds(); @@ -2252,43 +2212,7 @@ public class XmppConnectionService extends Service { return true; } - public void processBookmarksInitial( - final Account account, final Map bookmarks, final boolean pep) { - final Set previousBookmarks = account.getBookmarkedJids(); - for (final Bookmark bookmark : bookmarks.values()) { - previousBookmarks.remove(bookmark.getJid().asBareJid()); - processModifiedBookmark(bookmark, pep); - } - if (pep) { - processDeletedBookmarks(account, previousBookmarks); - } - account.setBookmarks(bookmarks); - } - - public void processDeletedBookmarks(final Account account, final Collection bookmarks) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": " - + bookmarks.size() - + " bookmarks have been removed"); - for (final Jid bookmark : bookmarks) { - processDeletedBookmark(account, bookmark); - } - } - - public void processDeletedBookmark(final Account account, final Jid jid) { - final Conversation conversation = find(account, jid); - if (conversation == null) { - return; - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); - archiveConversation(conversation, false); - } - - private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { + public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { final Account account = bookmark.getAccount(); Conversation conversation = find(bookmark); if (conversation != null) { @@ -2407,6 +2331,7 @@ public class XmppConnectionService extends Service { private void pushBookmarksPrivateXml(Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); final Iq iqPacket = new Iq(Iq.Type.SET); + // TODO we have extensions for that Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); for (final Bookmark bookmark : account.getBookmarks()) { @@ -2531,8 +2456,7 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "restoring roster..."); for (final Account account : accounts) { - account.initAccountServices( - this); // roster needs to be loaded at this stage + account.setXmppConnection(createConnection(account)); account.getXmppConnection().getManager(RosterManager.class).restore(); } getBitmapCache().evictAll(); @@ -2989,7 +2913,7 @@ public class XmppConnectionService extends Service { archiveConversation(conversation, true); } - private void archiveConversation( + public void archiveConversation( Conversation conversation, final boolean maySynchronizeWithBookmarks) { getNotificationService().clear(conversation); conversation.setStatus(Conversation.STATUS_ARCHIVED); @@ -3034,7 +2958,7 @@ public class XmppConnectionService extends Service { } public void createAccount(final Account account) { - account.initAccountServices(this); + account.setXmppConnection(createConnection(account)); databaseBackend.createAccount(account); if (CallIntegration.hasSystemFeature(this)) { CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); @@ -4450,8 +4374,7 @@ public class XmppConnectionService extends Service { account.getJid().asBareJid() + ": received timeout waiting for conference" + " configuration fetch"); - } else if (throwable - instanceof IqErrorResponseException errorResponseException) { + } else if (throwable instanceof IqErrorException errorResponseException) { if (callback != null) { callback.onFetchFailed( conversation, diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..7959c8ec4b985bf73cbaaa168143f4fed4c0c99d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Strings; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class IqErrorException extends Exception { + + private final Iq response; + + public IqErrorException(Iq response) { + super(getErrorText(response)); + this.response = response; + } + + public Error getError() { + return this.response.getError(); + } + + private static String getErrorText(final Iq response) { + final var error = response.getError(); + final var text = error == null ? null : error.getText(); + final var textContent = text == null ? null : text.getContent(); + if (Strings.isNullOrEmpty(textContent)) { + final var condition = error == null ? null : error.getExtension(Condition.class); + return condition == null ? null : condition.getName(); + } + return textContent; + } + + public Iq getResponse() { + return this.response; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java deleted file mode 100644 index fb8d8e730cd9d4e4b37d3321a084e9d4abc25f93..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import im.conversations.android.xmpp.model.stanza.Iq; - -public class IqErrorResponseException extends Exception { - - private final Iq response; - - public IqErrorResponseException(final Iq response) { - super(message(response)); - this.response = response; - } - - public Iq getResponse() { - return this.response; - } - - public static String message(final Iq iq) { - final var error = iq.getError(); - if (error == null) { - return "missing error element in response"; - } - final var text = error.getTextAsString(); - if (text != null) { - return text; - } - final var condition = error.getCondition(); - if (condition != null) { - return condition.getName(); - } - return "no condition attached to error"; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 767234a04b54b7dc078191462ddd16e68faddc03..1c709aa2d2914b5b4dd92be2df706757334c5326 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -4,12 +4,20 @@ import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ImmutableClassToInstanceMap; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.manager.AbstractManager; +import eu.siacs.conversations.xmpp.manager.AvatarManager; +import eu.siacs.conversations.xmpp.manager.AxolotlManager; import eu.siacs.conversations.xmpp.manager.BlockingManager; +import eu.siacs.conversations.xmpp.manager.BookmarkManager; import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.EntityTimeManager; +import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; +import eu.siacs.conversations.xmpp.manager.NickManager; +import eu.siacs.conversations.xmpp.manager.PepManager; import eu.siacs.conversations.xmpp.manager.PingManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; +import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; @@ -22,12 +30,20 @@ public class Managers { public static ClassToInstanceMap get( final XmppConnectionService context, final XmppConnection connection) { return new ImmutableClassToInstanceMap.Builder() + .put(AvatarManager.class, new AvatarManager(context, connection)) + .put(AxolotlManager.class, new AxolotlManager(context, connection)) .put(BlockingManager.class, new BlockingManager(context, connection)) + .put(BookmarkManager.class, new BookmarkManager(context, connection)) .put(CarbonsManager.class, new CarbonsManager(context, connection)) .put(DiscoManager.class, new DiscoManager(context, connection)) .put(EntityTimeManager.class, new EntityTimeManager(context, connection)) + .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection)) + .put(NickManager.class, new NickManager(context, connection)) + .put(PepManager.class, new PepManager(context, connection)) .put(PingManager.class, new PingManager(context, connection)) .put(PresenceManager.class, new PresenceManager(context, connection)) + .put(PrivateStorageManager.class, new PrivateStorageManager(context, connection)) + .put(PubSubManager.class, new PubSubManager(context, connection)) .put(RosterManager.class, new RosterManager(context, connection)) .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection)) .build(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java new file mode 100644 index 0000000000000000000000000000000000000000..c2777667734c76e46af46c9eccedc334957a7c9f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp; + +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class PreconditionNotMetException extends PubSubErrorException { + + public PreconditionNotMetException(final Iq response) { + super(response); + if (this.pubSubError instanceof PubSubError.PreconditionNotMet) { + return; + } + throw new AssertionError( + "This exception should only be constructed for PreconditionNotMet errors"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..3b6e079214c672913a1aa0c47d7d5e7102159aad --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp; + +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class PubSubErrorException extends IqErrorException { + + protected final PubSubError pubSubError; + + public PubSubErrorException(Iq response) { + super(response); + final var error = response.getError(); + final var pubSubError = error == null ? null : error.getExtension(PubSubError.class); + if (pubSubError == null) { + throw new AssertionError("This exception should only be constructed for PubSubErrors"); + } + this.pubSubError = pubSubError; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index dec02a32a3614c0ed97adfee4e51b5d271a90296..988dc6f890256f2824f25ade832e085647feceb0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -33,6 +33,7 @@ import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; @@ -77,6 +78,7 @@ import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.PingManager; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; import im.conversations.android.xmpp.model.AuthenticationStreamFeature; @@ -200,6 +202,8 @@ public class XmppConnection implements Runnable { private final Consumer presenceListener; private final Consumer unregisteredIqListener; private final Consumer messageListener; + private AxolotlService axolotlService; + private final PgpDecryptionService pgpDecryptionService; private OnStatusChanged statusListener = null; private final Runnable bindListener; private OnMessageAcknowledged acknowledgedListener = null; @@ -226,6 +230,8 @@ public class XmppConnection implements Runnable { this.messageListener = new MessageParser(service, this); this.bindListener = new BindProcessor(service, this); this.managers = Managers.get(service, this); + this.setAxolotlService(new AxolotlService(account, service)); + this.pgpDecryptionService = new PgpDecryptionService(service); } private static void fixResource(final Context context, final Account account) { @@ -277,7 +283,13 @@ public class XmppConnection implements Runnable { } } if (statusListener != null) { - statusListener.onStatusChanged(account); + try { + statusListener.onStatusChanged(account); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "error executing shit", e); + } + } else { + Log.d(Config.LOGTAG, "status changed listener was null"); } } @@ -2520,7 +2532,7 @@ public class XmppConnection implements Runnable { switch (type) { case RESULT -> settable.set(response); case TIMEOUT -> settable.setException(new TimeoutException()); - default -> settable.setException(new IqErrorResponseException(response)); + default -> settable.setException(new IqErrorException(response)); } }); return settable; @@ -2893,6 +2905,29 @@ public class XmppConnection implements Runnable { || from.equals(account); } + public boolean fromAccount(final Stanza stanza) { + final var account = getAccount().getJid(); + final Jid from = stanza.getFrom(); + return from == null || from.asBareJid().equals(account.asBareJid()); + } + + public AxolotlService getAxolotlService() { + return this.axolotlService; + } + + public PgpDecryptionService getPgpDecryptionService() { + return this.pgpDecryptionService; + } + + public void setAxolotlService(AxolotlService axolotlService) { + final var current = this.axolotlService; + if (current != null) { + this.advancedStreamFeaturesLoadedListeners.remove(current); + } + this.axolotlService = axolotlService; + this.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6ecf0edf874d0a07abdeff3feac4551b785e326a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java @@ -0,0 +1,62 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class AbstractBookmarkManager extends AbstractManager { + + private final XmppConnectionService service; + + protected AbstractBookmarkManager( + final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + this.service = service; + } + + // TODO rename to setBookmarks? + public void processBookmarksInitial(final Map bookmarks, final boolean pep) { + final var account = getAccount(); + // TODO we can internalize this getBookmarkedJid + final Set previousBookmarks = account.getBookmarkedJids(); + for (final Bookmark bookmark : bookmarks.values()) { + previousBookmarks.remove(bookmark.getJid().asBareJid()); + service.processModifiedBookmark(bookmark, pep); + } + if (pep) { + this.processDeletedBookmarks(account, previousBookmarks); + } + account.setBookmarks(bookmarks); + } + + public void processDeletedBookmarks(final Account account, final Collection bookmarks) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + bookmarks.size() + + " bookmarks have been removed"); + for (final Jid bookmark : bookmarks) { + processDeletedBookmark(account, bookmark); + } + } + + public void processDeletedBookmark(final Account account, final Jid jid) { + final Conversation conversation = service.find(account, jid); + if (conversation == null) { + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); + this.service.archiveConversation(conversation, false); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java new file mode 100644 index 0000000000000000000000000000000000000000..977627bc6b7643c62afe0e57fc683dec3d85ccb4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class AvatarManager extends AbstractManager { + + public AvatarManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(Jid from, final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java new file mode 100644 index 0000000000000000000000000000000000000000..889054032f2c982861163a523a797982e089f4ea --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class AxolotlManager extends AbstractManager { + + public AxolotlManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(Jid from, final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4bfb5d080d9685ad2b95735e169c311aaa8a006f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.bookmark2.Conference; +import im.conversations.android.xmpp.model.bookmark2.Nick; +import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Map; + +public class BookmarkManager extends AbstractBookmarkManager { + + public BookmarkManager(final XmppConnectionService service, XmppConnection connection) { + super(service, connection); + } + + public void fetch() { + final var future = getManager(PepManager.class).fetchItems(Conference.class); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final Map bookmarks) { + final var builder = new ImmutableMap.Builder(); + for (final var entry : bookmarks.entrySet()) { + final Bookmark bookmark = + Bookmark.parseFromItem( + entry.getKey(), entry.getValue(), getAccount()); + if (bookmark == null) { + continue; + } + builder.put(bookmark.getJid(), bookmark); + } + processBookmarksInitial(builder.buildKeepingLast(), true); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable); + } + }, + MoreExecutors.directExecutor()); + } + + public void handleItems(final Items items) { + final var retractions = items.getRetractions(); + final var itemMap = items.getItemMap(Conference.class); + if (!retractions.isEmpty()) { + // deleteItems(retractions); + } + if (!itemMap.isEmpty()) { + // updateItems(itemMap); + } + } + + public ListenableFuture publishBookmark(final Jid address, final boolean autoJoin) { + return publishBookmark(address, autoJoin, null); + } + + public ListenableFuture publishBookmark( + final Jid address, final boolean autoJoin, final String nick) { + final var itemId = address.toString(); + final var conference = new Conference(); + conference.setAutoJoin(autoJoin); + if (nick != null) { + conference.addExtension(new Nick()).setContent(nick); + } + return Futures.transform( + getManager(PepManager.class) + .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS), + result -> null, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retractBookmark(final Jid address) { + final var itemId = address.toString(); + return Futures.transform( + getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2), + result -> null, + MoreExecutors.directExecutor()); + } + + public void deleteAllItems() {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..71817f42e829de5c5964ed99ba344081de1fa246 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class LegacyBookmarkManager extends AbstractBookmarkManager { + + public LegacyBookmarkManager( + final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + } + + public void handleItems(final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java new file mode 100644 index 0000000000000000000000000000000000000000..56f8766adbbe456b768e5148f57a135a6a1eb54b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java @@ -0,0 +1,31 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ListenableFuture; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.nick.Nick; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class NickManager extends AbstractManager { + + public NickManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(final Jid from, Items items) { + final var item = items.getFirstItem(Nick.class); + final var nick = item == null ? null : item.getContent(); + if (from == null || Strings.isNullOrEmpty(nick)) { + return; + } + } + + public ListenableFuture publishNick(final String name) { + final Nick nick = new Nick(); + nick.setContent(name); + return getManager(PepManager.class).publishSingleton(nick, NodeConfiguration.PRESENCE); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba8534588cd6fcff3c0dced7b5bde417a014ff4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java @@ -0,0 +1,53 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import com.google.common.util.concurrent.ListenableFuture; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.Map; + +public class PepManager extends AbstractManager { + + public PepManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public ListenableFuture> fetchItems(final Class clazz) { + return pubSubManager().fetchItems(pepService(), clazz); + } + + public ListenableFuture fetchMostRecentItem( + final String node, final Class clazz) { + return pubSubManager().fetchMostRecentItem(pepService(), node, clazz); + } + + public ListenableFuture publish( + Extension item, final String itemId, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publish(pepService(), item, itemId, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + Extension item, final String node, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publishSingleton(pepService(), item, node, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + final Extension item, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publishSingleton(pepService(), item, nodeConfiguration); + } + + public ListenableFuture retract(final String itemId, final String node) { + return pubSubManager().retract(pepService(), itemId, node); + } + + private PubSubManager pubSubManager() { + return getManager(PubSubManager.class); + } + + private Jid pepService() { + return connection.getAccount().getJid().asBareJid(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java new file mode 100644 index 0000000000000000000000000000000000000000..1a5bd56c62939c39bedfe18871b16ae02f85edbf --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java @@ -0,0 +1,55 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.storage.PrivateStorage; +import java.util.Map; + +public class PrivateStorageManager extends AbstractBookmarkManager { + + public PrivateStorageManager(final XmppConnectionService service, XmppConnection connection) { + super(service, connection); + } + + public void fetchBookmarks() { + final var iq = new Iq(Iq.Type.GET); + final var privateStorage = iq.addExtension(new PrivateStorage()); + privateStorage.addExtension(new Storage()); + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Iq result) { + final var privateStorage = result.getExtension(PrivateStorage.class); + if (privateStorage == null) { + return; + } + final var bookmarkStorage = privateStorage.getExtension(Storage.class); + final Map bookmarks = + Bookmark.parseFromStorage(bookmarkStorage, getAccount()); + processBookmarksInitial(bookmarks, false); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not fetch bookmark from private storage", + t); + } + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java new file mode 100644 index 0000000000000000000000000000000000000000..47856e26729ce138814061b80f87f659fbe1329d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -0,0 +1,349 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.ExtensionFactory; +import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.PreconditionNotMetException; +import im.conversations.android.xmpp.PubSubErrorException; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.pubsub.PubSub; +import im.conversations.android.xmpp.model.pubsub.Publish; +import im.conversations.android.xmpp.model.pubsub.PublishOptions; +import im.conversations.android.xmpp.model.pubsub.Retract; +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.pubsub.event.Delete; +import im.conversations.android.xmpp.model.pubsub.event.Event; +import im.conversations.android.xmpp.model.pubsub.event.Purge; +import im.conversations.android.xmpp.model.pubsub.owner.Configure; +import im.conversations.android.xmpp.model.pubsub.owner.PubSubOwner; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Message; +import java.util.Map; + +public class PubSubManager extends AbstractManager { + + private static final String SINGLETON_ITEM_ID = "current"; + + public PubSubManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleEvent(final Message message) { + final var event = message.getExtension(Event.class); + final var action = event.getAction(); + final var from = message.getFrom(); + + if (from instanceof Jid.Invalid) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": ignoring event from invalid jid"); + return; + } + + if (action instanceof Purge purge) { + // purge is a deletion of all items in a node + handlePurge(message, purge); + } else if (action instanceof Items items) { + // the items wrapper contains, new and updated items as well as retractions which are + // deletions of individual items in a node + handleItems(message, items); + } else if (action instanceof Delete delete) { + // delete is the deletion of the node itself + handleDelete(message, delete); + } + } + + public ListenableFuture> fetchItems( + final Jid address, final Class clazz) { + final var id = ExtensionFactory.id(clazz); + if (id == null) { + return Futures.immediateFailedFuture( + new IllegalArgumentException( + String.format("%s is not a registered extension", clazz.getName()))); + } + return fetchItems(address, id.namespace, clazz); + } + + public ListenableFuture> fetchItems( + final Jid address, final String node, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getItemMap(clazz); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchItem( + final Jid address, final String itemId, final Class clazz) { + final var id = ExtensionFactory.id(clazz); + if (id == null) { + return Futures.immediateFailedFuture( + new IllegalArgumentException( + String.format("%s is not a registered extension", clazz.getName()))); + } + return fetchItem(address, id.namespace, itemId, clazz); + } + + public ListenableFuture fetchItem( + final Jid address, final String node, final String itemId, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + final var item = itemsWrapper.addExtension(new PubSub.Item()); + item.setId(itemId); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getItemOrThrow(itemId, clazz); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchMostRecentItem( + final Jid address, final String node, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + itemsWrapper.setMaxItems(1); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getOnlyItem(clazz); + }, + MoreExecutors.directExecutor()); + } + + private void handleItems(final Message message, final Items items) { + final var from = message.getFrom(); + final var isFromBare = from == null || from.isBareJid(); + final var node = items.getNode(); + if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { + getManager(BookmarkManager.class).handleItems(items); + return; + } + if (connection.fromAccount(message) && Namespace.BOOKMARKS.equals(node)) { + getManager(LegacyBookmarkManager.class).handleItems(items); + return; + } + if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { + getManager(AvatarManager.class).handleItems(from, items); + return; + } + if (isFromBare && Namespace.NICK.equals(node)) { + getManager(NickManager.class).handleItems(from, items); + return; + } + if (isFromBare && Namespace.AXOLOTL_DEVICE_LIST.equals(node)) { + getManager(AxolotlManager.class).handleItems(from, items); + } + } + + private void handlePurge(final Message message, final Purge purge) { + final var from = message.getFrom(); + final var node = purge.getNode(); + if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { + getManager(BookmarkManager.class).deleteAllItems(); + } + } + + private void handleDelete(final Message message, final Delete delete) {} + + public ListenableFuture publishSingleton( + Jid address, Extension item, final NodeConfiguration nodeConfiguration) { + final var id = ExtensionFactory.id(item.getClass()); + return publish(address, item, SINGLETON_ITEM_ID, id.namespace, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + Jid address, + Extension item, + final String node, + final NodeConfiguration nodeConfiguration) { + return publish(address, item, SINGLETON_ITEM_ID, node, nodeConfiguration); + } + + public ListenableFuture publish( + Jid address, + Extension item, + final String itemId, + final NodeConfiguration nodeConfiguration) { + final var id = ExtensionFactory.id(item.getClass()); + return publish(address, item, itemId, id.namespace, nodeConfiguration); + } + + public ListenableFuture publish( + final Jid address, + final Extension itemPayload, + final String itemId, + final String node, + final NodeConfiguration nodeConfiguration) { + final var future = publishNoRetry(address, itemPayload, itemId, node, nodeConfiguration); + return Futures.catchingAsync( + future, + PreconditionNotMetException.class, + ex -> { + Log.d( + Config.LOGTAG, + "Node " + node + " on " + address + " requires reconfiguration"); + final var reconfigurationFuture = + reconfigureNode(address, node, nodeConfiguration); + return Futures.transformAsync( + reconfigurationFuture, + ignored -> + publishNoRetry( + address, itemPayload, itemId, node, nodeConfiguration), + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture publishNoRetry( + final Jid address, + final Extension itemPayload, + final String itemId, + final String node, + final NodeConfiguration nodeConfiguration) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSub()); + final var publish = pubSub.addExtension(new Publish()); + publish.setNode(node); + final var item = publish.addExtension(new PubSub.Item()); + item.setId(itemId); + item.addExtension(itemPayload); + pubSub.addExtension(PublishOptions.of(nodeConfiguration)); + final ListenableFuture iqFuture = + Futures.transform( + connection.sendIqPacket(iq), + result -> null, + MoreExecutors.directExecutor()); + return Futures.catchingAsync( + iqFuture, + IqErrorException.class, + new PubSubExceptionTransformer<>(), + MoreExecutors.directExecutor()); + } + + private ListenableFuture reconfigureNode( + final Jid address, final String node, final NodeConfiguration nodeConfiguration) { + final Iq iq = new Iq(Iq.Type.GET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSubOwner()); + final var configure = pubSub.addExtension(new Configure()); + configure.setNode(node); + return Futures.transformAsync( + connection.sendIqPacket(iq), + result -> { + final var pubSubOwnerResult = result.getExtension(PubSubOwner.class); + final Configure configureResult = + pubSubOwnerResult == null + ? null + : pubSubOwnerResult.getExtension(Configure.class); + if (configureResult == null) { + throw new IllegalStateException( + "No configuration found in configuration request result"); + } + final var data = configureResult.getData(); + return setNodeConfiguration(address, node, data.submit(nodeConfiguration)); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture setNodeConfiguration( + final Jid address, final String node, final Data data) { + final Iq iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSubOwner()); + final var configure = pubSub.addExtension(new Configure()); + configure.setNode(node); + configure.addExtension(data); + return Futures.transform( + connection.sendIqPacket(iq), + result -> { + Log.d(Config.LOGTAG, "Modified node configuration " + node + " on " + address); + return null; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retract(final Jid address, final String itemId, final String node) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSub()); + final var retract = pubSub.addExtension(new Retract()); + retract.setNode(node); + retract.setNotify(true); + final var item = retract.addExtension(new PubSub.Item()); + item.setId(itemId); + return connection.sendIqPacket(iq); + } + + private static class PubSubExceptionTransformer + implements AsyncFunction { + + @Override + @NonNull + public ListenableFuture apply(@NonNull IqErrorException ex) { + final var error = ex.getError(); + if (error == null) { + return Futures.immediateFailedFuture(ex); + } + final PubSubError pubSubError = error.getExtension(PubSubError.class); + if (pubSubError instanceof PubSubError.PreconditionNotMet) { + return Futures.immediateFailedFuture( + new PreconditionNotMetException(ex.getResponse())); + } else if (pubSubError != null) { + return Futures.immediateFailedFuture(new PubSubErrorException(ex.getResponse())); + } else { + return Futures.immediateFailedFuture(ex); + } + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index e42e400dc9be30a3dfe29d1602252dca19d0a2aa..dc8e1dd5ff95dd545011292b53c232c84744bb55 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -7,6 +7,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.BookmarkManager; +import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; @@ -21,6 +23,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { @Override public void run() { + Log.d(Config.LOGTAG, "begin onBind()"); final var account = connection.getAccount(); final var features = connection.getFeatures(); service.cancelAvatarFetches(account); @@ -63,9 +66,10 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { getManager(RosterManager.class).request(); if (features.bookmarks2()) { - service.fetchBookmarks2(account); + connection.getManager(BookmarkManager.class).fetch(); + // log that we use bookmarks 1 and wait for +notify } else if (!features.bookmarksConversion()) { - service.fetchBookmarks(account); + connection.getManager(PrivateStorageManager.class).fetchBookmarks(); } if (features.mds()) { @@ -101,5 +105,6 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { connection.getManager(RosterManager.class).syncDirtyContacts(); service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account); + Log.d(Config.LOGTAG, "end onBind()"); } }