add HttpUploadManager slot request

This commit is contained in:
Daniel Gultsch 2023-03-05 12:09:56 +01:00
parent f9b3d42a8a
commit f1fbf15fea
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
14 changed files with 352 additions and 7 deletions

View file

@ -8,6 +8,7 @@ import androidx.room.Query;
import androidx.room.Transaction;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.util.concurrent.ListenableFuture;
import im.conversations.android.database.entity.DiscoEntity;
import im.conversations.android.database.entity.DiscoExtensionEntity;
import im.conversations.android.database.entity.DiscoExtensionFieldEntity;
@ -16,6 +17,7 @@ import im.conversations.android.database.entity.DiscoFeatureEntity;
import im.conversations.android.database.entity.DiscoIdentityEntity;
import im.conversations.android.database.entity.DiscoItemEntity;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.DiscoItemWithExtension;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.EntityCapabilities2;
@ -24,7 +26,9 @@ import im.conversations.android.xmpp.model.data.Value;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
import java.util.Collection;
import java.util.List;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.parts.Resourcepart;
@ -223,4 +227,11 @@ public abstract class DiscoDao {
"Discovering features for %s is not implemented",
entity.getClass().getName()));
}
@Query(
"SELECT disco_item.discoId,address FROM disco_item JOIN disco_feature ON"
+ " disco_item.discoId=disco_feature.discoId WHERE feature=:feature AND"
+ " (address=:address OR parentAddress=:address)")
public abstract ListenableFuture<List<DiscoItemWithExtension>> getItemByFeature(
DomainBareJid address, final String feature);
}

View file

@ -0,0 +1,36 @@
package im.conversations.android.database.model;
import androidx.room.Relation;
import com.google.common.collect.Iterables;
import im.conversations.android.database.entity.DiscoExtensionFieldEntity;
import im.conversations.android.database.entity.DiscoExtensionFieldValueEntity;
import java.util.List;
public class DiscoExtension {
public long id;
public String type;
@Relation(
entity = DiscoExtensionFieldEntity.class,
parentColumn = "id",
entityColumn = "extensionId")
public List<Field> fields;
public Field getField(final String name) {
return Iterables.find(fields, f -> name.equals(f.field), null);
}
public static class Field {
public long id;
public String field;
@Relation(
entity = DiscoExtensionFieldValueEntity.class,
parentColumn = "id",
entityColumn = "fieldId",
projection = {"value"})
public List<String> values;
}
}

View file

@ -0,0 +1,23 @@
package im.conversations.android.database.model;
import androidx.room.Relation;
import com.google.common.collect.Iterables;
import im.conversations.android.database.entity.DiscoExtensionEntity;
import java.util.List;
import org.jxmpp.jid.Jid;
public class DiscoItemWithExtension {
public Long discoId;
public Jid address;
@Relation(
entity = DiscoExtensionEntity.class,
parentColumn = "discoId",
entityColumn = "discoId")
public List<DiscoExtension> extensions;
public DiscoExtension getExtension(final String type) {
return Iterables.find(extensions, e -> type.equals(e.type), null);
}
}

View file

@ -17,6 +17,7 @@ import im.conversations.android.xmpp.model.pubsub.event.Retract;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.slf4j.Logger;
@ -86,7 +87,7 @@ public class BookmarkManager extends AbstractManager {
}
}
public ListenableFuture<Void> publishBookmark(final Jid address, final boolean autoJoin) {
public ListenableFuture<Void> publishBookmark(final BareJid address, final boolean autoJoin) {
return publishBookmark(address, autoJoin, null);
}

View file

@ -13,6 +13,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import im.conversations.android.BuildConfig;
import im.conversations.android.R;
import im.conversations.android.database.model.DiscoItemWithExtension;
import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.EntityCapabilities;
@ -240,6 +241,13 @@ public class DiscoManager extends AbstractManager {
MoreExecutors.directExecutor());
}
public ListenableFuture<List<DiscoItemWithExtension>> getServerItemByFeature(
final String feature) {
return getDatabase()
.discoDao()
.getItemByFeature(getAccount().address.asDomainBareJid(), feature);
}
public boolean hasFeature(final Entity entity, final String feature) {
return getDatabase().discoDao().hasFeature(getAccount().id, entity, feature);
}

