store reference to inReplyTo in database

This commit is contained in:
Daniel Gultsch 2023-02-13 12:01:43 +01:00
parent 56a462833e
commit 405445afbe
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
11 changed files with 205 additions and 8 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "b6a7be8218829fd38f51dcd76cb9cccd", "identityHash": "219a451e9a1889222b7549c8b3c0a5b3",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -1420,7 +1420,7 @@
}, },
{ {
"tableName": "message", "tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `outgoing` INTEGER NOT NULL, `toBare` TEXT, `toResource` TEXT, `fromBare` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `stanzaIdVerified` INTEGER NOT NULL, `latestVersion` INTEGER, `acknowledged` INTEGER NOT NULL, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`latestVersion`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `outgoing` INTEGER NOT NULL, `toBare` TEXT, `toResource` TEXT, `fromBare` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `stanzaIdVerified` INTEGER NOT NULL, `latestVersion` INTEGER, `acknowledged` INTEGER NOT NULL, `inReplyToMessageId` TEXT, `inReplyToStanzaId` TEXT, `inReplyToMessageEntityId` INTEGER, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`latestVersion`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToMessageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -1511,6 +1511,24 @@
"columnName": "acknowledged", "columnName": "acknowledged",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
},
{
"fieldPath": "inReplyToMessageId",
"columnName": "inReplyToMessageId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToStanzaId",
"columnName": "inReplyToStanzaId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToMessageEntityId",
"columnName": "inReplyToMessageEntityId",
"affinity": "INTEGER",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -1537,6 +1555,15 @@
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_latestVersion` ON `${TABLE_NAME}` (`latestVersion`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_message_latestVersion` ON `${TABLE_NAME}` (`latestVersion`)"
},
{
"name": "index_message_inReplyToMessageEntityId",
"unique": false,
"columnNames": [
"inReplyToMessageEntityId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_inReplyToMessageEntityId` ON `${TABLE_NAME}` (`inReplyToMessageEntityId`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -1561,6 +1588,17 @@
"referencedColumns": [ "referencedColumns": [
"id" "id"
] ]
},
{
"table": "message",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"inReplyToMessageEntityId"
],
"referencedColumns": [
"id"
]
} }
] ]
}, },
@ -2228,7 +2266,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, 'b6a7be8218829fd38f51dcd76cb9cccd')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '219a451e9a1889222b7549c8b3c0a5b3')"
] ]
} }
} }

View file

@ -9,6 +9,7 @@ import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.IDs; import im.conversations.android.IDs;
import im.conversations.android.database.ConversationsDatabase; import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.model.EmbeddedMessage;
import im.conversations.android.database.model.Modification; import im.conversations.android.database.model.Modification;
import im.conversations.android.transformer.Transformation; import im.conversations.android.transformer.Transformation;
import im.conversations.android.transformer.Transformer; import im.conversations.android.transformer.Transformer;
@ -16,6 +17,7 @@ import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.jabber.Body; import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.reactions.Reaction; import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.reply.Reply;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant; import java.time.Instant;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -371,4 +373,37 @@ public class TransformationTest {
Assert.assertEquals( Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body); "Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
} }
@Test
public void inReplyTo() {
final var m1 = new Message();
m1.setId("1");
m1.setTo(ACCOUNT);
m1.setFrom(REMOTE.withResource("junit"));
m1.addExtension(new Body("Hi. How are you?"));
this.transformer.transform(Transformation.of(m1, Instant.now(), REMOTE, "stanza-a", null));
final var m2 = new Message();
m2.setId("2");
m2.setTo(REMOTE);
m2.setFrom(ACCOUNT);
m2.addExtension(new Body("I am fine."));
final var reply = m2.addExtension(new Reply());
reply.setId("1");
reply.setTo(REMOTE);
this.transformer.transform(Transformation.of(m2, Instant.now(), REMOTE, "stanza-b", null));
final var messages = database.messageDao().getMessages(1L);
Assert.assertEquals(2, messages.size());
final var response = Iterables.get(messages, 1);
Assert.assertNotNull(response.inReplyToMessageEntityId);
final EmbeddedMessage embeddedMessage = response.inReplyTo;
Assert.assertNotNull(embeddedMessage);
Assert.assertEquals(REMOTE, embeddedMessage.fromBare);
Assert.assertEquals(1L, embeddedMessage.contents.size());
Assert.assertEquals(
"Hi. How are you?", Iterables.getOnlyElement(embeddedMessage.contents).body);
}
} }

