store messages in database

This commit is contained in:
Daniel Gultsch 2023-02-10 14:10:51 +01:00
parent dc371d7017
commit 9b62861a64
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
28 changed files with 752 additions and 71 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "2972255ca35c75ece48909471313d20a", "identityHash": "03075d3509cc0d79cf5e733cff6b71fd",
"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, `bareTo` TEXT, `toResource` TEXT, `bareFrom` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `acknowledged` INTEGER NOT NULL, FOREIGN KEY(`chatId`) REFERENCES `chat`(`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, 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 )",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -1447,8 +1447,14 @@
"notNull": false "notNull": false
}, },
{ {
"fieldPath": "bareTo", "fieldPath": "outgoing",
"columnName": "bareTo", "columnName": "outgoing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "toBare",
"columnName": "toBare",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
@ -1459,8 +1465,8 @@
"notNull": false "notNull": false
}, },
{ {
"fieldPath": "bareFrom", "fieldPath": "fromBare",
"columnName": "bareFrom", "columnName": "fromBare",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
@ -1488,6 +1494,18 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
{
"fieldPath": "stanzaIdVerified",
"columnName": "stanzaIdVerified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "latestVersion",
"columnName": "latestVersion",
"affinity": "INTEGER",
"notNull": false
},
{ {
"fieldPath": "acknowledged", "fieldPath": "acknowledged",
"columnName": "acknowledged", "columnName": "acknowledged",
@ -1510,6 +1528,15 @@
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId` ON `${TABLE_NAME}` (`chatId`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId` ON `${TABLE_NAME}` (`chatId`)"
},
{
"name": "index_message_latestVersion",
"unique": false,
"columnNames": [
"latestVersion"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_latestVersion` ON `${TABLE_NAME}` (`latestVersion`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -1523,11 +1550,22 @@
"referencedColumns": [ "referencedColumns": [
"id" "id"
] ]
},
{
"table": "message_version",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"latestVersion"
],
"referencedColumns": [
"id"
]
} }
] ]
}, },
{ {
"tableName": "message_part", "tableName": "message_content",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageVersionId` INTEGER NOT NULL, `language` TEXT, `type` TEXT, `body` TEXT, `url` TEXT, FOREIGN KEY(`messageVersionId`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageVersionId` INTEGER NOT NULL, `language` TEXT, `type` TEXT, `body` TEXT, `url` TEXT, FOREIGN KEY(`messageVersionId`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [ "fields": [
{ {
@ -1575,13 +1613,13 @@
}, },
"indices": [ "indices": [
{ {
"name": "index_message_part_messageVersionId", "name": "index_message_content_messageVersionId",
"unique": false, "unique": false,
"columnNames": [ "columnNames": [
"messageVersionId" "messageVersionId"
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_part_messageVersionId` ON `${TABLE_NAME}` (`messageVersionId`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_message_content_messageVersionId` ON `${TABLE_NAME}` (`messageVersionId`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -1600,7 +1638,7 @@
}, },
{ {
"tableName": "message_version", "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(`messageId`) 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, FOREIGN KEY(`messageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -1665,13 +1703,13 @@
}, },
"indices": [ "indices": [
{ {
"name": "index_message_version_messageId", "name": "index_message_version_messageEntityId",
"unique": false, "unique": false,
"columnNames": [ "columnNames": [
"messageId" "messageEntityId"
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_version_messageId` ON `${TABLE_NAME}` (`messageId`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_message_version_messageEntityId` ON `${TABLE_NAME}` (`messageEntityId`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -1680,7 +1718,7 @@
"onDelete": "CASCADE", "onDelete": "CASCADE",
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"columns": [ "columns": [
"messageId" "messageEntityId"
], ],
"referencedColumns": [ "referencedColumns": [
"id" "id"
@ -2112,7 +2150,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, '2972255ca35c75ece48909471313d20a')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '03075d3509cc0d79cf5e733cff6b71fd')"
] ]
} }
} }

View file

@ -24,6 +24,7 @@ public final class Namespace {
public static final String CONFERENCE = "jabber:x:conference"; public static final String CONFERENCE = "jabber:x:conference";
public static final String CSI = "urn:xmpp:csi:0"; public static final String CSI = "urn:xmpp:csi:0";
public static final String DATA = "jabber:x:data"; public static final String DATA = "jabber:x:data";
public static final String DELAY = "urn:xmpp:delay";
public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts"; public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts";
public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
@ -65,6 +66,7 @@ public final class Namespace {
public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1";
public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1";
public static final String LAST_MESSAGE_CORRECTION = "urn:xmpp:message-correct:0"; public static final String LAST_MESSAGE_CORRECTION = "urn:xmpp:message-correct:0";
public static final String MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2";
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";
@ -74,9 +76,9 @@ public final class Namespace {
public static final String PARS = "urn:xmpp:pars:0"; public static final String PARS = "urn:xmpp:pars:0";
public static final String PING = "urn:xmpp:ping"; public static final String PING = "urn:xmpp:ping";
public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String PUBSUB_OWNER = PUBSUB + "#owner";
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
public static final String PUB_SUB = "http://jabber.org/protocol/pubsub"; public static final String PUB_SUB = "http://jabber.org/protocol/pubsub";
public static final String PUB_SUB_ERRORS = PUB_SUB + "#errors"; public static final String PUB_SUB_ERRORS = PUB_SUB + "#errors";
public static final String PUB_SUB_EVENT = PUB_SUB + "#event"; public static final String PUB_SUB_EVENT = PUB_SUB + "#event";
@ -91,7 +93,6 @@ public final class Namespace {
public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String SASL_2 = "urn:xmpp:sasl:2";
public static final String STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"; public static final String STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas";
public static final String STANZA_IDS = "urn:xmpp:sid:0"; public static final String STANZA_IDS = "urn:xmpp:sid:0";
public static final String MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2";
public static final String STREAMS = "http://etherx.jabber.org/streams"; public static final String STREAMS = "http://etherx.jabber.org/streams";
public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3";
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";

View file

@ -10,6 +10,7 @@ import im.conversations.android.database.dao.AvatarDao;
import im.conversations.android.database.dao.AxolotlDao; import im.conversations.android.database.dao.AxolotlDao;
import im.conversations.android.database.dao.BlockingDao; import im.conversations.android.database.dao.BlockingDao;
import im.conversations.android.database.dao.BookmarkDao; import im.conversations.android.database.dao.BookmarkDao;
import im.conversations.android.database.dao.ChatDao;
import im.conversations.android.database.dao.DiscoDao; import im.conversations.android.database.dao.DiscoDao;
import im.conversations.android.database.dao.MessageDao; import im.conversations.android.database.dao.MessageDao;
import im.conversations.android.database.dao.NickDao; import im.conversations.android.database.dao.NickDao;
@ -35,8 +36,8 @@ import im.conversations.android.database.entity.DiscoExtensionFieldValueEntity;
import im.conversations.android.database.entity.DiscoFeatureEntity; import im.conversations.android.database.entity.DiscoFeatureEntity;
import im.conversations.android.database.entity.DiscoIdentityEntity; 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.MessageEntity; import im.conversations.android.database.entity.MessageEntity;
import im.conversations.android.database.entity.MessagePartEntity;
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;
@ -67,7 +68,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
DiscoIdentityEntity.class, DiscoIdentityEntity.class,
DiscoItemEntity.class, DiscoItemEntity.class,
MessageEntity.class, MessageEntity.class,
MessagePartEntity.class, MessageContentEntity.class,
MessageVersionEntity.class, MessageVersionEntity.class,
NickEntity.class, NickEntity.class,
PresenceEntity.class, PresenceEntity.class,
@ -107,6 +108,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
public abstract BookmarkDao bookmarkDao(); public abstract BookmarkDao bookmarkDao();
public abstract ChatDao chatDao();
public abstract DiscoDao discoDao(); public abstract DiscoDao discoDao();
public abstract MessageDao messageDao(); public abstract MessageDao messageDao();

View file

@ -0,0 +1,54 @@
package im.conversations.android.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Transaction;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.database.entity.ChatEntity;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.ChatIdentifier;
import im.conversations.android.database.model.ChatType;
import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Arrays;
@Dao
public abstract class ChatDao {
@Transaction
public ChatIdentifier getOrCreateChat(
final Account account,
final Jid remote,
final Message.Type messageType,
final boolean multiUserChat) {
final ChatType chatType;
if (multiUserChat
&& Arrays.asList(Message.Type.CHAT, Message.Type.NORMAL).contains(messageType)) {
chatType = ChatType.MUC_PM;
} else if (messageType == Message.Type.GROUPCHAT) {
chatType = ChatType.MUC;
} else {
chatType = ChatType.INDIVIDUAL;
}
final Jid address = chatType == ChatType.MUC_PM ? remote : remote.asBareJid();
final ChatIdentifier existing = get(account.id, address);
if (existing != null) {
return existing;
}
final var entity = new ChatEntity();
entity.accountId = account.id;
entity.address = address.toEscapedString();
entity.type = chatType;
entity.archived = true;
final long id = insert(entity);
return new ChatIdentifier(id, address, chatType, true);
}
@Query(
"SELECT id,address,type,archived FROM chat WHERE accountId=:accountId AND"
+ " address=:address")
protected abstract ChatIdentifier get(final long accountId, final Jid address);
@Insert
protected abstract long insert(ChatEntity chatEntity);
}

View file

@ -2,24 +2,38 @@ package im.conversations.android.database.dao;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction;
import com.google.common.base.Preconditions;
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.MessageEntity;
import im.conversations.android.database.entity.MessageVersionEntity;
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.MessageContent;
import im.conversations.android.database.model.MessageIdentifier;
import im.conversations.android.database.model.Modification;
import im.conversations.android.transformer.Transformation;
import java.util.Collection;
import java.util.List;
@Dao @Dao
public abstract class MessageDao { public abstract class MessageDao {
@Query( @Query(
"UPDATE message SET acknowledged=1 WHERE messageId=:messageId AND bareTo=:bareTo AND" "UPDATE message SET acknowledged=1 WHERE messageId=:messageId AND toBare=:toBare AND"
+ " toResource=NULL AND chatId IN (SELECT id FROM chat WHERE accountId=:account)") + " toResource=NULL AND chatId IN (SELECT id FROM chat WHERE accountId=:account)")
abstract int acknowledge(long account, String messageId, final String bareTo); abstract int acknowledge(long account, String messageId, final String toBare);
@Query( @Query(
"UPDATE message SET acknowledged=1 WHERE messageId=:messageId AND bareTo=:bareTo AND" "UPDATE message SET acknowledged=1 WHERE messageId=:messageId AND toBare=:toBare AND"
+ " toResource=:toResource AND chatId IN (SELECT id FROM chat WHERE" + " toResource=:toResource AND chatId IN (SELECT id FROM chat WHERE"
+ " accountId=:account)") + " accountId=:account)")
abstract int acknowledge( abstract int acknowledge(
long account, final String messageId, final String bareTo, final String toResource); long account, final String messageId, final String toBare, final String toResource);
public boolean acknowledge( public boolean acknowledge(
final Account account, @NonNull final String messageId, @NonNull final Jid to) { final Account account, @NonNull final String messageId, @NonNull final Jid to) {
@ -36,4 +50,87 @@ public abstract class MessageDao {
> 0; > 0;
} }
} }
@Transaction
public MessageIdentifier getOrCreateMessage(
ChatIdentifier chatIdentifier, final Transformation transformation) {
final MessageIdentifier messageIdentifier =
get(
chatIdentifier.id,
transformation.fromBare(),
transformation.stanzaId,
transformation.messageId);
if (messageIdentifier != null) {
if (messageIdentifier.isStub()) {
// TODO create version
// TODO fill up information
return messageIdentifier;
} else {
throw new IllegalStateException(
String.format(
"A message with stanzaId '%s' and messageId '%s' from %s already"
+ " exists",
transformation.stanzaId,
transformation.messageId,
transformation.from));
}
}
final MessageEntity entity = MessageEntity.of(chatIdentifier.id, transformation);
final long messageEntityId = insert(entity);
final long messageVersionId =
insert(
MessageVersionEntity.of(
messageEntityId, Modification.ORIGINAl, transformation));
setLatestMessageId(messageEntityId, messageVersionId);
return new MessageIdentifier(
messageEntityId,
transformation.stanzaId,
transformation.messageId,
transformation.fromBare(),
messageVersionId);
}
@Insert
protected abstract long insert(MessageEntity messageEntity);
@Insert
protected abstract long insert(MessageVersionEntity messageVersionEntity);
@Query("UPDATE message SET latestVersion=:messageVersionId WHERE id=:messageEntityId")
protected abstract void setLatestMessageId(
final long messageEntityId, final long messageVersionId);
public Long getOrCreateStub(final Transformation transformation) {
// TODO look up where parentId matches messageId (or stanzaId for group chats)
// when creating stub either set from (correction) or dont (other attachment)
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 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");
Preconditions.checkArgument(
contents.size() > 0,
"If you are trying to insert empty contents something went wrong");
insertMessageContent(
Lists.transform(contents, c -> MessageContentEntity.of(latestVersion, c)));
}
@Insert
protected abstract void insertMessageContent(Collection<MessageContentEntity> contentEntities);
} }

View file

@ -5,10 +5,11 @@ 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 im.conversations.android.database.model.MessageContent;
import im.conversations.android.database.model.PartType; import im.conversations.android.database.model.PartType;
@Entity( @Entity(
tableName = "message_part", tableName = "message_content",
foreignKeys = foreignKeys =
@ForeignKey( @ForeignKey(
entity = MessageVersionEntity.class, entity = MessageVersionEntity.class,
@ -16,7 +17,7 @@ import im.conversations.android.database.model.PartType;
childColumns = {"messageVersionId"}, childColumns = {"messageVersionId"},
onDelete = ForeignKey.CASCADE), onDelete = ForeignKey.CASCADE),
indices = {@Index(value = "messageVersionId")}) indices = {@Index(value = "messageVersionId")})
public class MessagePartEntity { public class MessageContentEntity {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public Long id; public Long id;
@ -30,4 +31,15 @@ public class MessagePartEntity {
public String body; public String body;
public String url; public String url;
public static MessageContentEntity of(
final long messageVersionId, final MessageContent content) {
final var entity = new MessageContentEntity();
entity.messageVersionId = messageVersionId;
entity.language = content.language;
entity.type = content.type;
entity.body = content.body;
entity.url = content.url;
return entity;
}
} }

View file

@ -1,21 +1,31 @@
package im.conversations.android.database.entity; package im.conversations.android.database.entity;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.Entity; 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;
import java.util.Objects;
@Entity( @Entity(
tableName = "message", tableName = "message",
foreignKeys = foreignKeys = {
@ForeignKey( @ForeignKey(
entity = ChatEntity.class, entity = ChatEntity.class,
parentColumns = {"id"}, parentColumns = {"id"},
childColumns = {"chatId"}, childColumns = {"chatId"},
onDelete = ForeignKey.CASCADE), onDelete = ForeignKey.CASCADE),
indices = {@Index(value = "chatId")}) @ForeignKey(
entity = MessageVersionEntity.class,
parentColumns = {"id"},
childColumns = {"latestVersion"},
onDelete = ForeignKey.CASCADE),
},
indices = {@Index(value = "chatId"), @Index(value = "latestVersion")})
public class MessageEntity { public class MessageEntity {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -28,17 +38,36 @@ public class MessageEntity {
public boolean outgoing; public boolean outgoing;
public String bareTo; public Jid toBare;
public String toResource; public String toResource;
public String bareFrom; public Jid fromBare;
public String fromResource; public String fromResource;
public String occupantId; public String occupantId;
public String messageId; public String messageId;
public String stanzaId; public String stanzaId;
// the stanza id might not be verified if this MessageEntity was created as a stub parent to attach reactions to or new versions (created by LMC etc) // the stanza id might not be verified if this MessageEntity was created as a stub parent to
public String stanzaIdVerified; // attach reactions to or new versions (created by LMC etc)
public boolean stanzaIdVerified;
@Nullable public Long latestVersion;
public boolean acknowledged = false; public boolean acknowledged = false;
public static MessageEntity of(final long chatId, final Transformation transformation) {
final var entity = new MessageEntity();
entity.chatId = chatId;
entity.receivedAt = transformation.receivedAt;
entity.sentAt = transformation.sentAt();
entity.outgoing = transformation.outgoing();
entity.toBare = transformation.toBare();
entity.toResource = transformation.toResource();
entity.fromBare = transformation.fromBare();
entity.fromResource = transformation.fromResource();
entity.messageId = transformation.messageId;
entity.stanzaId = transformation.stanzaId;
entity.stanzaIdVerified = Objects.nonNull(transformation.stanzaId);
return entity;
}
} }

View file

@ -5,18 +5,21 @@ 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.database.model.Modification; import im.conversations.android.database.model.Modification;
import im.conversations.android.transformer.Transformation;
import java.time.Instant; import java.time.Instant;
@Entity( @Entity(
tableName = "message_version", tableName = "message_version",
foreignKeys = foreignKeys = {
@ForeignKey( @ForeignKey(
entity = MessageEntity.class, entity = MessageEntity.class,
parentColumns = {"id"}, parentColumns = {"id"},
childColumns = {"messageId"}, childColumns = {"messageEntityId"},
onDelete = ForeignKey.CASCADE), onDelete = ForeignKey.CASCADE),
indices = {@Index(value = "messageId")}) },
indices = {@Index(value = "messageEntityId")})
public class MessageVersionEntity { public class MessageVersionEntity {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -26,14 +29,28 @@ public class MessageVersionEntity {
public String messageId; public String messageId;
public String stanzaId; public String stanzaId;
public Modification modification; public Modification modification;
public String modifiedBy; public Jid modifiedBy;
public String modifiedByResource; public String modifiedByResource;
public String occupantId; public String occupantId;
Instant receivedAt; public Instant receivedAt;
// the version order is determined by the receivedAt // the version order is determined by the receivedAt
// the actual display time and display order comes from the parent MessageEntity // the actual display time and display order comes from the parent MessageEntity
// the original has a receivedAt = null and stanzaId = null and inherits it's timestamp from // the original has a receivedAt = null and stanzaId = null and inherits it's timestamp from
// it's parent // it's parent
public static MessageVersionEntity of(
long messageEntityId,
final Modification modification,
final Transformation transformation) {
final var entity = new MessageVersionEntity();
entity.messageEntityId = messageEntityId;
entity.messageId = transformation.messageId;
entity.stanzaId = transformation.stanzaId;
entity.modification = modification;
entity.modifiedBy = transformation.fromBare();
entity.modifiedByResource = transformation.fromResource();
entity.receivedAt = transformation.receivedAt;
return entity;
}
} }

View file

@ -0,0 +1,18 @@
package im.conversations.android.database.model;
import eu.siacs.conversations.xmpp.Jid;
public class ChatIdentifier {
public final long id;
public final Jid address;
public final ChatType type;
public final boolean archived;
public ChatIdentifier(long id, Jid address, ChatType type, final boolean archived) {
this.id = id;
this.address = address;
this.type = type;
this.archived = archived;
}
}

View file

@ -0,0 +1,27 @@
package im.conversations.android.database.model;
public class MessageContent {
public final String language;
public final PartType type;
public final String body;
public final String url;
public MessageContent(String language, PartType type, String body, String url) {
this.language = language;
this.type = type;
this.body = body;
this.url = url;
}
public static MessageContent text(final String body, final String language) {
return new MessageContent(language, PartType.TEXT, body, null);
}
public static MessageContent file(final String url) {
return new MessageContent(null, PartType.FILE, null, url);
}
}

View file

@ -0,0 +1,37 @@
package im.conversations.android.database.model;
import com.google.common.base.MoreObjects;
import eu.siacs.conversations.xmpp.Jid;
public class MessageIdentifier {
public final long id;
public final String stanzaId;
public final String messageId;
public final Jid fromBare;
public final Long latestVersion;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("stanzaId", stanzaId)
.add("messageId", messageId)
.add("fromBare", fromBare)
.add("latestVersion", latestVersion)
.toString();
}
public MessageIdentifier(
long id, String stanzaId, String messageId, Jid fromBare, Long latestVersion) {
this.id = id;
this.stanzaId = stanzaId;
this.messageId = messageId;
this.fromBare = fromBare;
this.latestVersion = latestVersion;
}
public boolean isStub() {
return this.latestVersion == null;
}
}

View file

@ -1,16 +1,20 @@
package im.conversations.android.transformer; package im.conversations.android.transformer;
import androidx.annotation.NonNull;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
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.Jid; import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.model.DeliveryReceipt;
import im.conversations.android.xmpp.model.DeliveryReceiptRequest; import im.conversations.android.xmpp.model.DeliveryReceiptRequest;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.axolotl.Encrypted; import im.conversations.android.xmpp.model.axolotl.Encrypted;
import im.conversations.android.xmpp.model.jabber.Body; import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.jabber.Thread; import im.conversations.android.xmpp.model.jabber.Thread;
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.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -18,10 +22,18 @@ import java.util.List;
public class Transformation { public class Transformation {
private static final List<Class<? extends Extension>> EXTENSION_FOR_TRANSFORMATION = private static final List<Class<? extends Extension>> EXTENSION_FOR_TRANSFORMATION =
Arrays.asList(Body.class, Thread.class, Encrypted.class, OutOfBandData.class); Arrays.asList(
Body.class,
Thread.class,
Encrypted.class,
OutOfBandData.class,
DeliveryReceipt.class,
MultiUserChat.class);
public final Instant receivedAt;
public final Jid to; public final Jid to;
public final Jid from; public final Jid from;
public final Jid remote;
public final Message.Type type; public final Message.Type type;
public final String messageId; public final String messageId;
public final String stanzaId; public final String stanzaId;
@ -31,15 +43,19 @@ public class Transformation {
public final Collection<DeliveryReceiptRequest> deliveryReceiptRequests; public final Collection<DeliveryReceiptRequest> deliveryReceiptRequests;
private Transformation( private Transformation(
final Instant receivedAt,
final Jid to, final Jid to,
final Jid from, final Jid from,
final Jid remote,
final Message.Type type, final Message.Type type,
final String messageId, final String messageId,
final String stanzaId, final String stanzaId,
final List<Extension> extensions, final List<Extension> extensions,
final Collection<DeliveryReceiptRequest> deliveryReceiptRequests) { final Collection<DeliveryReceiptRequest> deliveryReceiptRequests) {
this.receivedAt = receivedAt;
this.to = to; this.to = to;
this.from = from; this.from = from;
this.remote = remote;
this.type = type; this.type = type;
this.messageId = messageId; this.messageId = messageId;
this.stanzaId = stanzaId; this.stanzaId = stanzaId;
@ -51,6 +67,32 @@ public class Transformation {
return this.extensions.size() > 0; return this.extensions.size() > 0;
} }
public Jid fromBare() {
return from == null ? null : from.asBareJid();
}
public String fromResource() {
return from == null ? null : from.getResource();
}
public Jid toBare() {
return to == null ? null : to.asBareJid();
}
public String toResource() {
return to == null ? null : to.getResource();
}
public Instant sentAt() {
// TODO get Delay that matches sender; return receivedAt if not found
return receivedAt;
}
public boolean outgoing() {
// TODO handle case for self addressed (to == from)
return remote.asBareJid().equals(toBare());
}
public <E extends Extension> E getExtension(final Class<E> clazz) { public <E extends Extension> E getExtension(final Class<E> clazz) {
final var extension = Iterables.find(this.extensions, clazz::isInstance, null); final var extension = Iterables.find(this.extensions, clazz::isInstance, null);
return extension == null ? null : clazz.cast(extension); return extension == null ? null : clazz.cast(extension);
@ -61,7 +103,11 @@ public class Transformation {
Collections2.filter(this.extensions, clazz::isInstance), clazz::cast); Collections2.filter(this.extensions, clazz::isInstance), clazz::cast);
} }
public static Transformation of(final Message message, final String stanzaId) { public static Transformation of(
@NonNull final Message message,
@NonNull final Instant receivedAt,
@NonNull final Jid remote,
final String stanzaId) {
final var to = message.getTo(); final var to = message.getTo();
final var from = message.getFrom(); final var from = message.getFrom();
final var type = message.getType(); final var type = message.getType();
@ -72,6 +118,14 @@ public class Transformation {
} }
final var requests = message.getExtensions(DeliveryReceiptRequest.class); final var requests = message.getExtensions(DeliveryReceiptRequest.class);
return new Transformation( return new Transformation(
to, from, type, messageId, stanzaId, extensionListBuilder.build(), requests); receivedAt,
to,
from,
remote,
type,
messageId,
stanzaId,
extensionListBuilder.build(),
requests);
} }
} }

View file

@ -0,0 +1,33 @@
package im.conversations.android.transformer;
import android.content.Context;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant;
public class TransformationFactory extends XmppConnection.Delegate {
public TransformationFactory(Context context, XmppConnection connection) {
super(context, connection);
}
public Transformation create(final Message message, final String stanzaId) {
return create(message, stanzaId, Instant.now());
}
public Transformation create(
final Message message, final String stanzaId, final Instant receivedAt) {
final var boundAddress = connection.getBoundAddress().asBareJid();
final var from = message.getFrom();
final var to = message.getTo();
final Jid remote;
if (from == null || from.asBareJid().equals(boundAddress)) {
remote = to == null ? boundAddress : to;
} else {
remote = from;
}
// TODO parse occupant on group chats
return Transformation.of(message, receivedAt, remote, stanzaId);
}
}

View file

@ -1,13 +1,29 @@
package im.conversations.android.transformer; package im.conversations.android.transformer;
import android.content.Context; import android.content.Context;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.MessageContent;
import im.conversations.android.xmpp.model.DeliveryReceipt;
import im.conversations.android.xmpp.model.axolotl.Encrypted; 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.jabber.Body;
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 java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Transformer { public class Transformer {
private static final Logger LOGGER = LoggerFactory.getLogger(Transformer.class);
private final Context context; private final Context context;
private final Account account; private final Account account;
@ -16,23 +32,93 @@ public class Transformer {
this.account = account; this.account = account;
} }
public boolean transform(final Transformation transformation) {
final var database = ConversationsDatabase.getInstance(context);
return database.runInTransaction(() -> transform(database, transformation));
}
/** /**
* @param transformation * @param transformation
* @return returns true if there is something we want to send a delivery receipt for. Basically * @return returns true if there is something we want to send a delivery receipt for. Basically
* anything that created a new message in the database. Notably not something that only * anything that created a new message in the database. Notably not something that only
* updated a status somewhere * updated a status somewhere
*/ */
public boolean transform(final Transformation transformation) { private boolean transform(
final var encrypted = transformation.getExtension(Encrypted.class); final ConversationsDatabase database, final Transformation transformation) {
final var bodies = transformation.getExtensions(Body.class); final var remote = transformation.remote;
final var outOfBandData = transformation.getExtensions(OutOfBandData.class); final var messageType = transformation.type;
final var deliveryReceipt = transformation.getExtension(DeliveryReceipt.class);
final Replace lastMessageCorrection = transformation.getExtension(Replace.class);
final var muc = transformation.getExtension(MultiUserChat.class);
// TODO get or create Chat
// TODO create MessageEntity or get existing entity
// TODO for replaced message create a new version; re-target latestVersion
// TODO apply errors, displayed, received etc
// TODO apply reactions
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
final boolean versionModification = Objects.nonNull(lastMessageCorrection);
// TODO get or create Cha
final ChatIdentifier chat =
database.chatDao()
.getOrCreateChat(account, remote, messageType, Objects.nonNull(muc));
if (contents.isEmpty()) {
LOGGER.info("Received message from {} w/o contents", transformation.from);
// TODO apply errors, displayed, received etc
// TODO apply reactions
} else {
if (versionModification) {
// TODO use getOrStub
// TODO check if versionModification has already been applied
// TODO for replaced message create a new version; re-target latestVersion
} else {
final var messageIdentifier =
database.messageDao().getOrCreateMessage(chat, transformation);
database.messageDao()
.insertMessageContent(messageIdentifier.latestVersion, contents);
return true;
}
}
return true; return true;
} }
protected List<MessageContent> parseContent(final Transformation 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();
}
} }

View file

@ -0,0 +1,44 @@
package im.conversations.android.xmpp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public final class Timestamps {
private Timestamps() {
throw new IllegalStateException("Do not instantiate me");
}
public static long parse(final String input) throws ParseException {
if (input == null) {
throw new IllegalArgumentException("timestamp should not be null");
}
final String timestamp = input.replace("Z", "+0000");
final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
final long milliseconds = getMilliseconds(timestamp);
final String formatted =
timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5);
final Date date = simpleDateFormat.parse(formatted);
if (date == null) {
throw new IllegalArgumentException("Date was null");
}
return date.getTime() + milliseconds;
}
private static long getMilliseconds(final String timestamp) {
if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') {
final String millis = timestamp.substring(19, timestamp.length() - 5);
try {
double fractions = Double.parseDouble("0" + millis);
return Math.round(1000 * fractions);
} catch (final NumberFormatException e) {
return 0;
}
} else {
return 0;
}
}
}

View file

@ -2,9 +2,10 @@ package im.conversations.android.xmpp.manager;
import android.content.Context; import android.content.Context;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.transformer.Transformation; import im.conversations.android.transformer.TransformationFactory;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.mam.Result; import im.conversations.android.xmpp.model.delay.Delay;
import im.conversations.android.xmpp.model.mam.Result;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -13,8 +14,11 @@ public class ArchiveManager extends AbstractManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class); private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class);
private final TransformationFactory transformationFactory;
public ArchiveManager(Context context, XmppConnection connection) { public ArchiveManager(Context context, XmppConnection connection) {
super(context, connection); super(context, connection);
this.transformationFactory = new TransformationFactory(context, connection);
} }
public void handle(final Message message) { public void handle(final Message message) {
@ -24,14 +28,20 @@ public class ArchiveManager extends AbstractManager {
final var stanzaId = result.getId(); final var stanzaId = result.getId();
final var queryId = result.getQueryId(); final var queryId = result.getQueryId();
final var forwarded = result.getForwarded(); final var forwarded = result.getForwarded();
final var forwardedMessage = forwarded == null ? null : forwarded.getMessage(); if (forwarded == null || queryId == null || stanzaId == null) {
if (forwardedMessage == null || queryId == null || stanzaId == null) {
LOGGER.info("Received invalid MAM result from {} ", from); LOGGER.info("Received invalid MAM result from {} ", from);
return; return;
} }
final var forwardedMessage = forwarded.getMessage();
final var delay = forwarded.getExtension(Delay.class);
final var receivedAt = delay == null ? null : delay.getStamp();
if (forwardedMessage == null || receivedAt == null) {
LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from);
return;
}
// TODO get query based on queryId and from // TODO get query based on queryId and from
final var transformation = Transformation.of(forwardedMessage, stanzaId); final var transformation = this.transformationFactory.create(message, stanzaId, receivedAt);
// TODO create transformation; add transformation to Query.Transformer // TODO create transformation; add transformation to Query.Transformer
} }

View file

@ -0,0 +1,10 @@
package im.conversations.android.xmpp.model;
public abstract class DeliveryReceipt extends Extension {
protected DeliveryReceipt(Class<? extends Extension> clazz) {
super(clazz);
}
public abstract String getId();
}

View file

@ -9,4 +9,8 @@ public class Encrypted extends Extension {
public Encrypted() { public Encrypted() {
super(Encrypted.class); super(Encrypted.class);
} }
public boolean hasPayload() {
return hasExtension(Payload.class);
}
} }

View file

@ -0,0 +1,29 @@
package im.conversations.android.xmpp.model.delay;
import com.google.common.base.Strings;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.Timestamps;
import im.conversations.android.xmpp.model.Extension;
import java.text.ParseException;
import java.time.Instant;
@XmlElement(namespace = Namespace.DELAY)
public class Delay extends Extension {
public Delay() {
super(Delay.class);
}
public Instant getStamp() {
final var stamp = this.getAttribute("stamp");
if (Strings.isNullOrEmpty(stamp)) {
return null;
}
try {
return Instant.ofEpochMilli(Timestamps.parse(stamp));
} catch (final IllegalArgumentException | ParseException e) {
return null;
}
}
}

View file

@ -9,4 +9,8 @@ public class Body extends Extension {
public Body() { public Body() {
super(Body.class); super(Body.class);
} }
public String getLang() {
return this.getAttribute("xml:lang");
}
} }

View file

@ -1,4 +1,4 @@
package im.conversations.android.xmpp.mam; package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;

View file

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

View file

@ -1,10 +1,10 @@
package im.conversations.android.xmpp.model.markers; package im.conversations.android.xmpp.model.markers;
import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.DeliveryReceipt;
@XmlElement @XmlElement
public class Received extends Extension { public class Received extends DeliveryReceipt {
public Received() { public Received() {
super(Received.class); super(Received.class);
@ -13,4 +13,8 @@ public class Received extends Extension {
public void setId(String id) { public void setId(String id) {
this.setAttribute("id", id); this.setAttribute("id", id);
} }
public String getId() {
return this.getAttribute("id");
}
} }

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.muc.user;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement(name = "x")
public class MultiUserChat extends Extension {
public MultiUserChat() {
super(MultiUserChat.class);
}
}

View file

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

View file

@ -1,10 +1,10 @@
package im.conversations.android.xmpp.model.receipts; package im.conversations.android.xmpp.model.receipts;
import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.DeliveryReceipt;
@XmlElement @XmlElement
public class Received extends Extension { public class Received extends DeliveryReceipt {
public Received() { public Received() {
super(Received.class); super(Received.class);
@ -13,4 +13,8 @@ public class Received extends Extension {
public void setId(String id) { public void setId(String id) {
this.setAttribute("id", id); this.setAttribute("id", id);
} }
public String getId() {
return this.getAttribute("id");
}
} }

View file

@ -1,10 +1,9 @@
package im.conversations.android.xmpp.processor; package im.conversations.android.xmpp.processor;
import android.content.Context; import android.content.Context;
import im.conversations.android.transformer.Transformation; import im.conversations.android.transformer.TransformationFactory;
import im.conversations.android.transformer.Transformer; import im.conversations.android.transformer.Transformer;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.mam.Result;
import im.conversations.android.xmpp.manager.ArchiveManager; import im.conversations.android.xmpp.manager.ArchiveManager;
import im.conversations.android.xmpp.manager.CarbonsManager; import im.conversations.android.xmpp.manager.CarbonsManager;
import im.conversations.android.xmpp.manager.ChatStateManager; import im.conversations.android.xmpp.manager.ChatStateManager;
@ -13,6 +12,7 @@ import im.conversations.android.xmpp.manager.ReceiptManager;
import im.conversations.android.xmpp.manager.StanzaIdManager; import im.conversations.android.xmpp.manager.StanzaIdManager;
import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.carbons.Sent;
import im.conversations.android.xmpp.model.mam.Result;
import im.conversations.android.xmpp.model.pubsub.event.Event; import im.conversations.android.xmpp.model.pubsub.event.Event;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import im.conversations.android.xmpp.model.state.ChatStateNotification; import im.conversations.android.xmpp.model.state.ChatStateNotification;
@ -25,6 +25,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
private static final Logger LOGGER = LoggerFactory.getLogger(MessageProcessor.class); private static final Logger LOGGER = LoggerFactory.getLogger(MessageProcessor.class);
private final Level level; private final Level level;
private final TransformationFactory transformationFactory;
public MessageProcessor(final Context context, final XmppConnection connection) { public MessageProcessor(final Context context, final XmppConnection connection) {
this(context, connection, Level.ROOT); this(context, connection, Level.ROOT);
@ -34,6 +35,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
final Context context, final XmppConnection connection, final Level level) { final Context context, final XmppConnection connection, final Level level) {
super(context, connection); super(context, connection);
this.level = level; this.level = level;
this.transformationFactory = new TransformationFactory(context, connection);
} }
@Override @Override
@ -59,10 +61,13 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
return; return;
} }
// LOGGER.info("Message from {} with {}", message.getFrom(), message.getExtensionIds());
final var from = message.getFrom(); final var from = message.getFrom();
final var id = message.getId(); final var id = message.getId();
final var stanzaId = getManager(StanzaIdManager.class).getStanzaId(message); final var stanzaId = getManager(StanzaIdManager.class).getStanzaId(message);
final var transformation = Transformation.of(message, stanzaId); final var transformation = transformationFactory.create(message, stanzaId);
final boolean sendReceipts; final boolean sendReceipts;
if (transformation.isAnythingToTransform()) { if (transformation.isAnythingToTransform()) {
final var transformer = new Transformer(context, getAccount()); final var transformer = new Transformer(context, getAccount());

View file

@ -0,0 +1,44 @@
package im.conversations.android.xmpp;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.XmlElementReader;
import im.conversations.android.xmpp.model.delay.Delay;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.ConscryptMode;
@RunWith(RobolectricTestRunner.class)
@ConscryptMode(ConscryptMode.Mode.OFF)
public class TimestampTest {
@Test
public void testZuluNoMillis() throws IOException {
final String xml =
"<delay xmlns='urn:xmpp:delay'\n"
+ " from='capulet.com'\n"
+ " stamp='2002-09-10T23:08:25Z'/>";
final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
assertThat(element, instanceOf(Delay.class));
final Delay delay = (Delay) element;
assertEquals(1031699305000L, delay.getStamp().toEpochMilli());
}
@Test
public void testZuluWithMillis() throws IOException {
final String xml =
"<delay xmlns='urn:xmpp:delay'\n"
+ " from='capulet.com'\n"
+ " stamp='2002-09-10T23:08:25.023Z'/>";
final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
assertThat(element, instanceOf(Delay.class));
final Delay delay = (Delay) element;
assertEquals(1031699305023L, delay.getStamp().toEpochMilli());
}
}