apply message corrections
This commit is contained in:
parent
be3a8dc5e1
commit
a69b4b14a5
|
@ -70,6 +70,7 @@ public final class Namespace {
|
|||
public static final String MUC = "http://jabber.org/protocol/muc";
|
||||
public static final String MUC_USER = MUC + "#user";
|
||||
public static final String NICK = "http://jabber.org/protocol/nick";
|
||||
public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0";
|
||||
public static final String OMEMO_DTLS_SRTP_VERIFICATION =
|
||||
"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
|
||||
public static final String OOB = "jabber:x:oob";
|
||||
|
|
|
@ -57,6 +57,10 @@ public abstract class MessageDao {
|
|||
}
|
||||
}
|
||||
|
||||
// this method returns a MessageIdentifier (message + version) used to create ORIGINAL messages
|
||||
// it might return something that was previously a stub (message that only has reactions or
|
||||
// corrections but no original content). but in the process of invoking this method the stub
|
||||
// will be upgraded to an original message (missing information filled in)
|
||||
@Transaction
|
||||
public MessageIdentifier getOrCreateMessage(
|
||||
ChatIdentifier chatIdentifier, final Transformation transformation) {
|
||||
|
@ -96,6 +100,96 @@ public abstract class MessageDao {
|
|||
messageVersionId);
|
||||
}
|
||||
|
||||
// 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
|
||||
// when found by messageId the from must either match (for corrections) or not be set (null) and
|
||||
// we only look up stubs
|
||||
// TODO the from matcher should be in the outer condition
|
||||
@Query(
|
||||
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
|
||||
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare=NULL) AND ((stanzaId !="
|
||||
+ " NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion=NULL)) OR"
|
||||
+ " (stanzaId = NULL AND messageId=:messageId AND latestVersion = NULL))")
|
||||
abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId);
|
||||
|
||||
public MessageIdentifier getOrCreateVersion(
|
||||
ChatIdentifier chat,
|
||||
Transformation transformation,
|
||||
final String messageId,
|
||||
final Modification modification) {
|
||||
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 {
|
||||
messageIdentifier =
|
||||
getByOccupantIdAndMessageId(
|
||||
chat.id,
|
||||
transformation.fromBare(),
|
||||
transformation.occupantId,
|
||||
messageId);
|
||||
}
|
||||
if (messageIdentifier == null) {
|
||||
LOGGER.info(
|
||||
"Create stub for {} because we could not find anything with id {} from {}",
|
||||
modification,
|
||||
messageId,
|
||||
transformation.fromBare());
|
||||
final var messageEntity = MessageEntity.stub(chat.id, messageId, transformation);
|
||||
final long messageEntityId = insert(messageEntity);
|
||||
final long messageVersionId =
|
||||
insert(MessageVersionEntity.of(messageEntityId, modification, transformation));
|
||||
// we do not point latestVersion to this newly created versions. We've only created a
|
||||
// stub and are waiting for the original message to arrive
|
||||
return new MessageIdentifier(
|
||||
messageEntityId, null, null, transformation.fromBare(), messageVersionId);
|
||||
}
|
||||
if (hasVersionWithMessageId(messageIdentifier.id, transformation.messageId)) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"A modification with messageId %s has already been applied",
|
||||
messageId));
|
||||
}
|
||||
final long messageVersionId =
|
||||
insert(MessageVersionEntity.of(messageIdentifier.id, modification, transformation));
|
||||
if (messageIdentifier.version != null) {
|
||||
// if the existing message was not a stub we retarget the version
|
||||
final long latestVersion = getLatestVersion(messageIdentifier.id);
|
||||
setLatestMessageId(messageIdentifier.id, latestVersion);
|
||||
}
|
||||
return new MessageIdentifier(
|
||||
messageIdentifier.id,
|
||||
messageIdentifier.stanzaId,
|
||||
messageIdentifier.messageId,
|
||||
messageIdentifier.fromBare,
|
||||
messageVersionId);
|
||||
}
|
||||
|
||||
@Query(
|
||||
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
|
||||
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND"
|
||||
+ " (occupantId=:occupantId OR occupantId IS NULL) AND messageId=:messageId")
|
||||
abstract MessageIdentifier getByOccupantIdAndMessageId(
|
||||
long chatId, Jid fromBare, String occupantId, String messageId);
|
||||
|
||||
@Query(
|
||||
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
|
||||
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND"
|
||||
+ " messageId=:messageId")
|
||||
abstract MessageIdentifier getByMessageId(long chatId, Jid fromBare, String messageId);
|
||||
|
||||
@Query(
|
||||
"SELECT id FROM message_version WHERE messageEntityId=:messageEntityId ORDER BY (CASE"
|
||||
+ " modification WHEN 'ORIGINAL' THEN 0 ELSE 1 END),receivedAt DESC LIMIT 1")
|
||||
abstract Long getLatestVersion(long messageEntityId);
|
||||
|
||||
@Query(
|
||||
"SELECT EXISTS (SELECT id FROM message_version WHERE messageEntityId=:messageEntityId"
|
||||
+ " AND messageId=:messageId)")
|
||||
abstract boolean hasVersionWithMessageId(long messageEntityId, String messageId);
|
||||
|
||||
@Insert
|
||||
protected abstract long insert(MessageEntity messageEntity);
|
||||
|
||||
|
@ -114,19 +208,6 @@ public abstract class MessageDao {
|
|||
return null;
|
||||
}
|
||||
|
||||
// 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
|
||||
// when found by messageId the from must either match (for corrections) or not be set (null) and
|
||||
// we only look up stubs
|
||||
// TODO the from matcher should be in the outer condition
|
||||
@Query(
|
||||
"SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE"
|
||||
+ " chatId=:chatId AND (fromBare=:fromBare OR fromBare=NULL) AND ((stanzaId !="
|
||||
+ " NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion=NULL)) OR"
|
||||
+ " (stanzaId = NULL AND messageId=:messageId AND latestVersion = NULL))")
|
||||
abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId);
|
||||
|
||||
public void insertMessageContent(Long latestVersion, List<MessageContent> contents) {
|
||||
Preconditions.checkNotNull(
|
||||
latestVersion, "Contents can only be inserted for a specific version");
|
||||
|
|
|
@ -65,9 +65,20 @@ public class MessageEntity {
|
|||
entity.toResource = transformation.toResource();
|
||||
entity.fromBare = transformation.fromBare();
|
||||
entity.fromResource = transformation.fromResource();
|
||||
entity.occupantId = transformation.occupantId;
|
||||
entity.messageId = transformation.messageId;
|
||||
entity.stanzaId = transformation.stanzaId;
|
||||
entity.stanzaIdVerified = Objects.nonNull(transformation.stanzaId);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public static MessageEntity stub(
|
||||
final long chatId, String messageId, Transformation transformation) {
|
||||
final var entity = new MessageEntity();
|
||||
entity.chatId = chatId;
|
||||
entity.fromBare = transformation.fromBare();
|
||||
entity.messageId = messageId;
|
||||
entity.stanzaIdVerified = false;
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import im.conversations.android.xmpp.model.DeliveryReceipt;
|
|||
import im.conversations.android.xmpp.model.DeliveryReceiptRequest;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import im.conversations.android.xmpp.model.axolotl.Encrypted;
|
||||
import im.conversations.android.xmpp.model.correction.Replace;
|
||||
import im.conversations.android.xmpp.model.error.Error;
|
||||
import im.conversations.android.xmpp.model.jabber.Body;
|
||||
import im.conversations.android.xmpp.model.jabber.Thread;
|
||||
import im.conversations.android.xmpp.model.markers.Displayed;
|
||||
|
@ -31,7 +33,8 @@ public class Transformation {
|
|||
OutOfBandData.class,
|
||||
DeliveryReceipt.class,
|
||||
MultiUserChat.class,
|
||||
Displayed.class);
|
||||
Displayed.class,
|
||||
Replace.class);
|
||||
|
||||
public final Instant receivedAt;
|
||||
public final Jid to;
|
||||
|
@ -41,6 +44,8 @@ public class Transformation {
|
|||
public final String messageId;
|
||||
public final String stanzaId;
|
||||
|
||||
public final String occupantId;
|
||||
|
||||
private final List<Extension> extensions;
|
||||
|
||||
public final Collection<DeliveryReceiptRequest> deliveryReceiptRequests;
|
||||
|
@ -53,6 +58,7 @@ public class Transformation {
|
|||
final Message.Type type,
|
||||
final String messageId,
|
||||
final String stanzaId,
|
||||
final String occupantId,
|
||||
final List<Extension> extensions,
|
||||
final Collection<DeliveryReceiptRequest> deliveryReceiptRequests) {
|
||||
this.receivedAt = receivedAt;
|
||||
|
@ -62,6 +68,7 @@ public class Transformation {
|
|||
this.type = type;
|
||||
this.messageId = messageId;
|
||||
this.stanzaId = stanzaId;
|
||||
this.occupantId = occupantId;
|
||||
this.extensions = extensions;
|
||||
this.deliveryReceiptRequests = deliveryReceiptRequests;
|
||||
}
|
||||
|
@ -97,11 +104,21 @@ public class Transformation {
|
|||
}
|
||||
|
||||
public <E extends Extension> E getExtension(final Class<E> clazz) {
|
||||
checkArgument(clazz);
|
||||
final var extension = Iterables.find(this.extensions, clazz::isInstance, null);
|
||||
return extension == null ? null : clazz.cast(extension);
|
||||
}
|
||||
|
||||
private void checkArgument(final Class<? extends Extension> clazz) {
|
||||
if (EXTENSION_FOR_TRANSFORMATION.contains(clazz) || clazz == Error.class) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s has not been registered for transformation", clazz.getName()));
|
||||
}
|
||||
|
||||
public <E extends Extension> Collection<E> getExtensions(final Class<E> clazz) {
|
||||
checkArgument(clazz);
|
||||
return Collections2.transform(
|
||||
Collections2.filter(this.extensions, clazz::isInstance), clazz::cast);
|
||||
}
|
||||
|
@ -110,7 +127,8 @@ public class Transformation {
|
|||
@NonNull final Message message,
|
||||
@NonNull final Instant receivedAt,
|
||||
@NonNull final Jid remote,
|
||||
final String stanzaId) {
|
||||
final String stanzaId,
|
||||
final String occupantId) {
|
||||
final var to = message.getTo();
|
||||
final var from = message.getFrom();
|
||||
final var type = message.getType();
|
||||
|
@ -134,6 +152,7 @@ public class Transformation {
|
|||
type,
|
||||
messageId,
|
||||
stanzaId,
|
||||
occupantId,
|
||||
extensionListBuilder.build(),
|
||||
requests);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package im.conversations.android.transformer;
|
||||
|
||||
import android.content.Context;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.manager.DiscoManager;
|
||||
import im.conversations.android.xmpp.model.occupant.OccupantId;
|
||||
import im.conversations.android.xmpp.model.stanza.Message;
|
||||
import java.time.Instant;
|
||||
|
||||
|
@ -27,7 +30,18 @@ public class TransformationFactory extends XmppConnection.Delegate {
|
|||
} else {
|
||||
remote = from;
|
||||
}
|
||||
// TODO parse occupant on group chats
|
||||
return Transformation.of(message, receivedAt, remote, stanzaId);
|
||||
final String occupantId;
|
||||
if (message.getType() == Message.Type.GROUPCHAT && message.hasExtension(OccupantId.class)) {
|
||||
if (from != null
|
||||
&& getManager(DiscoManager.class)
|
||||
.hasFeature(from.asBareJid(), Namespace.OCCUPANT_ID)) {
|
||||
occupantId = message.getExtension(OccupantId.class).getId();
|
||||
} else {
|
||||
occupantId = null;
|
||||
}
|
||||
} else {
|
||||
occupantId = null;
|
||||
}
|
||||
return Transformation.of(message, receivedAt, remote, stanzaId, occupantId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import im.conversations.android.database.ConversationsDatabase;
|
|||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.ChatIdentifier;
|
||||
import im.conversations.android.database.model.MessageContent;
|
||||
import im.conversations.android.database.model.MessageIdentifier;
|
||||
import im.conversations.android.database.model.MessageState;
|
||||
import im.conversations.android.database.model.Modification;
|
||||
import im.conversations.android.xmpp.model.DeliveryReceipt;
|
||||
import im.conversations.android.xmpp.model.axolotl.Encrypted;
|
||||
import im.conversations.android.xmpp.model.correction.Replace;
|
||||
|
@ -17,6 +19,7 @@ import im.conversations.android.xmpp.model.markers.Displayed;
|
|||
import im.conversations.android.xmpp.model.muc.user.MultiUserChat;
|
||||
import im.conversations.android.xmpp.model.oob.OutOfBandData;
|
||||
import im.conversations.android.xmpp.model.stanza.Message;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -66,31 +69,43 @@ public class Transformer {
|
|||
chat, transformation.messageId, MessageState.error(transformation));
|
||||
return false;
|
||||
}
|
||||
final Replace lastMessageCorrection = transformation.getExtension(Replace.class);
|
||||
final Replace messageCorrection = transformation.getExtension(Replace.class);
|
||||
final List<MessageContent> contents = parseContent(transformation);
|
||||
|
||||
// TODO this also needs to be true for retractions once we support those (anything that
|
||||
// creates a new message version
|
||||
// TODO a type=groupchat message correction is only valid with an occupant id
|
||||
final boolean versionModification = Objects.nonNull(lastMessageCorrection);
|
||||
final boolean identifiableSender =
|
||||
Arrays.asList(Message.Type.NORMAL, Message.Type.CHAT).contains(messageType)
|
||||
|| Objects.nonNull(transformation.occupantId);
|
||||
final boolean isMessageCorrection =
|
||||
Objects.nonNull(messageCorrection)
|
||||
&& messageCorrection.getId() != null
|
||||
&& identifiableSender;
|
||||
|
||||
if (contents.isEmpty()) {
|
||||
LOGGER.info("Received message from {} w/o contents", transformation.from);
|
||||
transformMessageState(chat, transformation);
|
||||
// TODO apply reactions
|
||||
} else {
|
||||
if (versionModification) {
|
||||
// TODO use getOrStub
|
||||
// TODO check if versionModification has already been applied
|
||||
final MessageIdentifier messageIdentifier;
|
||||
try {
|
||||
if (isMessageCorrection) {
|
||||
messageIdentifier =
|
||||
database.messageDao()
|
||||
.getOrCreateVersion(
|
||||
chat,
|
||||
transformation,
|
||||
messageCorrection.getId(),
|
||||
Modification.EDIT);
|
||||
|
||||
// TODO for replaced message create a new version; re-target latestVersion
|
||||
|
||||
} else {
|
||||
final var messageIdentifier =
|
||||
database.messageDao().getOrCreateMessage(chat, transformation);
|
||||
database.messageDao().insertMessageContent(messageIdentifier.version, contents);
|
||||
return true;
|
||||
} else {
|
||||
messageIdentifier =
|
||||
database.messageDao().getOrCreateMessage(chat, transformation);
|
||||
}
|
||||
} catch (final IllegalStateException e) {
|
||||
LOGGER.warn("Could not get message identifier", e);
|
||||
return false;
|
||||
}
|
||||
database.messageDao().insertMessageContent(messageIdentifier.version, contents);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
package im.conversations.android.xmpp.model.correction;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement(namespace = Namespace.LAST_MESSAGE_CORRECTION)
|
||||
public class Replace extends Extension {
|
||||
|
||||
public Replace() {
|
||||
super(Replace.class);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return Strings.emptyToNull(this.getAttribute("id"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package im.conversations.android.xmpp.model.occupant;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement(namespace = Namespace.OCCUPANT_ID)
|
||||
public class OccupantId extends Extension {
|
||||
|
||||
public OccupantId() {
|
||||
super(OccupantId.class);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return Strings.emptyToNull(this.getAttribute("id"));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue