add 'encryption' and 'identityKey' to message version entity
This commit is contained in:
parent
677cfcd34c
commit
cf5910e96e
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "6186e2691813f4fbd804b90fd770e18b",
|
||||
"identityHash": "a619bdeae0408fc2250a0bf2b9ab1f4e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account",
|
||||
|
@ -1834,7 +1834,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "message_version",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageEntityId` INTEGER NOT NULL, `messageId` TEXT, `stanzaId` TEXT, `modification` TEXT, `modifiedBy` TEXT, `modifiedByResource` TEXT, `occupantId` TEXT, `receivedAt` INTEGER, FOREIGN KEY(`messageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageEntityId` INTEGER NOT NULL, `messageId` TEXT, `stanzaId` TEXT, `modification` TEXT, `modifiedBy` TEXT, `modifiedByResource` TEXT, `occupantId` TEXT, `receivedAt` INTEGER, `encryption` TEXT, `identityKey` BLOB, FOREIGN KEY(`messageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -1889,6 +1889,18 @@
|
|||
"columnName": "receivedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "encryption",
|
||||
"columnName": "encryption",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "identityKey",
|
||||
"columnName": "identityKey",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
|
@ -2352,7 +2364,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, '6186e2691813f4fbd804b90fd770e18b')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a619bdeae0408fc2250a0bf2b9ab1f4e')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import com.google.common.collect.Iterables;
|
|||
import im.conversations.android.IDs;
|
||||
import im.conversations.android.database.ConversationsDatabase;
|
||||
import im.conversations.android.database.entity.AccountEntity;
|
||||
import im.conversations.android.database.model.Encryption;
|
||||
import im.conversations.android.database.model.MessageEmbedded;
|
||||
import im.conversations.android.database.model.Modification;
|
||||
import im.conversations.android.database.model.PartType;
|
||||
|
@ -83,6 +84,7 @@ public class MessageTransformationTest {
|
|||
final var message = Iterables.getOnlyElement(messages);
|
||||
final var onlyContent = Iterables.getOnlyElement(message.contents);
|
||||
Assert.assertEquals(GREETING, onlyContent.body);
|
||||
Assert.assertEquals(Encryption.CLEARTEXT,message.encryption);
|
||||
final var onlyReaction = Iterables.getOnlyElement(message.reactions);
|
||||
Assert.assertEquals("Y", onlyReaction.reaction);
|
||||
Assert.assertEquals(REMOTE, onlyReaction.reactionBy);
|
||||
|
|
|
@ -16,11 +16,12 @@ import im.conversations.android.database.entity.MessageStateEntity;
|
|||
import im.conversations.android.database.entity.MessageVersionEntity;
|
||||
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.Encryption;
|
||||
import im.conversations.android.database.model.MessageIdentifier;
|
||||
import im.conversations.android.database.model.MessageState;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
import im.conversations.android.database.model.Modification;
|
||||
import im.conversations.android.transformer.MessageContentWrapper;
|
||||
import im.conversations.android.transformer.MessageTransformation;
|
||||
import im.conversations.android.xmpp.model.reactions.Reactions;
|
||||
import im.conversations.android.xmpp.model.stanza.Message;
|
||||
|
@ -31,6 +32,7 @@ import org.jxmpp.jid.Jid;
|
|||
import org.jxmpp.jid.parts.Resourcepart;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
@Dao
|
||||
public abstract class MessageDao {
|
||||
|
@ -324,19 +326,37 @@ public abstract class MessageDao {
|
|||
+ " 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(
|
||||
final Long latestVersion, final MessageContentWrapper messageContentWrapper) {
|
||||
Preconditions.checkNotNull(
|
||||
latestVersion, "Contents can only be inserted for a specific version");
|
||||
Preconditions.checkArgument(
|
||||
contents.size() > 0,
|
||||
messageContentWrapper.contents.size() > 0,
|
||||
"If you are trying to insert empty contents something went wrong");
|
||||
insertMessageContent(
|
||||
Lists.transform(contents, c -> MessageContentEntity.of(latestVersion, c)));
|
||||
Lists.transform(
|
||||
messageContentWrapper.contents,
|
||||
c -> MessageContentEntity.of(latestVersion, c)));
|
||||
final int rows =
|
||||
updateMessageVersionEncryption(
|
||||
latestVersion,
|
||||
messageContentWrapper.encryption,
|
||||
messageContentWrapper.identityKey);
|
||||
if (rows != 1) {
|
||||
throw new IllegalStateException(
|
||||
"We expected to update encryption information on exactly 1 row");
|
||||
}
|
||||
}
|
||||
|
||||
@Insert
|
||||
protected abstract void insertMessageContent(Collection<MessageContentEntity> contentEntities);
|
||||
|
||||
@Query(
|
||||
"UPDATE message_version SET encryption=:encryption,identityKey=:identityKey WHERE"
|
||||
+ " id=:messageVersionId")
|
||||
protected abstract int updateMessageVersionEncryption(
|
||||
long messageVersionId, Encryption encryption, IdentityKey identityKey);
|
||||
|
||||
public void insertMessageState(
|
||||
ChatIdentifier chatIdentifier,
|
||||
final String messageId,
|
||||
|
@ -402,9 +422,10 @@ public abstract class MessageDao {
|
|||
@Query(
|
||||
"SELECT message.id as"
|
||||
+ " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion"
|
||||
+ " as version,inReplyToMessageEntityId FROM message JOIN message_version ON"
|
||||
+ " message.latestVersion=message_version.id WHERE message.chatId=:chatId AND"
|
||||
+ " latestVersion IS NOT NULL ORDER BY message.receivedAt")
|
||||
+ " as version,inReplyToMessageEntityId,encryption,identityKey FROM message JOIN"
|
||||
+ " message_version ON message.latestVersion=message_version.id WHERE"
|
||||
+ " message.chatId=:chatId AND latestVersion IS NOT NULL ORDER BY"
|
||||
+ " message.receivedAt")
|
||||
public abstract List<MessageWithContentReactions> getMessages(long chatId);
|
||||
|
||||
public void setInReplyTo(
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
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;
|
||||
import androidx.room.PrimaryKey;
|
||||
import com.google.common.base.Preconditions;
|
||||
import im.conversations.android.database.model.Encryption;
|
||||
import im.conversations.android.database.model.Modification;
|
||||
import im.conversations.android.transformer.MessageTransformation;
|
||||
import im.conversations.android.xmpp.model.stanza.Message;
|
||||
import java.time.Instant;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.jxmpp.jid.parts.Resourcepart;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
@Entity(
|
||||
tableName = "message_version",
|
||||
|
@ -36,6 +39,8 @@ public class MessageVersionEntity {
|
|||
public Resourcepart modifiedByResource;
|
||||
public String occupantId;
|
||||
public Instant receivedAt;
|
||||
@Nullable public Encryption encryption;
|
||||
@Nullable public IdentityKey identityKey;
|
||||
|
||||
// the version order is determined by the receivedAt
|
||||
// the actual display time and display order comes from the parent MessageEntity
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
public enum Encryption {
|
||||
OMEMO,
|
||||
CLEARTEXT,
|
||||
PGP
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.List;
|
||||
|
||||
public class MessageContent {
|
||||
|
||||
public final String language;
|
||||
|
@ -27,7 +24,4 @@ public class MessageContent {
|
|||
public static MessageContent file(final String url) {
|
||||
return new MessageContent(null, PartType.FILE, null, url);
|
||||
}
|
||||
|
||||
public static final List<MessageContent> RETRACTION =
|
||||
ImmutableList.of(new MessageContent(null, PartType.RETRACTION, null, null));
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
public class MessageWithContentReactions {
|
||||
|
||||
|
@ -31,6 +32,8 @@ public class MessageWithContentReactions {
|
|||
public Modification modification;
|
||||
public long version;
|
||||
public Long inReplyToMessageEntityId;
|
||||
public Encryption encryption;
|
||||
public IdentityKey identityKey;
|
||||
|
||||
@Relation(
|
||||
entity = MessageEntity.class,
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package im.conversations.android.transformer;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import im.conversations.android.axolotl.AxolotlPayload;
|
||||
import im.conversations.android.database.model.Encryption;
|
||||
import im.conversations.android.database.model.MessageContent;
|
||||
import im.conversations.android.database.model.PartType;
|
||||
import im.conversations.android.xmpp.model.jabber.Body;
|
||||
import im.conversations.android.xmpp.model.oob.OutOfBandData;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
public class MessageContentWrapper {
|
||||
|
||||
public static final MessageContentWrapper RETRACTION =
|
||||
new MessageContentWrapper(
|
||||
ImmutableList.of(new MessageContent(null, PartType.RETRACTION, null, null)),
|
||||
Encryption.CLEARTEXT,
|
||||
null);
|
||||
|
||||
public final List<MessageContent> contents;
|
||||
public final Encryption encryption;
|
||||
public final IdentityKey identityKey;
|
||||
|
||||
private MessageContentWrapper(
|
||||
List<MessageContent> contents, Encryption encryption, IdentityKey identityKey) {
|
||||
if (encryption == Encryption.OMEMO) {
|
||||
Preconditions.checkArgument(
|
||||
Objects.nonNull(identityKey),
|
||||
"OMEMO encrypted content must provide an identity key");
|
||||
}
|
||||
this.contents = contents;
|
||||
this.encryption = encryption;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public static MessageContentWrapper parseCleartext(final MessageTransformation transformation) {
|
||||
final Collection<Body> bodies = transformation.getExtensions(Body.class);
|
||||
final Collection<OutOfBandData> outOfBandData =
|
||||
transformation.getExtensions(OutOfBandData.class);
|
||||
final ImmutableList.Builder<MessageContent> messageContentBuilder = ImmutableList.builder();
|
||||
|
||||
if (bodies.size() == 1 && outOfBandData.size() == 1) {
|
||||
final String text = Iterables.getOnlyElement(bodies).getContent();
|
||||
final String url = Iterables.getOnlyElement(outOfBandData).getURL();
|
||||
if (!Strings.isNullOrEmpty(url) && url.equals(text)) {
|
||||
return cleartext(ImmutableList.of(MessageContent.file(url)));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO verify that body is not fallback
|
||||
for (final Body body : bodies) {
|
||||
final String text = body.getContent();
|
||||
if (Strings.isNullOrEmpty(text)) {
|
||||
continue;
|
||||
}
|
||||
messageContentBuilder.add(MessageContent.text(text, body.getLang()));
|
||||
}
|
||||
for (final OutOfBandData data : outOfBandData) {
|
||||
final String url = data.getURL();
|
||||
if (Strings.isNullOrEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
messageContentBuilder.add(MessageContent.file(url));
|
||||
}
|
||||
return cleartext(messageContentBuilder.build());
|
||||
}
|
||||
|
||||
private static MessageContentWrapper cleartext(final List<MessageContent> contents) {
|
||||
return new MessageContentWrapper(contents, Encryption.CLEARTEXT, null);
|
||||
}
|
||||
|
||||
public static MessageContentWrapper ofAxolotl(final AxolotlPayload payload) {
|
||||
if (payload.hasPayload()) {
|
||||
return new MessageContentWrapper(
|
||||
ImmutableList.of(MessageContent.text(payload.payloadAsString(), null)),
|
||||
Encryption.OMEMO,
|
||||
payload.identityKey);
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s does not have payload", payload.getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return this.contents.isEmpty();
|
||||
}
|
||||
}
|
|
@ -1,32 +1,24 @@
|
|||
package im.conversations.android.transformer;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import im.conversations.android.axolotl.AxolotlDecryptionException;
|
||||
import im.conversations.android.axolotl.AxolotlService;
|
||||
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;
|
||||
import im.conversations.android.xmpp.model.jabber.Body;
|
||||
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.reactions.Reactions;
|
||||
import im.conversations.android.xmpp.model.reply.Reply;
|
||||
import im.conversations.android.xmpp.model.retract.Retract;
|
||||
import im.conversations.android.xmpp.model.stanza.Message;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -89,13 +81,12 @@ public class Transformer {
|
|||
final Reactions reactions = transformation.getExtension(Reactions.class);
|
||||
final Retract retract = transformation.getExtension(Retract.class);
|
||||
final Encrypted encrypted = transformation.getExtension(Encrypted.class);
|
||||
final List<MessageContent> contents;
|
||||
final MessageContentWrapper contents;
|
||||
if (encrypted != null) {
|
||||
try {
|
||||
final var payload = axolotlService.decrypt(transformation.from, encrypted);
|
||||
if (payload.hasPayload()) {
|
||||
contents =
|
||||
ImmutableList.of(MessageContent.text(payload.payloadAsString(), null));
|
||||
contents = MessageContentWrapper.ofAxolotl(payload);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -107,7 +98,7 @@ public class Transformer {
|
|||
} else {
|
||||
// TODO we need to remove fallbacks for reactions, retractions and potentially other
|
||||
// things
|
||||
contents = parseContent(transformation);
|
||||
contents = MessageContentWrapper.parseCleartext(transformation);
|
||||
}
|
||||
|
||||
final boolean identifiableSender =
|
||||
|
@ -131,7 +122,8 @@ public class Transformer {
|
|||
.getOrCreateVersion(
|
||||
chat, transformation, retract.getId(), Modification.RETRACTION);
|
||||
database.messageDao()
|
||||
.insertMessageContent(messageIdentifier.version, MessageContent.RETRACTION);
|
||||
.insertMessageContent(
|
||||
messageIdentifier.version, MessageContentWrapper.RETRACTION);
|
||||
return true;
|
||||
} else if (contents.isEmpty()) {
|
||||
LOGGER.info("Received message from {} w/o contents", transformation.from);
|
||||
|
@ -173,42 +165,6 @@ public class Transformer {
|
|||
return true;
|
||||
}
|
||||
|
||||
protected List<MessageContent> parseContent(final MessageTransformation transformation) {
|
||||
final var encrypted = transformation.getExtension(Encrypted.class);
|
||||
final var encryptedWithPayload = encrypted != null && encrypted.hasPayload();
|
||||
final Collection<Body> bodies = transformation.getExtensions(Body.class);
|
||||
final Collection<OutOfBandData> outOfBandData =
|
||||
transformation.getExtensions(OutOfBandData.class);
|
||||
final ImmutableList.Builder<MessageContent> messageContentBuilder = ImmutableList.builder();
|
||||
|
||||
// TODO decrypt
|
||||
|
||||
if (bodies.size() == 1 && outOfBandData.size() == 1) {
|
||||
final String text = Iterables.getOnlyElement(bodies).getContent();
|
||||
final String url = Iterables.getOnlyElement(outOfBandData).getURL();
|
||||
if (!Strings.isNullOrEmpty(url) && url.equals(text)) {
|
||||
return ImmutableList.of(MessageContent.file(url));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO verify that body is not fallback
|
||||
for (final Body body : bodies) {
|
||||
final String text = body.getContent();
|
||||
if (Strings.isNullOrEmpty(text)) {
|
||||
continue;
|
||||
}
|
||||
messageContentBuilder.add(MessageContent.text(text, body.getLang()));
|
||||
}
|
||||
for (final OutOfBandData data : outOfBandData) {
|
||||
final String url = data.getURL();
|
||||
if (Strings.isNullOrEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
messageContentBuilder.add(MessageContent.file(url));
|
||||
}
|
||||
return messageContentBuilder.build();
|
||||
}
|
||||
|
||||
private void transformMessageState(
|
||||
final ChatIdentifier chat, final MessageTransformation transformation) {
|
||||
final var displayed = transformation.getExtension(Displayed.class);
|
||||
|
|
Loading…
Reference in a new issue