include publish-options. prepare code for reconfiguration

This commit is contained in:
Daniel Gultsch 2023-02-06 12:32:22 +01:00
parent c077e4e8da
commit 58b1e26367
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
13 changed files with 325 additions and 13 deletions

View file

@ -78,7 +78,7 @@ public final class Namespace {
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
public static final String PUBSUB_ERROR = PUBSUB + "#errors"; 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 = "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_EVENT = PUB_SUB + "#event";
public static final String PUB_SUB_OWNER = PUB_SUB + "#owner"; public static final String PUB_SUB_OWNER = PUB_SUB + "#owner";
public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items"; public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items";

View file

@ -21,4 +21,8 @@ public class IqErrorException extends Exception {
final var text = error == null ? null : error.getText(); final var text = error == null ? null : error.getText();
return text == null ? null : text.getContent(); return text == null ? null : text.getContent();
} }
public Iq getResponse() {
return this.response;
}
} }

View file

@ -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<String, Object> {
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<String, Object>()
.put(PERSIST_ITEMS, Boolean.TRUE)
.put(ACCESS_MODEL, "open")
.build());
public static final NodeConfiguration WHITELIST_MAX_ITEMS =
new NodeConfiguration(
new ImmutableMap.Builder<String, Object>()
.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<String, Object> delegate;
private NodeConfiguration(Map<String, Object> 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<? extends String, ?> map) {
this.delegate.putAll(map);
}
@Override
public void clear() {
this.delegate.clear();
}
@NonNull
@Override
public Set<String> keySet() {
return this.delegate.keySet();
}
@NonNull
@Override
public Collection<Object> values() {
return this.delegate.values();
}
@NonNull
@Override
public Set<Entry<String, Object>> entrySet() {
return this.delegate.entrySet();
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -11,6 +11,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.database.AxolotlDatabaseStore; import im.conversations.android.database.AxolotlDatabaseStore;
import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.IqErrorException;
import im.conversations.android.xmpp.NodeConfiguration;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.axolotl.AxolotlAddress; import im.conversations.android.xmpp.axolotl.AxolotlAddress;
import im.conversations.android.xmpp.model.axolotl.Bundle; import im.conversations.android.xmpp.model.axolotl.Bundle;
@ -215,7 +216,11 @@ public class AxolotlManager extends AbstractManager {
final var deviceList = new DeviceList(); final var deviceList = new DeviceList();
deviceList.setDeviceIds(deviceIds); deviceList.setDeviceIds(deviceIds);
return getManager(PubSubManager.class) return getManager(PubSubManager.class)
.publishSingleton(getAccount().address, deviceList, Namespace.AXOLOTL_DEVICE_LIST); .publishSingleton(
getAccount().address,
deviceList,
Namespace.AXOLOTL_DEVICE_LIST,
NodeConfiguration.OPEN);
} }
private ListenableFuture<Void> publishBundle() { private ListenableFuture<Void> publishBundle() {
@ -231,7 +236,8 @@ public class AxolotlManager extends AbstractManager {
Namespace.AXOLOTL_BUNDLES, Namespace.AXOLOTL_BUNDLES,
signalProtocolStore.getLocalRegistrationId()); signalProtocolStore.getLocalRegistrationId());
return getManager(PubSubManager.class) return getManager(PubSubManager.class)
.publishSingleton(getAccount().address, bundle, node); .publishSingleton(
getAccount().address, bundle, node, NodeConfiguration.OPEN);
}, },
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} }

View file

