From 58b1e263672dd393638e3e274c3e6ffec5aabc2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 6 Feb 2023 12:32:22 +0100 Subject: [PATCH] include publish-options. prepare code for reconfiguration --- .../eu/siacs/conversations/xml/Namespace.java | 2 +- .../android/xmpp/IqErrorException.java | 4 + .../android/xmpp/NodeConfiguration.java | 106 ++++++++++++++++++ .../xmpp/PreconditionNotMetException.java | 15 +++ .../android/xmpp/PubSubErrorException.java | 19 ++++ .../android/xmpp/manager/AxolotlManager.java | 10 +- .../android/xmpp/manager/PubSubManager.java | 97 ++++++++++++++-- .../android/xmpp/model/data/Data.java | 34 ++++++ .../android/xmpp/model/data/Field.java | 4 + .../android/xmpp/model/error/Condition.java | 2 +- .../xmpp/model/pubsub/PublishOptions.java | 21 ++++ .../xmpp/model/pubsub/error/PubSubError.java | 19 ++++ .../xmpp/model/pubsub/error/package-info.java | 5 + 13 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/NodeConfiguration.java create mode 100644 src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java create mode 100644 src/main/java/im/conversations/android/xmpp/PubSubErrorException.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index eeebc4ad5..b708c2757 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -78,7 +78,7 @@ public final class Namespace { public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUB_SUB = "http://jabber.org/protocol/pubsub"; - public static final String PUB_SUB_ERROR = PUB_SUB + "#errors"; + public static final String PUB_SUB_ERRORS = PUB_SUB + "#errors"; public static final String PUB_SUB_EVENT = PUB_SUB + "#event"; public static final String PUB_SUB_OWNER = PUB_SUB + "#owner"; public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items"; diff --git a/src/main/java/im/conversations/android/xmpp/IqErrorException.java b/src/main/java/im/conversations/android/xmpp/IqErrorException.java index 9b7ae887c..e064ee491 100644 --- a/src/main/java/im/conversations/android/xmpp/IqErrorException.java +++ b/src/main/java/im/conversations/android/xmpp/IqErrorException.java @@ -21,4 +21,8 @@ public class IqErrorException extends Exception { final var text = error == null ? null : error.getText(); return text == null ? null : text.getContent(); } + + public Iq getResponse() { + return this.response; + } } diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java new file mode 100644 index 000000000..56591efae --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java @@ -0,0 +1,106 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class NodeConfiguration implements Map { + + private static final String PERSIST_ITEMS = "pubsub#persist_items"; + private static final String ACCESS_MODEL = "pubsub#access_model"; + private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item"; + private static final String MAX_ITEMS = "pubsub#max_items"; + private static final String NOTIFY_DELETE = "pubsub#notify_delete"; + private static final String NOTIFY_RETRACT = "pubsub#notify_retract"; + + public static final NodeConfiguration OPEN = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "open") + .build()); + public static final NodeConfiguration WHITELIST_MAX_ITEMS = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "whitelist") + .put(SEND_LAST_PUBLISHED_ITEM, "never") + .put(MAX_ITEMS, "max") + .put(NOTIFY_DELETE, Boolean.TRUE) + .put(NOTIFY_RETRACT, Boolean.TRUE) + .build()); + private final Map delegate; + + private NodeConfiguration(Map map) { + this.delegate = map; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object o) { + return this.delegate.containsKey(o); + } + + @Override + public boolean containsValue(@Nullable Object o) { + return this.delegate.containsValue(o); + } + + @Nullable + @Override + public Object get(@Nullable Object o) { + return this.delegate.get(o); + } + + @Nullable + @Override + public Object put(String s, Object o) { + return this.delegate.put(s, o); + } + + @Nullable + @Override + public Object remove(@Nullable Object o) { + return this.delegate.remove(o); + } + + @Override + public void putAll(@NonNull Map map) { + this.delegate.putAll(map); + } + + @Override + public void clear() { + this.delegate.clear(); + } + + @NonNull + @Override + public Set keySet() { + return this.delegate.keySet(); + } + + @NonNull + @Override + public Collection values() { + return this.delegate.values(); + } + + @NonNull + @Override + public Set> entrySet() { + return this.delegate.entrySet(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java b/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java new file mode 100644 index 000000000..00ac5fe57 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java @@ -0,0 +1,15 @@ +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/im/conversations/android/xmpp/PubSubErrorException.java b/src/main/java/im/conversations/android/xmpp/PubSubErrorException.java new file mode 100644 index 000000000..3b6e07921 --- /dev/null +++ b/src/main/java/im/conversations/android/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/im/conversations/android/xmpp/manager/AxolotlManager.java b/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java index ff337833f..870756878 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java @@ -11,6 +11,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.database.AxolotlDatabaseStore; import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.NodeConfiguration; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.axolotl.AxolotlAddress; import im.conversations.android.xmpp.model.axolotl.Bundle; @@ -215,7 +216,11 @@ public class AxolotlManager extends AbstractManager { final var deviceList = new DeviceList(); deviceList.setDeviceIds(deviceIds); return getManager(PubSubManager.class) - .publishSingleton(getAccount().address, deviceList, Namespace.AXOLOTL_DEVICE_LIST); + .publishSingleton( + getAccount().address, + deviceList, + Namespace.AXOLOTL_DEVICE_LIST, + NodeConfiguration.OPEN); } private ListenableFuture publishBundle() { @@ -231,7 +236,8 @@ public class AxolotlManager extends AbstractManager { Namespace.AXOLOTL_BUNDLES, signalProtocolStore.getLocalRegistrationId()); return getManager(PubSubManager.class) - .publishSingleton(getAccount().address, bundle, node); + .publishSingleton( + getAccount().address, bundle, node, NodeConfiguration.OPEN); }, MoreExecutors.directExecutor()); } diff --git a/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java b/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java index 7ca455320..c50205e9c 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java @@ -1,16 +1,24 @@ package im.conversations.android.xmpp.manager; import android.content.Context; +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.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; 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.XmppConnection; import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.pubsub.Items; import im.conversations.android.xmpp.model.pubsub.PubSub; +import im.conversations.android.xmpp.model.pubsub.PublishOptions; +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; import im.conversations.android.xmpp.model.pubsub.event.Event; import im.conversations.android.xmpp.model.pubsub.event.Purge; import im.conversations.android.xmpp.model.stanza.Iq; @@ -164,34 +172,105 @@ public class PubSubManager extends AbstractManager { } } - public ListenableFuture publishSingleton(Jid address, Extension item) { + 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); + return publish(address, item, SINGLETON_ITEM_ID, id.namespace, nodeConfiguration); } - public ListenableFuture publishSingleton(Jid address, Extension item, final String node) { - return publish(address, item, SINGLETON_ITEM_ID, node); + 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) { + 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); + return publish(address, item, itemId, id.namespace, nodeConfiguration); } public ListenableFuture publish( final Jid address, final Extension itemPayload, final String itemId, - final String node) { + final String node, + final NodeConfiguration nodeConfiguration) { + final var future = publishNoRetry(address, itemPayload, itemId, node, nodeConfiguration); + return Futures.catchingAsync( + future, + PreconditionNotMetException.class, + ex -> { + LOGGER.info("Node {} on {} requires reconfiguration", node, address); + 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()); + pubSub.addExtension(PublishOptions.of(nodeConfiguration)); final var pubSubItemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); pubSubItemsWrapper.setNode(node); final var item = pubSubItemsWrapper.addExtension(new PubSub.Item()); item.setId(itemId); item.addExtension(itemPayload); - return Futures.transform( - connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); + 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) { + + return Futures.immediateVoidFuture(); + } + + 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/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index 582727718..1b281f3e3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -5,6 +5,7 @@ import com.google.common.collect.Iterables; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import java.util.Collection; +import java.util.Map; @XmlElement(name = "x") public class Data extends Extension { @@ -25,4 +26,37 @@ public class Data extends Extension { return Collections2.filter( this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); } + + private void addField(final String name, final Object value) { + if (value == null) { + throw new IllegalArgumentException("Null values are not supported on data fields"); + } + final var field = this.addExtension(new Field()); + field.setFieldName(name); + final var valueExtension = field.addExtension(new Value()); + if (value instanceof String) { + valueExtension.setContent((String) value); + } else if (value instanceof Integer) { + valueExtension.setContent(String.valueOf(value)); + } else if (value instanceof Boolean) { + valueExtension.setContent(Boolean.TRUE.equals(value) ? "true" : "false"); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not a supported field value", value.getClass().getSimpleName())); + } + } + + private void setFormType(final String formType) { + this.addField(FORM_TYPE, formType); + } + + public static Data of(final String formType, final Map values) { + final var data = new Data(); + data.setFormType(formType); + for (final Map.Entry entry : values.entrySet()) { + data.addField(entry.getKey(), entry.getValue()); + } + return data; + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index f59398254..a362bf9f5 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -19,4 +19,8 @@ public class Field extends Extension { public Collection getValues() { return Collections2.transform(getExtensions(Value.class), Element::getContent); } + + public void setFieldName(String name) { + this.setAttribute("var", name); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Condition.java b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java index 8b6c8b73d..bd68c2c43 100644 --- a/src/main/java/im/conversations/android/xmpp/model/error/Condition.java +++ b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java @@ -6,7 +6,7 @@ import im.conversations.android.xmpp.model.Extension; public abstract class Condition extends Extension { - private Condition(Class clazz) { + private Condition(Class clazz) { super(clazz); } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java new file mode 100644 index 000000000..4cc334f7f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.pubsub; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement +public class PublishOptions extends Extension { + + public PublishOptions() { + super(PublishOptions.class); + } + + public static PublishOptions of(NodeConfiguration nodeConfiguration) { + final var publishOptions = new PublishOptions(); + publishOptions.addExtension(Data.of(Namespace.PUB_SUB_PUBLISH_OPTIONS, nodeConfiguration)); + return publishOptions; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java new file mode 100644 index 000000000..e52dc5d08 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.pubsub.error; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public abstract class PubSubError extends Extension { + + private PubSubError(Class clazz) { + super(clazz); + } + + @XmlElement + public static class PreconditionNotMet extends PubSubError { + + private PreconditionNotMet() { + super(PreconditionNotMet.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java new file mode 100644 index 000000000..e5333e569 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUB_SUB_ERRORS) +package im.conversations.android.xmpp.model.pubsub.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage;