store reactions in database

This commit is contained in:
Daniel Gultsch 2023-02-11 17:28:32 +01:00
parent a69b4b14a5
commit 6c24cb12dd
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
11 changed files with 145 additions and 16 deletions

View file

@ -70,6 +70,7 @@ 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";

View file

@ -38,11 +38,11 @@ import im.conversations.android.database.entity.DiscoIdentityEntity;
import im.conversations.android.database.entity.DiscoItemEntity; import im.conversations.android.database.entity.DiscoItemEntity;
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.MessageEntity;
import im.conversations.android.database.entity.MessageReactionEntity;
import im.conversations.android.database.entity.MessageStateEntity; import im.conversations.android.database.entity.MessageStateEntity;
import im.conversations.android.database.entity.MessageVersionEntity; import im.conversations.android.database.entity.MessageVersionEntity;
import im.conversations.android.database.entity.NickEntity; import im.conversations.android.database.entity.NickEntity;
import im.conversations.android.database.entity.PresenceEntity; import im.conversations.android.database.entity.PresenceEntity;
import im.conversations.android.database.entity.ReactionEntity;
import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.entity.RosterItemEntity;
import im.conversations.android.database.entity.RosterItemGroupEntity; import im.conversations.android.database.entity.RosterItemGroupEntity;
@ -74,7 +74,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
MessageVersionEntity.class, MessageVersionEntity.class,
NickEntity.class, NickEntity.class,
PresenceEntity.class, PresenceEntity.class,
ReactionEntity.class, MessageReactionEntity.class,
RosterItemEntity.class, RosterItemEntity.class,
RosterItemGroupEntity.class RosterItemGroupEntity.class
}, },

View file

@ -6,10 +6,12 @@ import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
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.MessageEntity;
import im.conversations.android.database.entity.MessageReactionEntity;
import im.conversations.android.database.entity.MessageStateEntity; import im.conversations.android.database.entity.MessageStateEntity;
import im.conversations.android.database.entity.MessageVersionEntity; import im.conversations.android.database.entity.MessageVersionEntity;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
@ -19,6 +21,8 @@ import im.conversations.android.database.model.MessageIdentifier;
import im.conversations.android.database.model.MessageState; import im.conversations.android.database.model.MessageState;
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.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -105,12 +109,12 @@ public abstract class MessageDao {
// when found by stanzaId the stanzaId must either by verified or belonging to a stub // when found by stanzaId the stanzaId must either by verified or belonging to a stub
// when found by messageId the from must either match (for corrections) or not be set (null) and // when found by messageId the from must either match (for corrections) or not be set (null) and
// we only look up stubs // we only look up stubs
// TODO the from matcher should be in the outer condition
@Query( @Query(
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE" "SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare=NULL) AND ((stanzaId !=" + " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND ((stanzaId IS"
+ " NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion=NULL)) OR" + " NOT NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion IS"
+ " (stanzaId = NULL AND messageId=:messageId AND latestVersion = NULL))") + " NULL)) OR (stanzaId IS NULL AND messageId=:messageId AND latestVersion IS"
+ " NULL))")
abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId); abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId);
public MessageIdentifier getOrCreateVersion( public MessageIdentifier getOrCreateVersion(
@ -200,14 +204,39 @@ public abstract class MessageDao {
protected abstract void setLatestMessageId( protected abstract void setLatestMessageId(
final long messageEntityId, final long messageVersionId); final long messageEntityId, final long messageVersionId);
public Long getOrCreateStub(final Transformation transformation) { public MessageIdentifier getOrCreateStub(
// TODO look up where parentId matches messageId (or stanzaId for group chats) final ChatIdentifier chat, final Message.Type messageType, final String parentId) {
final MessageIdentifier existing;
// when creating stub either set from (correction) or dont (other attachment) if (messageType == Message.Type.GROUPCHAT) {
existing = getByStanzaId(chat.id, parentId);
return null; } else {
existing = getByMessageId(chat.id, parentId);
}
if (existing != null) {
return existing;
}
final MessageEntity messageEntity;
if (messageType == Message.Type.GROUPCHAT) {
LOGGER.info("Create stub for stanza id {}", parentId);
messageEntity = MessageEntity.stubOfStanzaId(chat.id, parentId);
} else {
LOGGER.info("Create stub for message id {}", parentId);
messageEntity = MessageEntity.stubOfMessageId(chat.id, parentId);
}
final long messageEntityId = insert(messageEntity);
return new MessageIdentifier(messageEntityId, null, null, null, null);
} }
@Query(
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
+ " chatId=:chatId AND messageId=:messageId")
protected abstract MessageIdentifier getByMessageId(final long chatId, final String messageId);
@Query(
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
+ " chatId=:chatId AND stanzaId=:stanzaId")
protected abstract MessageIdentifier getByStanzaId(final long chatId, final String stanzaId);
public void insertMessageContent(Long latestVersion, List<MessageContent> contents) { public void insertMessageContent(Long latestVersion, List<MessageContent> contents) {
Preconditions.checkNotNull( Preconditions.checkNotNull(
latestVersion, "Contents can only be inserted for a specific version"); latestVersion, "Contents can only be inserted for a specific version");
@ -245,4 +274,19 @@ public abstract class MessageDao {
@Insert @Insert
protected abstract void insert(MessageStateEntity messageStateEntity); protected abstract void insert(MessageStateEntity messageStateEntity);
@Insert
protected abstract void insertReactions(Collection<MessageReactionEntity> reactionEntities);
public void insertReactions(
ChatIdentifier chat, Reactions reactions, Transformation transformation) {
final Message.Type messageType = transformation.type;
final MessageIdentifier messageIdentifier =
getOrCreateStub(chat, messageType, reactions.getId());
// TODO delete old reactions
insertReactions(
Collections2.transform(
reactions.getReactions(),
r -> MessageReactionEntity.of(messageIdentifier.id, r, transformation)));
}
} }

View file

@ -50,6 +50,7 @@ public abstract class RosterDao {
} }
final RosterItemEntity entity = RosterItemEntity.of(account.id, item); final RosterItemEntity entity = RosterItemEntity.of(account.id, item);
final long id = insert(entity); final long id = insert(entity);
// TODO groups
} }
setRosterVersion(account.id, version); setRosterVersion(account.id, version);
} }