View file

@ -1,11 +1,166 @@
package im.conversations.android.xmpp.manager;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import im.conversations.android.database.model.DiscoItemWithExtension;
import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.upload.Get;
import im.conversations.android.xmpp.model.upload.Header;
import im.conversations.android.xmpp.model.upload.Put;
import im.conversations.android.xmpp.model.upload.Request;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpUploadManager extends AbstractManager {
private static final List<String> ALLOWED_HEADERS =
Arrays.asList("Authorization", "Cookie", "Expires");
private static final Logger LOGGER = LoggerFactory.getLogger(HttpUploadManager.class);
public HttpUploadManager(Context context, XmppConnection connection) {
super(context, connection);
}
public ListenableFuture<Slot> request(
final String filename, final long size, final MediaType mediaType) {
return Futures.transformAsync(
getManager(DiscoManager.class).getServerItemByFeature(Namespace.HTTP_UPLOAD),
items -> {
final List<Service> services = Service.of(items);
final Service service =
Iterables.find(
services,
s -> s.maxFileSize == 0 || s.maxFileSize >= size,
null);
if (service == null) {
throw new IllegalStateException(
String.format(
"No upload service found that can handle files of size %d",
size));
}
LOGGER.info("Requesting slot from {}", service.address);
return request(service.address, filename, size, mediaType);
},
MoreExecutors.directExecutor());
}
private ListenableFuture<Slot> request(
final Jid address, final String filename, final long size, final MediaType mediaType) {
final var iq = new Iq(Iq.Type.GET);
iq.setTo(address);
final var request = iq.addExtension(new Request());
request.setFilename(filename);
request.setSize(size);
request.setContentType(mediaType.type());
final var iqFuture = connection.sendIqPacket(iq);
// catch and rethrow 'file-too-large'
return Futures.transform(
iqFuture,
result -> {
final var slot =
result.getExtension(
im.conversations.android.xmpp.model.upload.Slot.class);
if (slot == null) {
throw new IllegalStateException("No slot in response");
}
final var get = slot.getExtension(Get.class);
final var put = slot.getExtension(Put.class);
final var getUrl = get == null ? null : get.getUrl();
final var putUrl = put == null ? null : put.getUrl();
if (get == null || put == null) {
throw new IllegalStateException("Missing put or get URL in response");
}
final ImmutableMap.Builder<String, String> headers =
new ImmutableMap.Builder<>();
for (final Header header : put.getHeaders()) {
final String name = header.getHeaderName();
final String value = header.getContent();
if (value != null && ALLOWED_HEADERS.contains(name)) {
headers.put(name, value);
}
}
return new Slot(putUrl, headers.buildKeepingLast(), getUrl);
},
MoreExecutors.directExecutor());
}
public static class Slot {
public final HttpUrl put;
public final Map<String, String> headers;
public final HttpUrl get;
public Slot(HttpUrl put, final Map<String, String> headers, final HttpUrl get) {
this.put = put;
this.headers = headers;
this.get = get;
}
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("put", put)
.add("headers", headers)
.add("get", get)
.toString();
}
}
private static class Service {
public final Jid address;
public final long maxFileSize;
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("address", address)
.add("maxFileSize", maxFileSize)
.toString();
}
private Service(Jid address, long maxFileSize) {
this.address = address;
this.maxFileSize = maxFileSize;
}
public static List<Service> of(List<DiscoItemWithExtension> items) {
return Lists.transform(items, Service::of);
}
private static Service of(final DiscoItemWithExtension item) {
final var discoExtension = item.getExtension(Namespace.HTTP_UPLOAD);
final long maxFileSize;
if (discoExtension == null) {
maxFileSize = 0;
} else {
final var field = discoExtension.getField("max-file-size");
final var value = field == null ? null : Iterables.getFirst(field.values, null);
if (Strings.isNullOrEmpty(value)) {
maxFileSize = 0;
} else {
maxFileSize = Longs.tryParse(value);
}
}
return new Service(item.address, maxFileSize);
}
}
}