View file

@ -70,7 +70,6 @@ public final class Namespace {
public static final String MUC = "http://jabber.org/protocol/muc"; public static final String MUC = "http://jabber.org/protocol/muc";
public static final String MUC_USER = MUC + "#user"; public static final String MUC_USER = MUC + "#user";
public static final String NICK = "http://jabber.org/protocol/nick"; public static final String NICK = "http://jabber.org/protocol/nick";
public static final String REACTIONS = "urn:xmpp:reactions:0";
public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0"; public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0";
public static final String OMEMO_DTLS_SRTP_VERIFICATION = public static final String OMEMO_DTLS_SRTP_VERIFICATION =
"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
@ -88,8 +87,10 @@ public final class Namespace {
public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items"; public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items";
public static final String PUB_SUB_PUBLISH_OPTIONS = PUB_SUB + "#publish-options"; public static final String PUB_SUB_PUBLISH_OPTIONS = PUB_SUB + "#publish-options";
public static final String PUSH = "urn:xmpp:push:0"; public static final String PUSH = "urn:xmpp:push:0";
public static final String REACTIONS = "urn:xmpp:reactions:0";
public static final String REGISTER = "jabber:iq:register"; public static final String REGISTER = "jabber:iq:register";
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register"; public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
public static final String REPLY = "urn:xmpp:reply:0";
public static final String ROSTER = "jabber:iq:roster"; public static final String ROSTER = "jabber:iq:roster";
public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String SASL_2 = "urn:xmpp:sasl:2";

View file

@ -135,7 +135,7 @@ public class InvalidJid implements Jid {
} }
public static boolean isValid(Jid jid) { public static boolean isValid(Jid jid) {
return !(jid != null && jid instanceof InvalidJid); return !(jid instanceof InvalidJid);
} }
public static boolean invalid(final Jid jid) { public static boolean invalid(final Jid jid) {

View file

@ -399,8 +399,45 @@ public abstract class MessageDao {
@Query( @Query(
"SELECT message.id as" "SELECT message.id as"
+ " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion" + " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion"
+ " as version FROM message JOIN message_version ON" + " as version,inReplyToMessageEntityId FROM message JOIN message_version ON"
+ " message.latestVersion=message_version.id WHERE message.chatId=:chatId AND" + " message.latestVersion=message_version.id WHERE message.chatId=:chatId AND"
+ " latestVersion IS NOT NULL ORDER BY message.receivedAt") + " latestVersion IS NOT NULL ORDER BY message.receivedAt")
public abstract List<MessageWithContentReactions> getMessages(long chatId); public abstract List<MessageWithContentReactions> getMessages(long chatId);
public void setInReplyTo(
ChatIdentifier chat,
MessageIdentifier messageIdentifier,
Message.Type messageType,
final Jid to,
String inReplyTo) {
if (messageType == Message.Type.GROUPCHAT) {
final Long messageEntityId = getMessageByStanzaId(chat.id, inReplyTo);
setInReplyToStanzaId(messageIdentifier.id, inReplyTo, messageEntityId);
} else {
final Long messageEntityId = getMessageByMessageId(chat.id, to.asBareJid(), inReplyTo);
setInReplyToMessageId(messageIdentifier.id, inReplyTo, messageEntityId);
}
}
@Query(
"UPDATE message SET"
+ " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId"
+ " WHERE id=:id")
protected abstract void setInReplyToStanzaId(
final long id, String stanzaId, long inReplyToMessageEntityId);
@Query(
"UPDATE message SET"
+ " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId"
+ " WHERE id=:id")
protected abstract void setInReplyToMessageId(
final long id, String messageId, long inReplyToMessageEntityId);
@Query(
"SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND"
+ " messageId=:messageId")
protected abstract Long getMessageByMessageId(long chatId, Jid fromBare, String messageId);
@Query("SELECT id FROM message WHERE chatId=:chatId AND stanzaId=:stanzaId")
protected abstract Long getMessageByStanzaId(long chatId, String stanzaId);
} }

View file

@ -24,8 +24,17 @@ import java.util.Objects;
parentColumns = {"id"}, parentColumns = {"id"},
childColumns = {"latestVersion"}, childColumns = {"latestVersion"},
onDelete = ForeignKey.CASCADE), onDelete = ForeignKey.CASCADE),
@ForeignKey(
entity = MessageEntity.class,
parentColumns = {"id"},
childColumns = {"inReplyToMessageEntityId"},
onDelete = ForeignKey.SET_NULL),
}, },
indices = {@Index(value = "chatId"), @Index(value = "latestVersion")}) indices = {
@Index(value = "chatId"),
@Index(value = "latestVersion"),
@Index("inReplyToMessageEntityId")
})
public class MessageEntity { public class MessageEntity {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -55,6 +64,10 @@ public class MessageEntity {
public boolean acknowledged = false; public boolean acknowledged = false;
public String inReplyToMessageId;
public String inReplyToStanzaId;
@Nullable public Long inReplyToMessageEntityId;
public static MessageEntity of(final long chatId, final Transformation transformation) { public static MessageEntity of(final long chatId, final Transformation transformation) {
final var entity = new MessageEntity(); final var entity = new MessageEntity();
entity.chatId = chatId; entity.chatId = chatId;

View file

@ -0,0 +1,23 @@
package im.conversations.android.database.model;
import androidx.room.Relation;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.database.entity.MessageContentEntity;
import java.time.Instant;
import java.util.List;
public class EmbeddedMessage {
public long id;
public Jid fromBare;
public String fromResource;
public Instant sentAt;
public Long latestVersion;
@Relation(
entity = MessageContentEntity.class,
parentColumn = "latestVersion",
entityColumn = "messageVersionId")
public List<MessageContent> contents;
}

View file

@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.database.entity.MessageContentEntity; import im.conversations.android.database.entity.MessageContentEntity;
import im.conversations.android.database.entity.MessageEntity;
import im.conversations.android.database.entity.MessageReactionEntity; import im.conversations.android.database.entity.MessageReactionEntity;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.Collection;
@ -28,6 +29,13 @@ public class MessageWithContentReactions {
public Modification modification; public Modification modification;
public long version; public long version;
public Long inReplyToMessageEntityId;
@Relation(
entity = MessageEntity.class,
parentColumn = "inReplyToMessageEntityId",
entityColumn = "id")
public EmbeddedMessage inReplyTo;
@Relation( @Relation(
entity = MessageContentEntity.class, entity = MessageContentEntity.class,

View file

@ -17,6 +17,7 @@ import im.conversations.android.xmpp.model.markers.Displayed;
import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import im.conversations.android.xmpp.model.muc.user.MultiUserChat;
import im.conversations.android.xmpp.model.oob.OutOfBandData; import im.conversations.android.xmpp.model.oob.OutOfBandData;
import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.reply.Reply;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
@ -36,7 +37,8 @@ public class Transformation {
MultiUserChat.class, MultiUserChat.class,
Displayed.class, Displayed.class,
Replace.class, Replace.class,
Reactions.class); Reactions.class,
Reply.class);
public final Instant receivedAt; public final Instant receivedAt;
public final Jid to; public final Jid to;

View file

@ -4,6 +4,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import eu.siacs.conversations.xmpp.InvalidJid;
import im.conversations.android.database.ConversationsDatabase; import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.ChatIdentifier; import im.conversations.android.database.model.ChatIdentifier;
@ -19,6 +20,7 @@ import im.conversations.android.xmpp.model.markers.Displayed;
import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import im.conversations.android.xmpp.model.muc.user.MultiUserChat;
import im.conversations.android.xmpp.model.oob.OutOfBandData; import im.conversations.android.xmpp.model.oob.OutOfBandData;
import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.reply.Reply;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -113,6 +115,14 @@ public class Transformer {
return false; return false;
} }
database.messageDao().insertMessageContent(messageIdentifier.version, contents); database.messageDao().insertMessageContent(messageIdentifier.version, contents);
final var reply = transformation.getExtension(Reply.class);
if (Objects.nonNull(reply)
&& Objects.nonNull(reply.getId())
&& InvalidJid.isValid(reply.getTo())) {
database.messageDao()
.setInReplyTo(
chat, messageIdentifier, messageType, reply.getTo(), reply.getId());
}
return true; return true;
} }
return true; return true;

View file

@ -0,0 +1,30 @@
package im.conversations.android.xmpp.model.reply;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement(namespace = Namespace.REPLY)
public class Reply extends Extension {
public Reply() {
super(Reply.class);
}
public Jid getTo() {
return this.getAttributeAsJid("to");
}
public String getId() {
return this.getAttribute("id");
}
public void setTo(final Jid to) {
this.setAttribute("to", to);
}
public void setId(final String id) {
this.setAttribute("id", id);
}
}