View file

@ -81,4 +81,18 @@ public class MessageEntity {
entity.stanzaIdVerified = false; entity.stanzaIdVerified = false;
return entity; return entity;
} }
public static MessageEntity stubOfStanzaId(final long chatId, String stanzaId) {
final var entity = new MessageEntity();
entity.stanzaIdVerified = false;
entity.stanzaId = stanzaId;
return entity;
}
public static MessageEntity stubOfMessageId(final long chatId, String messageId) {
final var entity = new MessageEntity();
entity.stanzaIdVerified = false;
entity.messageId = messageId;
return entity;
}
} }

View file

@ -5,6 +5,8 @@ import androidx.room.Entity;
import androidx.room.ForeignKey; import androidx.room.ForeignKey;
import androidx.room.Index; import androidx.room.Index;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.transformer.Transformation;
import java.time.Instant; import java.time.Instant;
@Entity( @Entity(
@ -16,7 +18,7 @@ import java.time.Instant;
childColumns = {"messageEntityId"}, childColumns = {"messageEntityId"},
onDelete = ForeignKey.CASCADE), onDelete = ForeignKey.CASCADE),
indices = {@Index(value = "messageEntityId")}) indices = {@Index(value = "messageEntityId")})
public class ReactionEntity { public class MessageReactionEntity {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public Long id; public Long id;
@ -25,11 +27,25 @@ public class ReactionEntity {
public String stanzaId; public String stanzaId;
public String messageId; public String messageId;
public String reactionBy; public Jid reactionBy;
public String reactionByResource; public String reactionByResource;
public String occupantId; public String occupantId;
public Instant receivedAt; public Instant receivedAt;
public String reaction; public String reaction;
public static MessageReactionEntity of(
long messageEntityId, final String reaction, final Transformation transformation) {
final var entity = new MessageReactionEntity();
entity.messageEntityId = messageEntityId;
entity.reaction = reaction;
entity.stanzaId = transformation.stanzaId;
entity.messageId = transformation.messageId;
entity.reactionBy = transformation.fromBare();
entity.reactionByResource = transformation.fromResource();
entity.occupantId = transformation.occupantId;
entity.receivedAt = transformation.receivedAt;
return entity;
}
} }

View file

@ -16,6 +16,7 @@ import im.conversations.android.xmpp.model.jabber.Thread;
import im.conversations.android.xmpp.model.markers.Displayed; 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.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;
@ -34,7 +35,8 @@ public class Transformation {
DeliveryReceipt.class, DeliveryReceipt.class,
MultiUserChat.class, MultiUserChat.class,
Displayed.class, Displayed.class,
Replace.class); Replace.class,
Reactions.class);
public final Instant receivedAt; public final Instant receivedAt;
public final Jid to; public final Jid to;

View file

@ -18,6 +18,7 @@ import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.markers.Displayed; 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.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;
@ -70,11 +71,16 @@ public class Transformer {
return false; return false;
} }
final Replace messageCorrection = transformation.getExtension(Replace.class); final Replace messageCorrection = transformation.getExtension(Replace.class);
final Reactions reactions = transformation.getExtension(Reactions.class);
final List<MessageContent> contents = parseContent(transformation); final List<MessageContent> contents = parseContent(transformation);
final boolean identifiableSender = final boolean identifiableSender =
Arrays.asList(Message.Type.NORMAL, Message.Type.CHAT).contains(messageType) Arrays.asList(Message.Type.NORMAL, Message.Type.CHAT).contains(messageType)
|| Objects.nonNull(transformation.occupantId); || Objects.nonNull(transformation.occupantId);
final boolean isReaction =
Objects.nonNull(reactions)
&& Objects.nonNull(reactions.getId())
&& identifiableSender;
final boolean isMessageCorrection = final boolean isMessageCorrection =
Objects.nonNull(messageCorrection) Objects.nonNull(messageCorrection)
&& messageCorrection.getId() != null && messageCorrection.getId() != null
@ -83,7 +89,9 @@ public class Transformer {
if (contents.isEmpty()) { if (contents.isEmpty()) {
LOGGER.info("Received message from {} w/o contents", transformation.from); LOGGER.info("Received message from {} w/o contents", transformation.from);
transformMessageState(chat, transformation); transformMessageState(chat, transformation);
// TODO apply reactions if (isReaction) {
database.messageDao().insertReactions(chat, reactions, transformation);
}
} else { } else {
final MessageIdentifier messageIdentifier; final MessageIdentifier messageIdentifier;
try { try {

View file

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

View file

@ -0,0 +1,26 @@
package im.conversations.android.xmpp.model.reactions;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
import java.util.Objects;
@XmlElement
public class Reactions extends Extension {
public Reactions() {
super(Reactions.class);
}
public Collection<String> getReactions() {
return Collections2.filter(
Collections2.transform(getExtensions(Reaction.class), Reaction::getContent),
r -> Objects.nonNull(Strings.nullToEmpty(r)));
}
public String getId() {
return this.getAttribute("id");
}
}

View file

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