store messages in database
This commit is contained in:
parent
dc371d7017
commit
9b62861a64
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "2972255ca35c75ece48909471313d20a",
|
||||
"identityHash": "03075d3509cc0d79cf5e733cff6b71fd",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account",
|
||||
|
@ -1420,7 +1420,7 @@
|
|||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -1447,8 +1447,14 @@
|
|||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bareTo",
|
||||
"columnName": "bareTo",
|
||||
"fieldPath": "outgoing",
|
||||
"columnName": "outgoing",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "toBare",
|
||||
"columnName": "toBare",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
|
@ -1459,8 +1465,8 @@
|
|||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bareFrom",
|
||||
"columnName": "bareFrom",
|
||||
"fieldPath": "fromBare",
|
||||
"columnName": "fromBare",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
|
@ -1488,6 +1494,18 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "stanzaIdVerified",
|
||||
"columnName": "stanzaIdVerified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "latestVersion",
|
||||
"columnName": "latestVersion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "acknowledged",
|
||||
"columnName": "acknowledged",
|
||||
|
@ -1510,6 +1528,15 @@
|
|||
],
|
||||
"orders": [],
|
||||
"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": [
|
||||
|
@ -1523,11 +1550,22 @@
|
|||
"referencedColumns": [
|
||||
"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 )",
|
||||
"fields": [
|
||||
{
|
||||
|
@ -1575,13 +1613,13 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_message_part_messageVersionId",
|
||||
"name": "index_message_content_messageVersionId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"messageVersionId"
|
||||
],
|
||||
"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": [
|
||||
|
@ -1600,7 +1638,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(`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": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -1665,13 +1703,13 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_message_version_messageId",
|
||||
"name": "index_message_version_messageEntityId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"messageId"
|
||||
"messageEntityId"
|
||||
],
|
||||
"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": [
|
||||
|
@ -1680,7 +1718,7 @@
|
|||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"messageId"
|
||||
"messageEntityId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
|
@ -2112,7 +2150,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, '2972255ca35c75ece48909471313d20a')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '03075d3509cc0d79cf5e733cff6b71fd')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ public final class Namespace {
|
|||
public static final String CONFERENCE = "jabber:x:conference";
|
||||
public static final String CSI = "urn:xmpp:csi:0";
|
||||
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 DISCO_INFO = "http://jabber.org/protocol/disco#info";
|
||||
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_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 MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2";
|
||||
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";
|
||||
|
@ -74,9 +76,9 @@ public final class Namespace {
|
|||
public static final String PARS = "urn:xmpp:pars:0";
|
||||
public static final String PING = "urn:xmpp:ping";
|
||||
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_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_ERRORS = PUB_SUB + "#errors";
|
||||
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 STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas";
|
||||
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 STREAM_MANAGEMENT = "urn:xmpp:sm:3";
|
||||
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
||||
|
|
|
@ -10,6 +10,7 @@ import im.conversations.android.database.dao.AvatarDao;
|
|||
import im.conversations.android.database.dao.AxolotlDao;
|
||||
import im.conversations.android.database.dao.BlockingDao;
|
||||
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.MessageDao;
|
||||
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.DiscoIdentityEntity;
|
||||
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.MessagePartEntity;
|
||||
import im.conversations.android.database.entity.MessageVersionEntity;
|
||||
import im.conversations.android.database.entity.NickEntity;
|
||||
import im.conversations.android.database.entity.PresenceEntity;
|
||||
|
@ -67,7 +68,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
|
|||
DiscoIdentityEntity.class,
|
||||
DiscoItemEntity.class,
|
||||
MessageEntity.class,
|
||||
MessagePartEntity.class,
|
||||
MessageContentEntity.class,
|
||||
MessageVersionEntity.class,
|
||||
NickEntity.class,
|
||||
PresenceEntity.class,
|
||||
|
@ -107,6 +108,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
|
|||
|
||||
public abstract BookmarkDao bookmarkDao();
|
||||
|
||||
public abstract ChatDao chatDao();
|
||||
|
||||
public abstract DiscoDao discoDao();
|
||||
|
||||
public abstract MessageDao messageDao();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -2,24 +2,38 @@ package im.conversations.android.database.dao;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
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 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.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
|
||||
public abstract class MessageDao {
|
||||
|
||||
@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)")
|
||||
abstract int acknowledge(long account, String messageId, final String bareTo);
|
||||
abstract int acknowledge(long account, String messageId, final String toBare);
|
||||
|
||||
@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"
|
||||
+ " accountId=:account)")
|
||||
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(
|
||||
final Account account, @NonNull final String messageId, @NonNull final Jid to) {
|
||||
|
@ -36,4 +50,87 @@ public abstract class MessageDao {
|
|||
> 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 don’t (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);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@ import androidx.room.Entity;
|
|||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import im.conversations.android.database.model.MessageContent;
|
||||
import im.conversations.android.database.model.PartType;
|
||||
|
||||
@Entity(
|
||||
tableName = "message_part",
|
||||
tableName = "message_content",
|
||||
foreignKeys =
|
||||
@ForeignKey(
|
||||
entity = MessageVersionEntity.class,
|
||||
|
@ -16,7 +17,7 @@ import im.conversations.android.database.model.PartType;
|
|||
childColumns = {"messageVersionId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
indices = {@Index(value = "messageVersionId")})
|
||||
public class MessagePartEntity {
|
||||
public class MessageContentEntity {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public Long id;
|
||||
|
@ -30,4 +31,15 @@ public class MessagePartEntity {
|
|||
public String body;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,31 @@
|
|||
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 eu.siacs.conversations.xmpp.Jid;
|
||||
import im.conversations.android.transformer.Transformation;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity(
|
||||
tableName = "message",
|
||||
foreignKeys =
|
||||
@ForeignKey(
|
||||
entity = ChatEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"chatId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
indices = {@Index(value = "chatId")})
|
||||
foreignKeys = {
|
||||
@ForeignKey(
|
||||
entity = ChatEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"chatId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
@ForeignKey(
|
||||
entity = MessageVersionEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"latestVersion"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
},
|
||||
indices = {@Index(value = "chatId"), @Index(value = "latestVersion")})
|
||||
public class MessageEntity {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -28,17 +38,36 @@ public class MessageEntity {
|
|||
|
||||
public boolean outgoing;
|
||||
|
||||
public String bareTo;
|
||||
public Jid toBare;
|
||||
public String toResource;
|
||||
public String bareFrom;
|
||||
public Jid fromBare;
|
||||
public String fromResource;
|
||||
|
||||
public String occupantId;
|
||||
|
||||
public String messageId;
|
||||
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)
|
||||
public String stanzaIdVerified;
|
||||
// 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)
|
||||
public boolean stanzaIdVerified;
|
||||
|
||||
@Nullable public Long latestVersion;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,21 @@ import androidx.room.Entity;
|
|||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import im.conversations.android.database.model.Modification;
|
||||
import im.conversations.android.transformer.Transformation;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity(
|
||||
tableName = "message_version",
|
||||
foreignKeys =
|
||||
@ForeignKey(
|
||||
entity = MessageEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"messageId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
indices = {@Index(value = "messageId")})
|
||||
foreignKeys = {
|
||||
@ForeignKey(
|
||||
entity = MessageEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"messageEntityId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
},
|
||||
indices = {@Index(value = "messageEntityId")})
|
||||
public class MessageVersionEntity {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -26,14 +29,28 @@ public class MessageVersionEntity {
|
|||
public String messageId;
|
||||
public String stanzaId;
|
||||
public Modification modification;
|
||||
public String modifiedBy;
|
||||
public Jid modifiedBy;
|
||||
public String modifiedByResource;
|
||||
public String occupantId;
|
||||
Instant receivedAt;
|
||||
public Instant receivedAt;
|
||||
|
||||
// the version order is determined by the receivedAt
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,20 @@
|
|||
package im.conversations.android.transformer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
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.Extension;
|
||||
import im.conversations.android.xmpp.model.axolotl.Encrypted;
|
||||
import im.conversations.android.xmpp.model.jabber.Body;
|
||||
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.stanza.Message;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
@ -18,10 +22,18 @@ import java.util.List;
|
|||
public class 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 from;
|
||||
public final Jid remote;
|
||||
public final Message.Type type;
|
||||
public final String messageId;
|
||||
public final String stanzaId;
|
||||
|
@ -31,15 +43,19 @@ public class Transformation {
|
|||
public final Collection<DeliveryReceiptRequest> deliveryReceiptRequests;
|
||||
|
||||
private Transformation(
|
||||
final Instant receivedAt,
|
||||
final Jid to,
|
||||
final Jid from,
|
||||
final Jid remote,
|
||||
final Message.Type type,
|
||||
final String messageId,
|
||||
final String stanzaId,
|
||||
final List<Extension> extensions,
|
||||
final Collection<DeliveryReceiptRequest> deliveryReceiptRequests) {
|
||||
this.receivedAt = receivedAt;
|
||||
this.to = to;
|
||||
this.from = from;
|
||||
this.remote = remote;
|
||||
this.type = type;
|
||||
this.messageId = messageId;
|
||||
this.stanzaId = stanzaId;
|
||||
|
@ -51,6 +67,32 @@ public class Transformation {
|
|||
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) {
|
||||
final var extension = Iterables.find(this.extensions, clazz::isInstance, null);
|
||||
return extension == null ? null : clazz.cast(extension);
|
||||
|
@ -61,7 +103,11 @@ public class Transformation {
|
|||
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 from = message.getFrom();
|
||||
final var type = message.getType();
|
||||
|
@ -72,6 +118,14 @@ public class Transformation {
|
|||
}
|
||||
final var requests = message.getExtensions(DeliveryReceiptRequest.class);
|
||||
return new Transformation(
|
||||
to, from, type, messageId, stanzaId, extensionListBuilder.build(), requests);
|
||||
receivedAt,
|
||||
to,
|
||||
from,
|
||||
remote,
|
||||
type,
|
||||
messageId,
|
||||
stanzaId,
|
||||
extensionListBuilder.build(),
|
||||
requests);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,29 @@
|
|||
package im.conversations.android.transformer;
|
||||
|
||||
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.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.correction.Replace;
|
||||
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 java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Transformer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Transformer.class);
|
||||
|
||||
private final Context context;
|
||||
private final Account account;
|
||||
|
||||
|
@ -16,23 +32,93 @@ public class Transformer {
|
|||
this.account = account;
|
||||
}
|
||||
|
||||
public boolean transform(final Transformation transformation) {
|
||||
final var database = ConversationsDatabase.getInstance(context);
|
||||
return database.runInTransaction(() -> transform(database, transformation));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transformation
|
||||
* @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
|
||||
* updated a status somewhere
|
||||
*/
|
||||
public boolean transform(final Transformation transformation) {
|
||||
final var encrypted = transformation.getExtension(Encrypted.class);
|
||||
final var bodies = transformation.getExtensions(Body.class);
|
||||
final var outOfBandData = transformation.getExtensions(OutOfBandData.class);
|
||||
private boolean transform(
|
||||
final ConversationsDatabase database, final Transformation transformation) {
|
||||
final var remote = transformation.remote;
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
44
src/main/java/im/conversations/android/xmpp/Timestamps.java
Normal file
44
src/main/java/im/conversations/android/xmpp/Timestamps.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,10 @@ package im.conversations.android.xmpp.manager;
|
|||
|
||||
import android.content.Context;
|
||||
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.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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -13,8 +14,11 @@ public class ArchiveManager extends AbstractManager {
|
|||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class);
|
||||
|
||||
private final TransformationFactory transformationFactory;
|
||||
|
||||
public ArchiveManager(Context context, XmppConnection connection) {
|
||||
super(context, connection);
|
||||
this.transformationFactory = new TransformationFactory(context, connection);
|
||||
}
|
||||
|
||||
public void handle(final Message message) {
|
||||
|
@ -24,14 +28,20 @@ public class ArchiveManager extends AbstractManager {
|
|||
final var stanzaId = result.getId();
|
||||
final var queryId = result.getQueryId();
|
||||
final var forwarded = result.getForwarded();
|
||||
final var forwardedMessage = forwarded == null ? null : forwarded.getMessage();
|
||||
if (forwardedMessage == null || queryId == null || stanzaId == null) {
|
||||
if (forwarded == null || queryId == null || stanzaId == null) {
|
||||
LOGGER.info("Received invalid MAM result from {} ", from);
|
||||
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
|
||||
|
||||
final var transformation = Transformation.of(forwardedMessage, stanzaId);
|
||||
final var transformation = this.transformationFactory.create(message, stanzaId, receivedAt);
|
||||
|
||||
// TODO create transformation; add transformation to Query.Transformer
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -9,4 +9,8 @@ public class Encrypted extends Extension {
|
|||
public Encrypted() {
|
||||
super(Encrypted.class);
|
||||
}
|
||||
|
||||
public boolean hasPayload() {
|
||||
return hasExtension(Payload.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,4 +9,8 @@ public class Body extends Extension {
|
|||
public Body() {
|
||||
super(Body.class);
|
||||
}
|
||||
|
||||
public String getLang() {
|
||||
return this.getAttribute("xml:lang");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.xmpp.model.Extension;
|
|
@ -1,5 +1,5 @@
|
|||
@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 im.conversations.android.annotation.XmlPackage;
|
|
@ -1,10 +1,10 @@
|
|||
package im.conversations.android.xmpp.model.markers;
|
||||
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import im.conversations.android.xmpp.model.DeliveryReceipt;
|
||||
|
||||
@XmlElement
|
||||
public class Received extends Extension {
|
||||
public class Received extends DeliveryReceipt {
|
||||
|
||||
public Received() {
|
||||
super(Received.class);
|
||||
|
@ -13,4 +13,8 @@ public class Received extends Extension {
|
|||
public void setId(String id) {
|
||||
this.setAttribute("id", id);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.getAttribute("id");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -1,10 +1,10 @@
|
|||
package im.conversations.android.xmpp.model.receipts;
|
||||
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import im.conversations.android.xmpp.model.DeliveryReceipt;
|
||||
|
||||
@XmlElement
|
||||
public class Received extends Extension {
|
||||
public class Received extends DeliveryReceipt {
|
||||
|
||||
public Received() {
|
||||
super(Received.class);
|
||||
|
@ -13,4 +13,8 @@ public class Received extends Extension {
|
|||
public void setId(String id) {
|
||||
this.setAttribute("id", id);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.getAttribute("id");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package im.conversations.android.xmpp.processor;
|
||||
|
||||
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.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.mam.Result;
|
||||
import im.conversations.android.xmpp.manager.ArchiveManager;
|
||||
import im.conversations.android.xmpp.manager.CarbonsManager;
|
||||
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.model.carbons.Received;
|
||||
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.stanza.Message;
|
||||
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 final Level level;
|
||||
private final TransformationFactory transformationFactory;
|
||||
|
||||
public MessageProcessor(final Context context, final XmppConnection connection) {
|
||||
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) {
|
||||
super(context, connection);
|
||||
this.level = level;
|
||||
this.transformationFactory = new TransformationFactory(context, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,10 +61,13 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
|
|||
return;
|
||||
}
|
||||
|
||||
// LOGGER.info("Message from {} with {}", message.getFrom(), message.getExtensionIds());
|
||||
|
||||
final var from = message.getFrom();
|
||||
|
||||
final var id = message.getId();
|
||||
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;
|
||||
if (transformation.isAnythingToTransform()) {
|
||||
final var transformer = new Transformer(context, getAccount());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue