parse caps from presence
This commit is contained in:
parent
a2b21d97eb
commit
43a82e504b
|
@ -5,6 +5,13 @@ public final class Namespace {
|
|||
public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
|
||||
public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
|
||||
public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2";
|
||||
|
||||
public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
|
||||
|
||||
public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
|
||||
|
||||
public static final String HASHES = "urn:xmpp:hashes:2";
|
||||
|
||||
public static final String BLOCKING = "urn:xmpp:blocking";
|
||||
public static final String ROSTER = "jabber:iq:roster";
|
||||
public static final String REGISTER = "jabber:iq:register";
|
||||
|
@ -26,7 +33,8 @@ public final class Namespace {
|
|||
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
|
||||
public static final String PUBSUB_OWNER = PUBSUB + "#owner";
|
||||
public static final String NICK = "http://jabber.org/protocol/nick";
|
||||
public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
|
||||
public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL =
|
||||
"http://jabber.org/protocol/offline";
|
||||
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
|
||||
public static final String BIND2 = "urn:xmpp:bind:0";
|
||||
public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3";
|
||||
|
@ -51,9 +59,12 @@ public final class Namespace {
|
|||
public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
|
||||
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
|
||||
public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video";
|
||||
public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
|
||||
public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
|
||||
public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0";
|
||||
public static final String JINGLE_RTP_HEADER_EXTENSIONS =
|
||||
"urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
|
||||
public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION =
|
||||
"urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
|
||||
public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES =
|
||||
"urn:xmpp:jingle:apps:rtp:ssma:0";
|
||||
public static final String IBB = "http://jabber.org/protocol/ibb";
|
||||
public static final String PING = "urn:xmpp:ping";
|
||||
public static final String PUSH = "urn:xmpp:push:0";
|
||||
|
@ -64,6 +75,7 @@ public final class Namespace {
|
|||
public static final String INVITE = "urn:xmpp:invite";
|
||||
public static final String PARS = "urn:xmpp:pars:0";
|
||||
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
|
||||
public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
|
||||
public static final String OMEMO_DTLS_SRTP_VERIFICATION =
|
||||
"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
|
||||
public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ 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.xmpp.EntityCapabilities;
|
||||
import im.conversations.android.xmpp.EntityCapabilities2;
|
||||
import im.conversations.android.xmpp.model.data.Data;
|
||||
import im.conversations.android.xmpp.model.data.Field;
|
||||
import im.conversations.android.xmpp.model.data.Value;
|
||||
|
@ -59,6 +61,27 @@ public abstract class DiscoDao {
|
|||
insertDiscoItems(entities);
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public boolean set(
|
||||
final Account account,
|
||||
final Jid address,
|
||||
final String node,
|
||||
final EntityCapabilities.Hash capsHash) {
|
||||
final Long existingDiscoId;
|
||||
if (capsHash instanceof EntityCapabilities2.EntityCaps2Hash) {
|
||||
existingDiscoId = getDiscoId(account.id, capsHash.hash);
|
||||
} else if (capsHash instanceof EntityCapabilities.EntityCapsHash) {
|
||||
existingDiscoId = getDiscoIdByCapsHash(account.id, capsHash.hash);
|
||||
} else {
|
||||
existingDiscoId = null;
|
||||
}
|
||||
if (existingDiscoId == null) {
|
||||
return false;
|
||||
}
|
||||
insert(DiscoItemWithDiscoId.of(account.id, address, node, existingDiscoId));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public void set(
|
||||
final Account account,
|
||||
|
@ -100,6 +123,9 @@ public abstract class DiscoDao {
|
|||
@Query("SELECT id FROM disco WHERE accountId=:accountId AND caps2HashSha256=:caps2HashSha256")
|
||||
protected abstract Long getDiscoId(final long accountId, final byte[] caps2HashSha256);
|
||||
|
||||
@Query("SELECT id FROM disco WHERE accountId=:accountId AND capsHash=:capsHash")
|
||||
protected abstract Long getDiscoIdByCapsHash(final long accountId, final byte[] capsHash);
|
||||
|
||||
public static class DiscoItemWithParent {
|
||||
public long accountId;
|
||||
public Jid address;
|
||||
|
|
|
@ -106,5 +106,9 @@ public final class EntityCapabilities {
|
|||
protected EntityCapsHash(byte[] hash) {
|
||||
super(hash);
|
||||
}
|
||||
|
||||
public static EntityCapsHash of(final String encoded) {
|
||||
return new EntityCapsHash(BaseEncoding.base64().decode(encoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package im.conversations.android.xmpp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.hash.HashFunction;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import im.conversations.android.xmpp.model.Hash;
|
||||
import im.conversations.android.xmpp.model.data.Data;
|
||||
import im.conversations.android.xmpp.model.data.Field;
|
||||
import im.conversations.android.xmpp.model.data.Value;
|
||||
|
@ -29,17 +28,17 @@ public class EntityCapabilities2 {
|
|||
private static final char FILE_SEPARATOR = 0x1c;
|
||||
|
||||
public static EntityCaps2Hash hash(final InfoQuery info) {
|
||||
return hash(Algorithm.SHA_256, info);
|
||||
return hash(Hash.Algorithm.SHA_256, info);
|
||||
}
|
||||
|
||||
public static EntityCaps2Hash hash(final Algorithm algorithm, final InfoQuery info) {
|
||||
public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) {
|
||||
final String result = algorithm(info);
|
||||
final var hashFunction = toHashFunction(algorithm);
|
||||
return new EntityCaps2Hash(
|
||||
algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes());
|
||||
}
|
||||
|
||||
private static HashFunction toHashFunction(final Algorithm algorithm) {
|
||||
private static HashFunction toHashFunction(final Hash.Algorithm algorithm) {
|
||||
switch (algorithm) {
|
||||
case SHA_1:
|
||||
return Hashing.sha1();
|
||||
|
@ -149,33 +148,15 @@ public class EntityCapabilities2 {
|
|||
|
||||
public static class EntityCaps2Hash extends EntityCapabilities.Hash {
|
||||
|
||||
public final Algorithm algorithm;
|
||||
public final Hash.Algorithm algorithm;
|
||||
|
||||
protected EntityCaps2Hash(final Algorithm algorithm, byte[] hash) {
|
||||
protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) {
|
||||
super(hash);
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Algorithm {
|
||||
SHA_1,
|
||||
SHA_256,
|
||||
SHA_512;
|
||||
|
||||
public static Algorithm tryParse(@Nullable final String name) {
|
||||
try {
|
||||
return valueOf(
|
||||
CaseFormat.LOWER_HYPHEN.to(
|
||||
CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name)));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString());
|
||||
public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) {
|
||||
return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ public final class Extensions {
|
|||
im.conversations.android.xmpp.model.blocking.Block.class,
|
||||
im.conversations.android.xmpp.model.blocking.Blocklist.class,
|
||||
im.conversations.android.xmpp.model.blocking.Unblock.class,
|
||||
im.conversations.android.xmpp.model.capabilties.LegacyCapabilities.class,
|
||||
im.conversations.android.xmpp.model.capabilties.Capabilities.class,
|
||||
im.conversations.android.xmpp.model.data.Data.class,
|
||||
im.conversations.android.xmpp.model.data.Field.class,
|
||||
im.conversations.android.xmpp.model.data.Value.class,
|
||||
|
@ -32,7 +34,8 @@ public final class Extensions {
|
|||
im.conversations.android.xmpp.model.disco.items.Item.class,
|
||||
im.conversations.android.xmpp.model.disco.items.ItemsQuery.class,
|
||||
im.conversations.android.xmpp.model.roster.Query.class,
|
||||
im.conversations.android.xmpp.model.roster.Item.class);
|
||||
im.conversations.android.xmpp.model.roster.Item.class,
|
||||
im.conversations.android.xmpp.model.Hash.class);
|
||||
|
||||
private static final BiMap<Id, Class<? extends Extension>> EXTENSION_CLASS_MAP;
|
||||
|
||||
|
|
|
@ -2826,7 +2826,7 @@ public class XmppConnection implements Runnable {
|
|||
return ConversationsDatabase.getInstance(context);
|
||||
}
|
||||
|
||||
public <T extends AbstractManager> T getManager(Class<T> type) {
|
||||
protected <T extends AbstractManager> T getManager(Class<T> type) {
|
||||
return connection.managers.getInstance(type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.conversations.android.xmpp.manager;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -26,6 +27,15 @@ public class DiscoManager extends AbstractManager {
|
|||
return info(entity, null);
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> info(
|
||||
final Jid entity, @Nullable final String node, final EntityCapabilities.Hash hash) {
|
||||
// TODO construct node with appended hash
|
||||
if (getDatabase().discoDao().set(getAccount(), entity, node, hash)) {
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
return Futures.transform(info(entity, node), f -> null, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public ListenableFuture<InfoQuery> info(final Jid entity, final String node) {
|
||||
final var iqRequest = new IqPacket(IqPacket.TYPE.GET);
|
||||
iqRequest.setTo(entity);
|
||||
|
|
41
src/main/java/im/conversations/android/xmpp/model/Hash.java
Normal file
41
src/main/java/im/conversations/android/xmpp/model/Hash.java
Normal file
|
@ -0,0 +1,41 @@
|
|||
package im.conversations.android.xmpp.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Strings;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
|
||||
@XmlElement(namespace = Namespace.HASHES)
|
||||
public class Hash extends Extension {
|
||||
public Hash() {
|
||||
super(Hash.class);
|
||||
}
|
||||
|
||||
public Algorithm getAlgorithm() {
|
||||
return Algorithm.tryParse(this.getAttribute("algo"));
|
||||
}
|
||||
|
||||
public enum Algorithm {
|
||||
SHA_1,
|
||||
SHA_256,
|
||||
SHA_512;
|
||||
|
||||
public static Algorithm tryParse(@Nullable final String name) {
|
||||
try {
|
||||
return valueOf(
|
||||
CaseFormat.LOWER_HYPHEN.to(
|
||||
CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name)));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package im.conversations.android.xmpp.model.capabilties;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.EntityCapabilities2;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import im.conversations.android.xmpp.model.Hash;
|
||||
|
||||
@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES_2)
|
||||
public class Capabilities extends Extension {
|
||||
|
||||
public Capabilities() {
|
||||
super(Capabilities.class);
|
||||
}
|
||||
|
||||
public EntityCapabilities2.EntityCaps2Hash getHash() {
|
||||
final Optional<Hash> sha256Hash =
|
||||
Iterables.tryFind(
|
||||
getExtensions(Hash.class), h -> h.getAlgorithm() == Hash.Algorithm.SHA_256);
|
||||
if (sha256Hash.isPresent()) {
|
||||
final String content = sha256Hash.get().getContent();
|
||||
if (Strings.isNullOrEmpty(content)) {
|
||||
return null;
|
||||
}
|
||||
if (BaseEncoding.base64().canDecode(content)) {
|
||||
return EntityCapabilities2.EntityCaps2Hash.of(Hash.Algorithm.SHA_256, content);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package im.conversations.android.xmpp.model.capabilties;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.EntityCapabilities;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES)
|
||||
public class LegacyCapabilities extends Extension {
|
||||
|
||||
private static final String HASH_ALGORITHM = "sha-1";
|
||||
|
||||
public LegacyCapabilities() {
|
||||
super(LegacyCapabilities.class);
|
||||
}
|
||||
|
||||
public String getNode() {
|
||||
return this.getAttribute("node");
|
||||
}
|
||||
|
||||
public EntityCapabilities.EntityCapsHash getHash() {
|
||||
final String hash = getAttribute("hash");
|
||||
final String ver = getAttribute("ver");
|
||||
if (Strings.isNullOrEmpty(ver) || Strings.isNullOrEmpty(hash)) {
|
||||
return null;
|
||||
}
|
||||
if (HASH_ALGORITHM.equals(hash) && BaseEncoding.base64().canDecode(hash)) {
|
||||
return EntityCapabilities.EntityCapsHash.of(hash);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,43 @@ package im.conversations.android.xmpp.processor;
|
|||
|
||||
import android.content.Context;
|
||||
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
|
||||
import im.conversations.android.xmpp.EntityCapabilities;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.manager.DiscoManager;
|
||||
import im.conversations.android.xmpp.model.capabilties.Capabilities;
|
||||
import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PresenceProcessor implements Consumer<PresencePacket> {
|
||||
public class PresenceProcessor extends XmppConnection.Delegate implements Consumer<PresencePacket> {
|
||||
|
||||
public PresenceProcessor(final Context context, final XmppConnection connection) {}
|
||||
public PresenceProcessor(final Context context, final XmppConnection connection) {
|
||||
super(context, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PresencePacket presencePacket) {}
|
||||
public void accept(final PresencePacket presencePacket) {
|
||||
// TODO do this only for contacts?
|
||||
fetchCapabilities(presencePacket);
|
||||
}
|
||||
|
||||
private void fetchCapabilities(final PresencePacket presencePacket) {
|
||||
final var entity = presencePacket.getFrom();
|
||||
final String node;
|
||||
final EntityCapabilities.Hash hash;
|
||||
final var capabilities = presencePacket.getExtension(Capabilities.class);
|
||||
final var legacyCapabilities = presencePacket.getExtension(LegacyCapabilities.class);
|
||||
if (capabilities != null) {
|
||||
node = null;
|
||||
hash = capabilities.getHash();
|
||||
} else if (legacyCapabilities != null) {
|
||||
node = legacyCapabilities.getNode();
|
||||
hash = legacyCapabilities.getHash();
|
||||
} else {
|
||||
node = null;
|
||||
hash = null;
|
||||
}
|
||||
if (hash != null) {
|
||||
getManager(DiscoManager.class).info(entity, node, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue