diff --git a/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/schemas/im.conversations.android.database.ConversationsDatabase/1.json index ca6b562e6..fc3e8c521 100644 --- a/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "afc5b1df44123031e340e1a3db15396d", + "identityHash": "adc70f7066828bb6cf1fc32aa3a24b2f", "entities": [ { "tableName": "account", @@ -248,7 +248,7 @@ }, { "tableName": "disco", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `capsHash` BLOB, `caps2Hash` BLOB, `caps2Algorithm` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `capsHash` BLOB, `caps2HashSha256` BLOB, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -269,16 +269,10 @@ "notNull": false }, { - "fieldPath": "caps2Hash", - "columnName": "caps2Hash", + "fieldPath": "caps2HashSha256", + "columnName": "caps2HashSha256", "affinity": "BLOB", "notNull": false - }, - { - "fieldPath": "caps2Algorithm", - "columnName": "caps2Algorithm", - "affinity": "TEXT", - "notNull": false } ], "primaryKey": { @@ -289,13 +283,24 @@ }, "indices": [ { - "name": "index_disco_accountId", + "name": "index_disco_accountId_capsHash", "unique": false, "columnNames": [ - "accountId" + "accountId", + "capsHash" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_disco_accountId` ON `${TABLE_NAME}` (`accountId`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_disco_accountId_capsHash` ON `${TABLE_NAME}` (`accountId`, `capsHash`)" + }, + { + "name": "index_disco_accountId_caps2HashSha256", + "unique": true, + "columnNames": [ + "accountId", + "caps2HashSha256" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_disco_accountId_caps2HashSha256` ON `${TABLE_NAME}` (`accountId`, `caps2HashSha256`)" } ], "foreignKeys": [ @@ -1299,7 +1304,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'afc5b1df44123031e340e1a3db15396d')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'adc70f7066828bb6cf1fc32aa3a24b2f')" ] } } \ No newline at end of file diff --git a/src/main/java/im/conversations/android/database/dao/DiscoDao.java b/src/main/java/im/conversations/android/database/dao/DiscoDao.java index f2a9aaf72..e14b48796 100644 --- a/src/main/java/im/conversations/android/database/dao/DiscoDao.java +++ b/src/main/java/im/conversations/android/database/dao/DiscoDao.java @@ -1,12 +1,26 @@ package im.conversations.android.database.dao; import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; import androidx.room.Transaction; import androidx.room.Upsert; import com.google.common.collect.Collections2; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.database.entity.DiscoEntity; +import im.conversations.android.database.entity.DiscoExtensionEntity; +import im.conversations.android.database.entity.DiscoExtensionFieldEntity; +import im.conversations.android.database.entity.DiscoExtensionFieldValueEntity; +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.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.data.Value; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.items.Item; import java.util.Collection; @@ -14,16 +28,78 @@ import java.util.Collection; public abstract class DiscoDao { @Upsert(entity = DiscoItemEntity.class) - protected abstract void setDiscoItems(Collection items); + protected abstract void insertDiscoItems(Collection items); + + @Insert + protected abstract void insertDiscoIdentities(Collection identities); + + @Insert + protected abstract void insertDiscoFeatures(Collection features); + + @Insert + protected abstract void insertDiscoFieldValues( + Collection value); + + @Upsert(entity = DiscoItemEntity.class) + protected abstract void insert(DiscoItemWithDiscoId item); + + @Insert + protected abstract long insert(DiscoEntity entity); + + @Insert + protected abstract long insert(DiscoExtensionEntity entity); + + @Insert + protected abstract long insert(DiscoExtensionFieldEntity entity); @Transaction - public void setDiscoItems( - final Account account, final Jid parent, final Collection items) { + public void set(final Account account, final Jid parent, final Collection items) { final var entities = Collections2.transform(items, i -> DiscoItemWithParent.of(account.id, parent, i)); - setDiscoItems(entities); + insertDiscoItems(entities); } + @Transaction + public void set( + final Account account, + final Jid address, + final String node, + final byte[] capsHash, + final byte[] caps2HashSha256, + final InfoQuery infoQuery) { + + final Long existingDiscoId = getDiscoId(account.id, caps2HashSha256); + if (existingDiscoId != null) { + insert(DiscoItemWithDiscoId.of(account.id, address, node, existingDiscoId)); + return; + } + final long discoId = insert(DiscoEntity.of(account.id, capsHash, caps2HashSha256)); + + insertDiscoIdentities( + Collections2.transform( + infoQuery.getExtensions(Identity.class), + i -> DiscoIdentityEntity.of(discoId, i))); + + insertDiscoFeatures( + Collections2.transform( + infoQuery.getExtensions(Feature.class), + f -> DiscoFeatureEntity.of(discoId, f.getVar()))); + for (final Data data : infoQuery.getExtensions(Data.class)) { + final var extensionId = insert(DiscoExtensionEntity.of(discoId)); + for (final var field : data.getExtensions(Field.class)) { + final var fieldId = + insert(DiscoExtensionFieldEntity.of(extensionId, field.getFieldName())); + insertDiscoFieldValues( + Collections2.transform( + field.getExtensions(Value.class), + v -> DiscoExtensionFieldValueEntity.of(fieldId, v.getContent()))); + } + } + } + + @Query("SELECT id FROM disco WHERE accountId=:accountId AND caps2HashSha256=:caps2HashSha256") + protected abstract Long getDiscoId(final long accountId, final byte[] caps2HashSha256); + public static class DiscoItemWithParent { public long accountId; public Jid address; @@ -40,4 +116,21 @@ public abstract class DiscoDao { return entity; } } + + public static class DiscoItemWithDiscoId { + public long accountId; + public Jid address; + public String node; + public long discoId; + + public static DiscoItemWithDiscoId of( + final long account, final Jid address, final String node, final long discoId) { + final var entity = new DiscoItemWithDiscoId(); + entity.accountId = account; + entity.address = address; + entity.node = node; + entity.discoId = discoId; + return entity; + } + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoEntity.java index 13be54d76..a4f30325e 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoEntity.java @@ -14,15 +14,28 @@ import androidx.room.PrimaryKey; parentColumns = {"id"}, childColumns = {"accountId"}, onDelete = ForeignKey.CASCADE), - indices = {@Index(value = {"accountId"})}) + indices = { + @Index(value = {"accountId", "capsHash"}), + @Index( + value = {"accountId", "caps2HashSha256"}, + unique = true) + }) public class DiscoEntity { @PrimaryKey(autoGenerate = true) public Long id; - @NonNull Long accountId; + @NonNull public Long accountId; public byte[] capsHash; - public byte[] caps2Hash; - public String caps2Algorithm; + public byte[] caps2HashSha256; + + public static DiscoEntity of( + final long accountId, final byte[] capsHash, final byte[] caps2HashSha256) { + final var entity = new DiscoEntity(); + entity.accountId = accountId; + entity.capsHash = capsHash; + entity.caps2HashSha256 = caps2HashSha256; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java index 777c63ee5..4eaaad42f 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java @@ -21,4 +21,10 @@ public class DiscoExtensionEntity { public Long id; @NonNull public Long discoId; + + public static DiscoExtensionEntity of(long discoId) { + final var entity = new DiscoExtensionEntity(); + entity.discoId = discoId; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java index 5a63b7916..aef8bb4e8 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java @@ -23,4 +23,11 @@ public class DiscoExtensionFieldEntity { @NonNull public Long extensionId; public String field; + + public static DiscoExtensionFieldEntity of(final long extensionId, final String fieldName) { + final var entity = new DiscoExtensionFieldEntity(); + entity.extensionId = extensionId; + entity.field = fieldName; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java index b2847c236..706b4751f 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java @@ -23,4 +23,8 @@ public class DiscoExtensionFieldValueEntity { @NonNull public Long fieldId; public String value; + + public static DiscoExtensionFieldValueEntity of(long fieldId, final String value) { + return null; + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java index 6366ca6a5..6f33b5433 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java @@ -23,4 +23,11 @@ public class DiscoFeatureEntity { @NonNull public Long discoId; @NonNull public String feature; + + public static DiscoFeatureEntity of(final long discoId, final String feature) { + final var entity = new DiscoFeatureEntity(); + entity.discoId = discoId; + entity.feature = feature; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java b/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java index e3a000486..f6124ffba 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java @@ -5,6 +5,7 @@ import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; +import im.conversations.android.xmpp.model.disco.info.Identity; @Entity( tableName = "disco_identity", @@ -25,4 +26,13 @@ public class DiscoIdentityEntity { public String category; public String type; public String name; + + public static DiscoIdentityEntity of(final long discoId, final Identity i) { + final var entity = new DiscoIdentityEntity(); + entity.discoId = discoId; + entity.category = i.getCategory(); + entity.type = i.getType(); + entity.name = i.getIdentityName(); + return entity; + } } diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java index 20290520f..2a58f6e39 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -4,6 +4,7 @@ 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.primitives.Bytes; import im.conversations.android.xmpp.model.data.Data; @@ -25,8 +26,12 @@ public class EntityCapabilities2 { private static final char FILE_SEPARATOR = 0x1c; public static byte[] hash(final InfoQuery info) { + return hash(Hashing.sha256(), info); + } + + public static byte[] hash(HashFunction hashFunction, final InfoQuery info) { final String algo = algorithm(info); - return Hashing.sha256().hashString(algo, StandardCharsets.UTF_8).asBytes(); + return hashFunction.hashString(algo, StandardCharsets.UTF_8).asBytes(); } private static String asHex(final String message) { diff --git a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java index 94c34f8cc..eba58cbeb 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -7,6 +7,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.EntityCapabilities2; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.items.Item; @@ -21,18 +23,34 @@ public class DiscoManager extends AbstractManager { } public ListenableFuture info(final Jid entity) { - final var iqPacket = new IqPacket(IqPacket.TYPE.GET); - iqPacket.setTo(entity); - iqPacket.addChild(new InfoQuery()); - final var future = connection.sendIqPacket(iqPacket); + return info(entity, null); + } + + public ListenableFuture info(final Jid entity, final String node) { + final var iqRequest = new IqPacket(IqPacket.TYPE.GET); + iqRequest.setTo(entity); + final var infoQueryRequest = new InfoQuery(); + if (node != null) { + infoQueryRequest.setNode(node); + } + iqRequest.addChild(infoQueryRequest); + final var future = connection.sendIqPacket(iqRequest); return Futures.transform( future, iqResult -> { final var infoQuery = iqResult.getExtension(InfoQuery.class); if (infoQuery == null) { - throw new IllegalStateException(); + throw new IllegalStateException("Response did not have query child"); } - // TODO store query + if (!Objects.equals(node, infoQuery.getNode())) { + throw new IllegalStateException( + "Node in response did not match node in request"); + } + final byte[] caps = EntityCapabilities.hash(infoQuery); + final byte[] caps2 = EntityCapabilities2.hash(infoQuery); + getDatabase() + .discoDao() + .set(getAccount(), entity, node, caps, caps2, infoQuery); return infoQuery; }, MoreExecutors.directExecutor()); @@ -53,7 +71,7 @@ public class DiscoManager extends AbstractManager { final var items = itemsQuery.getExtensions(Item.class); final var validItems = Collections2.filter(items, i -> Objects.nonNull(i.getJid())); - getDatabase().discoDao().setDiscoItems(getAccount(), entity, validItems); + getDatabase().discoDao().set(getAccount(), entity, validItems); return validItems; }, MoreExecutors.directExecutor()); diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java index 62f4d8fb2..8b8935da8 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -9,4 +9,12 @@ public class InfoQuery extends Extension { public InfoQuery() { super(InfoQuery.class); } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public String getNode() { + return this.getAttribute("node"); + } }