in group chats corrections and reactions use different ids. we need to merge stubs

This commit is contained in:
Daniel Gultsch 2023-02-13 10:45:13 +01:00
parent 2728a96ab9
commit 56a462833e
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
4 changed files with 274 additions and 15 deletions

View file

@ -153,7 +153,7 @@ public class TransformationTest {
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(Modification.EDIT, message.modification);
Assert.assertEquals(Modification.CORRECTION, message.modification);
Assert.assertEquals("Hi example!", onlyContent.body);
}
@ -187,7 +187,188 @@ public class TransformationTest {
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(Modification.EDIT, message.modification);
Assert.assertEquals(Modification.CORRECTION, message.modification);
Assert.assertEquals("Hi example!", onlyContent.body);
}
@Test
public void replacingReactions() {
final var group = Jid.ofEscaped("a@group.example.com");
final var message = new Message(Message.Type.GROUPCHAT);
message.addExtension(new Body("Please give me a thumbs up"));
message.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(message, Instant.now(), REMOTE, "stanza-a", "id-user-a"));
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(group.withResource("user-b"));
reactionA.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("N"));
this.transformer.transform(
Transformation.of(reactionA, Instant.now(), REMOTE, "stanza-b", "id-user-b"));
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(group.withResource("user-b"));
reactionB.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(reactionB, Instant.now(), REMOTE, "stanza-c", "id-user-b"));
final var messages = database.messageDao().getMessages(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(1, dbMessage.reactions.size());
}
@Test
public void twoCorrectionsOneReactionBeforeOriginalInGroupChat() {
final var group = Jid.ofEscaped("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// first correction
final var m1 = new Message(Message.Type.GROUPCHAT);
// m1.setId(ogMessageId);
m1.addExtension(new Body("Please give me an thumbs up"));
m1.addExtension(new Replace()).setId(ogMessageId);
m1.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(
m1,
Instant.ofEpochMilli(2000),
REMOTE,
"irrelevant-stanza-id1",
"id-user-a"));
// second correction
final var m2 = new Message(Message.Type.GROUPCHAT);
// m2.setId(ogMessageId);
m2.addExtension(new Body("Please give me a thumbs up"));
m2.addExtension(new Replace()).setId(ogMessageId);
m2.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(
m2,
Instant.ofEpochMilli(3000),
REMOTE,
"irrelevant-stanza-id2",
"id-user-a"));
// a reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(group.withResource("user-b"));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(
reactionB, Instant.now(), REMOTE, "irrelevant-stanza-id3", "id-user-b"));
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me thumbs up"));
m4.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, "id-user-a"));
final var messages = database.messageDao().getMessages(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(1, dbMessage.reactions.size());
Assert.assertEquals(Modification.CORRECTION, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
@Test
public void twoReactionsOneCorrectionBeforeOriginalInGroupChat() {
final var group = Jid.ofEscaped("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// first reaction
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(group.withResource("user-b"));
reactionA.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(
reactionA, Instant.now(), REMOTE, "irrelevant-stanza-id1", "id-user-b"));
// second reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(group.withResource("user-c"));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(
reactionB, Instant.now(), REMOTE, "irrelevant-stanza-id2", "id-user-c"));
// a correction
final var m1 = new Message(Message.Type.GROUPCHAT);
m1.addExtension(new Body("Please give me a thumbs up"));
m1.addExtension(new Replace()).setId(ogMessageId);
m1.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(
m1,
Instant.ofEpochMilli(2000),
REMOTE,
"irrelevant-stanza-id3",
"id-user-a"));
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me thumbs up (Typo)"));
m4.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, "id-user-a"));
final var messages = database.messageDao().getMessages(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(2, dbMessage.reactions.size());
final var onlyReaction = Iterables.getOnlyElement(dbMessage.getAggregatedReactions());
Assert.assertEquals(2L, (long) onlyReaction.getValue());
Assert.assertEquals(Modification.CORRECTION, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
@Test
public void twoReactionsInGroupChat() {
final var group = Jid.ofEscaped("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me a thumbs up"));
m4.setFrom(group.withResource("user-a"));
this.transformer.transform(
Transformation.of(m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, "id-user-a"));
// first reaction
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(group.withResource("user-b"));
reactionA.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(
reactionA, Instant.now(), REMOTE, "irrelevant-stanza-id1", "id-user-b"));
// second reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(group.withResource("user-c"));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
Transformation.of(
reactionB, Instant.now(), REMOTE, "irrelevant-stanza-id2", "id-user-c"));
final var messages = database.messageDao().getMessages(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(2, dbMessage.reactions.size());
final var onlyReaction = Iterables.getOnlyElement(dbMessage.getAggregatedReactions());
Assert.assertEquals(2L, (long) onlyReaction.getValue());
Assert.assertEquals(Modification.ORIGINAL, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
}

View file

@ -74,10 +74,14 @@ public abstract class MessageDao {
get(
chatIdentifier.id,
transformation.fromBare(),
transformation.occupantId,
transformation.stanzaId,
transformation.messageId);
if (messageIdentifier != null) {
if (messageIdentifier.isStub()) {
if (transformation.type == Message.Type.GROUPCHAT) {
mergeMessageStubs(chatIdentifier, messageIdentifier, transformation);
}
LOGGER.info(
"Found stub for stanzaId '{}' and messageId '{}'",
transformation.stanzaId,
@ -93,9 +97,11 @@ public abstract class MessageDao {
updatedEntity.id = messageIdentifier.id;
updatedEntity.latestVersion = getLatestVersion(messageIdentifier.id);
LOGGER.info(
"Created original version {} and updated latest version to {}",
"Created original version {} and updated latest version to {} for"
+ " messageEntityId {}",
originalVersionId,
updatedEntity.latestVersion);
updatedEntity.latestVersion,
messageIdentifier.id);
update(updatedEntity);
return new MessageIdentifier(
updatedEntity.id,
@ -128,6 +134,52 @@ public abstract class MessageDao {
messageVersionId);
}
private void mergeMessageStubs(
ChatIdentifier chatIdentifier,
MessageIdentifier messageIdentifier,
final Transformation transformation) {
final Long stub;
if (messageIdentifier.messageId == null && transformation.messageId != null) {
stub = getMessageStubByMessageId(chatIdentifier.id, transformation.messageId);
} else if (messageIdentifier.stanzaId == null && transformation.stanzaId != null) {
stub = getMessageStubByStanzaId(chatIdentifier.id, transformation.stanzaId);
} else {
return;
}
if (stub == null) {
return;
}
LOGGER.info("Updating message.id in dangling stub {} => {}", stub, messageIdentifier.id);
updateMessageEntityIdInReactions(stub, messageIdentifier.id);
updateMessageEntityIdInVersions(stub, messageIdentifier.id);
deleteMessageEntity(stub);
}
@Query(
"UPDATE message_reaction SET messageEntityId=:newMessageEntityId WHERE"
+ " messageEntityId=:oldMessageEntityId")
protected abstract void updateMessageEntityIdInReactions(
long oldMessageEntityId, Long newMessageEntityId);
@Query(
"UPDATE message_version SET messageEntityId=:newMessageEntityId WHERE"
+ " messageEntityId=:oldMessageEntityId")
protected abstract void updateMessageEntityIdInVersions(
long oldMessageEntityId, Long newMessageEntityId);
@Query("DELETE FROM message WHERE id=:messageEntityId")
protected abstract void deleteMessageEntity(final long messageEntityId);
@Query(
"SELECT id FROM message WHERE chatId=:chatId AND messageId=:messageId AND stanzaId IS"
+ " NULL AND latestVersion IS NULL")
protected abstract Long getMessageStubByMessageId(long chatId, String messageId);
@Query(
"SELECT id FROM message WHERE chatId=:chatId AND stanzaId=:stanzaId AND messageId IS"
+ " NULL AND latestVersion IS NULL")
protected abstract Long getMessageStubByStanzaId(long chatId, String stanzaId);
// this gets either a message or a stub.
// stubs are recognized by latestVersion=NULL
// when found by stanzaId the stanzaId must either by verified or belonging to a stub
@ -135,11 +187,12 @@ public abstract class MessageDao {
// we only look up stubs
@Query(
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND ((stanzaId IS"
+ " NOT NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion IS"
+ " NULL)) OR (stanzaId IS NULL AND messageId=:messageId AND latestVersion IS"
+ " NULL))")
abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId);
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND"
+ " (occupantId=:occupantId OR occupantId IS NULL) AND ((stanzaId IS NOT NULL AND"
+ " stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion IS NULL)) OR"
+ " (stanzaId IS NULL AND messageId=:messageId AND latestVersion IS NULL))")
abstract MessageIdentifier get(
long chatId, Jid fromBare, String occupantId, String stanzaId, String messageId);
public MessageIdentifier getOrCreateVersion(
ChatIdentifier chat,
@ -149,15 +202,19 @@ public abstract class MessageDao {
Preconditions.checkArgument(
messageId != null, "A modification must reference a message id");
final MessageIdentifier messageIdentifier;
if (transformation.occupantId == null) {
messageIdentifier = getByMessageId(chat.id, transformation.fromBare(), messageId);
} else {
// TODO use type for condition and then null check occupantID
if (transformation.type == Message.Type.GROUPCHAT) {
Preconditions.checkNotNull(
transformation.occupantId,
"To create a version of a group chat message occupant id must be set");
messageIdentifier =
getByOccupantIdAndMessageId(
chat.id,
transformation.fromBare(),
transformation.occupantId,
messageId);
} else {
messageIdentifier = getByMessageId(chat.id, transformation.fromBare(), messageId);
}
if (messageIdentifier == null) {
LOGGER.info(
@ -310,13 +367,34 @@ public abstract class MessageDao {
final Message.Type messageType = transformation.type;
final MessageIdentifier messageIdentifier =
getOrCreateStub(chat, messageType, reactions.getId());
// TODO delete old reactions
if (messageType == Message.Type.GROUPCHAT) {
Preconditions.checkNotNull(
transformation.occupantId,
"OccupantId must not be null when processing reactions in group chats");
deleteReactionsByOccupantId(messageIdentifier.id, transformation.occupantId);
} else {
deleteReactionsByFromBare(messageIdentifier.id, transformation.fromBare());
}
LOGGER.info(
"Inserting reaction from {} to messageEntityId {}",
transformation.from,
messageIdentifier.id);
insertReactions(
Collections2.transform(
reactions.getReactions(),
r -> MessageReactionEntity.of(messageIdentifier.id, r, transformation)));
}
@Query(
"DELETE FROM message_reaction WHERE messageEntityId=:messageEntityId AND"
+ " occupantId=:occupantId")
protected abstract void deleteReactionsByOccupantId(long messageEntityId, String occupantId);
@Query(
"DELETE FROM message_reaction WHERE messageEntityId=:messageEntityId AND"
+ " reactionBy=:fromBare")
protected abstract void deleteReactionsByFromBare(long messageEntityId, Jid fromBare);
@Transaction
@Query(
"SELECT message.id as"

View file

@ -2,7 +2,7 @@ package im.conversations.android.database.model;
public enum Modification {
ORIGINAL,
EDIT, // XEP-0308: Last Message Correction
CORRECTION, // XEP-0308: Last Message Correction
RETRACTION, // XEP-0424: Message Retraction
MODERATION // XEP-0425: Message Moderation
}

View file

@ -102,7 +102,7 @@ public class Transformer {
chat,
transformation,
messageCorrection.getId(),
Modification.EDIT);
Modification.CORRECTION);
} else {
messageIdentifier =