include publish-options. prepare code for reconfiguration
This commit is contained in:
parent
c077e4e8da
commit
58b1e26367
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Void> 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());
|
||||
}
|
||||
|
|
|
@ -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<Void> publishSingleton(Jid address, Extension item) {
|
||||
public ListenableFuture<Void> 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<Void> publishSingleton(Jid address, Extension item, final String node) {
|
||||
return publish(address, item, SINGLETON_ITEM_ID, node);
|
||||
public ListenableFuture<Void> publishSingleton(
|
||||
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());
|
||||
return publish(address, item, itemId, id.namespace);
|
||||
return publish(address, item, itemId, id.namespace, nodeConfiguration);
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> 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<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);
|
||||
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<Void> iqFuture =
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,4 +19,8 @@ public class Field extends Extension {
|
|||
public Collection<String> getValues() {
|
||||
return Collections2.transform(getExtensions(Value.class), Element::getContent);
|
||||
}
|
||||
|
||||
public void setFieldName(String name) {
|
||||
this.setAttribute("var", name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import im.conversations.android.xmpp.model.Extension;
|
|||
|
||||
public abstract class Condition extends Extension {
|
||||
|
||||
private Condition(Class<? extends Extension> clazz) {
|
||||
private Condition(Class<? extends Condition> clazz) {
|
||||
super(clazz);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in a new issue