View file

@ -63,7 +63,8 @@ public class MultiUserChatManager extends AbstractManager {
}
private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) {
if (infoQuery.hasFeature(Namespace.MUC)) {
if (infoQuery.hasFeature(Namespace.MUC)
&& infoQuery.hasIdentityWithCategory("conference")) {
sendJoinPresence(mucWithNick);
} else {
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
@ -91,8 +92,6 @@ public class MultiUserChatManager extends AbstractManager {
final MucUser mucUser = presencePacket.getExtension(MucUser.class);
Preconditions.checkArgument(
mucUser.getStatus().contains(MucUser.STATUS_CODE_SELF_PRESENCE));
// TODO flag chat as joined
LOGGER.info("Received self presence for {}", presencePacket.getFrom());
final var database = getDatabase();
database.runInTransaction(
() -> {
@ -107,7 +106,6 @@ public class MultiUserChatManager extends AbstractManager {
"Available presence received for archived or non existent chat");
return;
}
// TODO set status codes
database.chatDao()
.setMucState(
chatIdentifier.id, MucState.AVAILABLE, mucUser.getStatus());
@ -132,8 +130,11 @@ public class MultiUserChatManager extends AbstractManager {
} else if (chatIdentifier.archived) {
database.chatDao().setMucState(chatIdentifier.id, null);
} else {
// TODO set status codes
database.chatDao().setMucState(chatIdentifier.id, MucState.UNAVAILABLE);
database.chatDao()
.setMucState(
chatIdentifier.id,
MucState.UNAVAILABLE,
mucUser.getStatus());
}
});
}

View file

@ -31,4 +31,8 @@ public class InfoQuery extends Extension {
public Collection<Identity> getIdentities() {
return this.getExtensions(Identity.class);
}
public boolean hasIdentityWithCategory(final String category) {
return Iterables.any(getIdentities(), i -> category.equals(i.getCategory()));
}
}

View file

@ -0,0 +1,22 @@
package im.conversations.android.xmpp.model.upload;
import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import okhttp3.HttpUrl;
@XmlElement
public class Get extends Extension {
public Get() {
super(Get.class);
}
public HttpUrl getUrl() {
final var url = this.getAttribute("url");
if (Strings.isNullOrEmpty(url)) {
return null;
}
return HttpUrl.parse(url);
}
}

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.upload;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Header extends Extension {
public Header() {
super(Header.class);
}
public String getHeaderName() {
return this.getAttribute("name");
}
}

View file

@ -0,0 +1,27 @@
package im.conversations.android.xmpp.model.upload;
import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
import okhttp3.HttpUrl;
@XmlElement
public class Put extends Extension {
public Put() {
super(Put.class);
}
public HttpUrl getUrl() {
final var url = this.getAttribute("url");
if (Strings.isNullOrEmpty(url)) {
return null;
}
return HttpUrl.parse(url);
}
public Collection<Header> getHeaders() {
return this.getExtensions(Header.class);
}
}

View file

@ -0,0 +1,24 @@
package im.conversations.android.xmpp.model.upload;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Request extends Extension {
public Request() {
super(Request.class);
}
public void setFilename(String filename) {
this.setAttribute("filename", filename);
}
public void setSize(long size) {
this.setAttribute("size", size);
}
public void setContentType(String type) {
this.setAttribute("content-ype", type);
}
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.upload;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Slot extends Extension {
public Slot() {
super(Slot.class);
}
}

View file

@ -0,0 +1,5 @@
@XmlPackage(namespace = Namespace.HTTP_UPLOAD)
package im.conversations.android.xmpp.model.upload;
import im.conversations.android.annotation.XmlPackage;
import im.conversations.android.xml.Namespace;