@ -1,16 +1,24 @@
package im.conversations.android.xmpp.manager; package im.conversations.android.xmpp.manager;
import android.content.Context; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.ExtensionFactory; 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.XmppConnection;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.pubsub.Items; import im.conversations.android.xmpp.model.pubsub.Items;
import im.conversations.android.xmpp.model.pubsub.PubSub; 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.Event;
import im.conversations.android.xmpp.model.pubsub.event.Purge; import im.conversations.android.xmpp.model.pubsub.event.Purge;
import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Iq;
@ -164,34 +172,105 @@ public class PubSubManager extends AbstractManager {
} }
} }
public ListenableFuture<Void> publishSingleton(Jid address, Extension item) { public ListenableFuture<Void> publishSingleton(
Jid address, Extension item, final NodeConfiguration nodeConfiguration) {
final var id = ExtensionFactory.id(item.getClass()); 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<Void> publishSingleton(Jid address, Extension item, final String node) { public ListenableFuture<Void> publishSingleton(
return publish(address, item, SINGLETON_ITEM_ID, node); Jid address,
Extension item,
final String node,
final NodeConfiguration nodeConfiguration) {
return publish(address, item, SINGLETON_ITEM_ID, node, nodeConfiguration);
} }
public ListenableFuture<Void> publish(Jid address, Extension item, final String itemId) { public ListenableFuture<Void> publish(
Jid address,
Extension item,
final String itemId,
final NodeConfiguration nodeConfiguration) {
final var id = ExtensionFactory.id(item.getClass()); final var id = ExtensionFactory.id(item.getClass());
return publish(address, item, itemId, id.namespace); return publish(address, item, itemId, id.namespace, nodeConfiguration);
} }
public ListenableFuture<Void> publish( public ListenableFuture<Void> publish(
final Jid address, final Jid address,
final Extension itemPayload, final Extension itemPayload,
final String itemId, 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<Void> publishNoRetry(
final Jid address,
final Extension itemPayload,
final String itemId,
final String node,
final NodeConfiguration nodeConfiguration) {
final var iq = new Iq(Iq.Type.SET); final var iq = new Iq(Iq.Type.SET);
iq.setTo(address); iq.setTo(address);
final var pubSub = iq.addExtension(new PubSub()); final var pubSub = iq.addExtension(new PubSub());
pubSub.addExtension(PublishOptions.of(nodeConfiguration));
final var pubSubItemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); final var pubSubItemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
pubSubItemsWrapper.setNode(node); pubSubItemsWrapper.setNode(node);
final var item = pubSubItemsWrapper.addExtension(new PubSub.Item()); final var item = pubSubItemsWrapper.addExtension(new PubSub.Item());
item.setId(itemId); item.setId(itemId);
item.addExtension(itemPayload); item.addExtension(itemPayload);
return Futures.transform( final ListenableFuture<Void> iqFuture =
connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); Futures.transform(
connection.sendIqPacket(iq),
result -> null,
MoreExecutors.directExecutor());
return Futures.catchingAsync(
iqFuture,
IqErrorException.class,
new PubSubExceptionTransformer<>(),
MoreExecutors.directExecutor());
}
private ListenableFuture<Void> reconfigureNode(
final Jid address, final String node, final NodeConfiguration nodeConfiguration) {
return Futures.immediateVoidFuture();
}
private static class PubSubExceptionTransformer<V>
implements AsyncFunction<IqErrorException, V> {
@Override
@NonNull
public ListenableFuture<V> 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);
}
}
} }
} }

View file

@ -5,6 +5,7 @@ import com.google.common.collect.Iterables;
import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
@XmlElement(name = "x") @XmlElement(name = "x")
public class Data extends Extension { public class Data extends Extension {
@ -25,4 +26,37 @@ public class Data extends Extension {
return Collections2.filter( return Collections2.filter(
this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); 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<String, Object> values) {
final var data = new Data();
data.setFormType(formType);
for (final Map.Entry<String, Object> entry : values.entrySet()) {
data.addField(entry.getKey(), entry.getValue());
}
return data;
}
} }

View file

@ -19,4 +19,8 @@ public class Field extends Extension {
public Collection<String> getValues() { public Collection<String> getValues() {
return Collections2.transform(getExtensions(Value.class), Element::getContent); return Collections2.transform(getExtensions(Value.class), Element::getContent);
} }
public void setFieldName(String name) {
this.setAttribute("var", name);
}
} }

View file

@ -6,7 +6,7 @@ import im.conversations.android.xmpp.model.Extension;
public abstract class Condition extends Extension { public abstract class Condition extends Extension {
private Condition(Class<? extends Extension> clazz) { private Condition(Class<? extends Condition> clazz) {
super(clazz); super(clazz);
} }

View file

@ -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;
}
}

View file

@ -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<? extends PubSubError> clazz) {
super(clazz);
}
@XmlElement
public static class PreconditionNotMet extends PubSubError {
private PreconditionNotMet() {
super(PreconditionNotMet.class);
}
}
}

View file

@ -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;