From 944c48e00be547d9ee8359831da3456418fc2b0f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 19 Jan 2023 09:17:42 +0100 Subject: [PATCH] store presence in DB --- .../1.json | 12 +++-- .../android/database/dao/DiscoDao.java | 13 ++++-- .../android/database/dao/PresenceDao.java | 44 ++++++++++++++++++- .../database/entity/DiscoExtensionEntity.java | 6 ++- .../database/entity/PresenceEntity.java | 19 +++++++- .../android/database/model/PresenceShow.java | 12 ++++- .../android/database/model/PresenceType.java | 12 ++++- .../android/xmpp/manager/DiscoManager.java | 2 + .../xmpp/processor/PresenceProcessor.java | 19 ++++++++ 9 files changed, 127 insertions(+), 12 deletions(-) diff --git a/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/schemas/im.conversations.android.database.ConversationsDatabase/1.json index fc3e8c521..fd5fadb33 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": "adc70f7066828bb6cf1fc32aa3a24b2f", + "identityHash": "a521ff7a3e16cca9f6cbfe51241ec021", "entities": [ { "tableName": "account", @@ -319,7 +319,7 @@ }, { "tableName": "disco_ext", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `discoId` INTEGER NOT NULL, FOREIGN KEY(`discoId`) REFERENCES `disco`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `discoId` INTEGER NOT NULL, `type` TEXT, FOREIGN KEY(`discoId`) REFERENCES `disco`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -332,6 +332,12 @@ "columnName": "discoId", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -1304,7 +1310,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, 'adc70f7066828bb6cf1fc32aa3a24b2f')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a521ff7a3e16cca9f6cbfe51241ec021')" ] } } \ 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 9bc08b268..392f09b9e 100644 --- a/src/main/java/im/conversations/android/database/dao/DiscoDao.java +++ b/src/main/java/im/conversations/android/database/dao/DiscoDao.java @@ -18,7 +18,6 @@ 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; import im.conversations.android.xmpp.model.disco.info.Feature; import im.conversations.android.xmpp.model.disco.info.Identity; @@ -38,6 +37,12 @@ public abstract class DiscoDao { @Insert protected abstract void insertDiscoFeatures(Collection features); + @Query( + "DELETE FROM disco_item WHERE accountId=:account AND parent=:parent AND address NOT" + + " IN(:existent)") + protected abstract void deleteNonExistentDiscoItems( + final long account, final Jid parent, final Collection existent); + @Insert protected abstract void insertDiscoFieldValues( Collection value); @@ -59,6 +64,8 @@ public abstract class DiscoDao { final var entities = Collections2.transform(items, i -> DiscoItemWithParent.of(account.id, parent, i)); insertDiscoItems(entities); + deleteNonExistentDiscoItems( + account.id, parent, Collections2.transform(items, Item::getJid)); } @Transaction @@ -108,8 +115,8 @@ public abstract class DiscoDao { 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 extensionId = insert(DiscoExtensionEntity.of(discoId, data.getFormType())); + for (final var field : data.getFields()) { final var fieldId = insert(DiscoExtensionFieldEntity.of(extensionId, field.getFieldName())); insertDiscoFieldValues( diff --git a/src/main/java/im/conversations/android/database/dao/PresenceDao.java b/src/main/java/im/conversations/android/database/dao/PresenceDao.java index c5cf93945..3942bc6dd 100644 --- a/src/main/java/im/conversations/android/database/dao/PresenceDao.java +++ b/src/main/java/im/conversations/android/database/dao/PresenceDao.java @@ -2,10 +2,50 @@ package im.conversations.android.database.dao; import androidx.room.Dao; import androidx.room.Query; +import androidx.room.Upsert; +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.database.entity.PresenceEntity; +import im.conversations.android.database.model.Account; +import im.conversations.android.database.model.PresenceShow; +import im.conversations.android.database.model.PresenceType; +import java.util.Arrays; @Dao -public interface PresenceDao { +public abstract class PresenceDao { @Query("DELETE FROM presence WHERE accountId=:account") - void deletePresences(long account); + public abstract void deletePresences(long account); + + @Query("DELETE FROM presence WHERE accountId=:account AND address=:address") + abstract void deletePresences(long account, Jid address); + + @Query( + "DELETE FROM presence WHERE accountId=:account AND address=:address AND" + + " resource=:resource") + abstract void deletePresence(long account, Jid address, String resource); + + @Upsert + abstract void insert(PresenceEntity entity); + + public void set( + Account account, + Jid address, + String resource, + PresenceType type, + PresenceShow show, + String status) { + if (resource == null + && Arrays.asList(PresenceType.ERROR, PresenceType.UNAVAILABLE).contains(type)) { + deletePresences(account.id, address); + } + if (type == PresenceType.UNAVAILABLE) { + if (resource != null) { + deletePresence(account.id, address, resource); + } + // unavailable presence only delete previous nothing left to do + return; + } + final var entity = PresenceEntity.of(account.id, address, resource, type, show, status); + insert(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 4eaaad42f..632e599b0 100644 --- a/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java +++ b/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java @@ -1,6 +1,7 @@ package im.conversations.android.database.entity; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; @@ -22,9 +23,12 @@ public class DiscoExtensionEntity { @NonNull public Long discoId; - public static DiscoExtensionEntity of(long discoId) { + @Nullable public String type; + + public static DiscoExtensionEntity of(long discoId, final String type) { final var entity = new DiscoExtensionEntity(); entity.discoId = discoId; + entity.type = type; return entity; } } diff --git a/src/main/java/im/conversations/android/database/entity/PresenceEntity.java b/src/main/java/im/conversations/android/database/entity/PresenceEntity.java index dffb9ca63..22409eed3 100644 --- a/src/main/java/im/conversations/android/database/entity/PresenceEntity.java +++ b/src/main/java/im/conversations/android/database/entity/PresenceEntity.java @@ -31,7 +31,7 @@ public class PresenceEntity { @NonNull public Long accountId; - @NonNull public String address; + @NonNull public Jid address; @Nullable public String resource; @@ -53,4 +53,21 @@ public class PresenceEntity { // set to true if presence has status code 110 (this means we are online) public boolean mucUserSelf; + + public static PresenceEntity of( + long account, + Jid address, + String resource, + PresenceType type, + PresenceShow show, + String status) { + final var entity = new PresenceEntity(); + entity.accountId = account; + entity.address = address; + entity.resource = resource; + entity.type = type; + entity.show = show; + entity.status = status; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/model/PresenceShow.java b/src/main/java/im/conversations/android/database/model/PresenceShow.java index 9ef056429..51a3469be 100644 --- a/src/main/java/im/conversations/android/database/model/PresenceShow.java +++ b/src/main/java/im/conversations/android/database/model/PresenceShow.java @@ -1,8 +1,18 @@ package im.conversations.android.database.model; +import java.util.Locale; + public enum PresenceShow { CHAT, AWAY, XA, - DND + DND; + + public static PresenceShow of(final String value) { + try { + return value == null ? null : valueOf(value.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } } diff --git a/src/main/java/im/conversations/android/database/model/PresenceType.java b/src/main/java/im/conversations/android/database/model/PresenceType.java index 81104c2fc..943c96f55 100644 --- a/src/main/java/im/conversations/android/database/model/PresenceType.java +++ b/src/main/java/im/conversations/android/database/model/PresenceType.java @@ -1,7 +1,17 @@ package im.conversations.android.database.model; +import androidx.annotation.Nullable; +import java.util.Locale; + public enum PresenceType { UNAVAILABLE, ERROR, - SUBSCRIBE + SUBSCRIBE; + + public static PresenceType of(@Nullable String typeAttribute) { + if (typeAttribute == null) { + return null; + } + return of(typeAttribute.toUpperCase(Locale.ROOT)); + } } 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 38cb1043d..a87679685 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -47,6 +47,8 @@ public class DiscoManager extends AbstractManager { } iqRequest.addChild(infoQueryRequest); final var future = connection.sendIqPacket(iqRequest); + // TODO we need to remove the disco info associated with $entity in case of failure + // this might happen in (rather unlikely) scenarios where an item no longer speaks disco return Futures.transform( future, iqResult -> { diff --git a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java index 7324f6f1b..98c9560dd 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java @@ -2,6 +2,8 @@ package im.conversations.android.xmpp.processor; import android.content.Context; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import im.conversations.android.database.model.PresenceShow; +import im.conversations.android.database.model.PresenceType; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.manager.DiscoManager; import java.util.function.Consumer; @@ -14,6 +16,23 @@ public class PresenceProcessor extends XmppConnection.Delegate implements Consum @Override public void accept(final PresencePacket presencePacket) { + final var from = presencePacket.getFrom(); + final var address = from == null ? null : from.asBareJid(); + final var resource = from == null ? null : from.getResource(); + final var typeAttribute = presencePacket.getAttribute("type"); + final PresenceType type; + try { + type = PresenceType.of(typeAttribute); + } catch (final IllegalArgumentException e) { + // log we don’t parse presence of type $type + return; + } + final var show = PresenceShow.of(presencePacket.findChildContent("show")); + final var status = presencePacket.findChildContent("status"); + getDatabase().presenceDao().set(getAccount(), address, resource, type, show, status); + + // TODO store presence info + // TODO do this only for contacts? fetchCapabilities(presencePacket); }