fetch MAM messages

This commit is contained in:
Daniel Gultsch 2023-03-10 20:03:02 +01:00
parent bb2d077b7c
commit 58c5bd0f1b
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
37 changed files with 1122 additions and 77 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "1780dce1d6aca78c94a2c5c497d158c5", "identityHash": "cc15c6de66482506c7f895ccaff971b4",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -118,6 +118,86 @@
], ],
"foreignKeys": [] "foreignKeys": []
}, },
{
"tableName": "archive_page",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `archive` TEXT NOT NULL, `type` TEXT NOT NULL, `start` TEXT NOT NULL, `end` TEXT NOT NULL, `reachedMaxPages` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "archive",
"columnName": "archive",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "start",
"columnName": "start",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "end",
"columnName": "end",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "reachedMaxPages",
"columnName": "reachedMaxPages",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_archive_page_accountId_archive_type",
"unique": true,
"columnNames": [
"accountId",
"archive",
"type"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_archive_page_accountId_archive_type` ON `${TABLE_NAME}` (`accountId`, `archive`, `type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{ {
"tableName": "avatar_additional", "tableName": "avatar_additional",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `avatarId` INTEGER NOT NULL, `avatar_external_url` TEXT, `avatar_external_id` TEXT, `avatar_external_type` TEXT, `avatar_external_bytes` INTEGER NOT NULL, `avatar_external_height` INTEGER NOT NULL, `avatar_external_width` INTEGER NOT NULL, FOREIGN KEY(`avatarId`) REFERENCES `avatar`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `avatarId` INTEGER NOT NULL, `avatar_external_url` TEXT, `avatar_external_id` TEXT, `avatar_external_type` TEXT, `avatar_external_bytes` INTEGER NOT NULL, `avatar_external_height` INTEGER NOT NULL, `avatar_external_width` INTEGER NOT NULL, FOREIGN KEY(`avatarId`) REFERENCES `avatar`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
@ -1709,15 +1789,6 @@
] ]
}, },
"indices": [ "indices": [
{
"name": "index_message_chatId",
"unique": false,
"columnNames": [
"chatId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId` ON `${TABLE_NAME}` (`chatId`)"
},
{ {
"name": "index_message_latestVersion", "name": "index_message_latestVersion",
"unique": false, "unique": false,
@ -1735,6 +1806,16 @@
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_inReplyToMessageEntityId` ON `${TABLE_NAME}` (`inReplyToMessageEntityId`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_message_inReplyToMessageEntityId` ON `${TABLE_NAME}` (`inReplyToMessageEntityId`)"
},
{
"name": "index_message_chatId_receivedAt",
"unique": false,
"columnNames": [
"chatId",
"receivedAt"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId_receivedAt` ON `${TABLE_NAME}` (`chatId`, `receivedAt`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -2600,7 +2681,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, '1780dce1d6aca78c94a2c5c497d158c5')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cc15c6de66482506c7f895ccaff971b4')"
] ]
} }
} }

View file

@ -6,6 +6,7 @@ import androidx.room.Room;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
import androidx.room.TypeConverters; import androidx.room.TypeConverters;
import im.conversations.android.database.dao.AccountDao; import im.conversations.android.database.dao.AccountDao;
import im.conversations.android.database.dao.ArchiveDao;
import im.conversations.android.database.dao.AvatarDao; 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;
@ -19,6 +20,7 @@ import im.conversations.android.database.dao.PresenceDao;
import im.conversations.android.database.dao.RosterDao; import im.conversations.android.database.dao.RosterDao;
import im.conversations.android.database.dao.ServiceRecordDao; import im.conversations.android.database.dao.ServiceRecordDao;
import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.entity.ArchivePageEntity;
import im.conversations.android.database.entity.AvatarAdditionalEntity; import im.conversations.android.database.entity.AvatarAdditionalEntity;
import im.conversations.android.database.entity.AvatarEntity; import im.conversations.android.database.entity.AvatarEntity;
import im.conversations.android.database.entity.AxolotlDeviceListEntity; import im.conversations.android.database.entity.AxolotlDeviceListEntity;
@ -56,6 +58,7 @@ import im.conversations.android.database.entity.ServiceRecordCacheEntity;
@Database( @Database(
entities = { entities = {
AccountEntity.class, AccountEntity.class,
ArchivePageEntity.class,
AvatarAdditionalEntity.class, AvatarAdditionalEntity.class,
AvatarEntity.class, AvatarEntity.class,
AxolotlDeviceListEntity.class, AxolotlDeviceListEntity.class,
@ -114,6 +117,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
public abstract AccountDao accountDao(); public abstract AccountDao accountDao();
public abstract ArchiveDao archiveDao();
public abstract AvatarDao avatarDao(); public abstract AvatarDao avatarDao();
public abstract AxolotlDao axolotlDao(); public abstract AxolotlDao axolotlDao();

View file

@ -0,0 +1,174 @@
package im.conversations.android.database.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Upsert;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import im.conversations.android.database.entity.ArchivePageEntity;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.StanzaId;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.manager.ArchiveManager;
import java.util.List;
import java.util.Objects;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Dao
public abstract class ArchiveDao {
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveDao.class);
@Transaction
public List<Range> resetLivePage(final Account account, final Jid archive) {
final var page =
getPage(
account.id,
archive,
ArchivePageEntity.Type.START,
ArchivePageEntity.Type.MIDDLE);
final var livePage = getPage(account.id, archive, ArchivePageEntity.Type.LIVE);
if (page == null && livePage == null) {
LOGGER.info("Emitting initial query for {}", archive);
return ImmutableList.of(new Range(Range.Order.REVERSE, null));
}
final ImmutableList.Builder<Range> queryRangeBuilder = new ImmutableList.Builder<>();
final boolean gapLess = page != null && livePage != null && page.end.equals(livePage.start);
if (gapLess) {
LOGGER.info("Page and live page for {} were gap-less", archive);
page.end = livePage.end;
insert(page);
if (page.type != ArchivePageEntity.Type.START && !page.reachedMaxPages) {
queryRangeBuilder.add(new Range(Range.Order.REVERSE, page.start));
}
queryRangeBuilder.add(new Range(Range.Order.NORMAL, livePage.end));
} else if (page != null) {
LOGGER.info("Ignoring live page for {}", archive);
// this will simply ignore the last live page and overwrite it
if (page.type != ArchivePageEntity.Type.START && !page.reachedMaxPages) {
queryRangeBuilder.add(new Range(Range.Order.REVERSE, page.start));
}
queryRangeBuilder.add(new Range(Range.Order.NORMAL, page.end));
} else {
LOGGER.info("Converting live page into regular page for {}", archive);
insert(
ArchivePageEntity.of(
account,
archive,
ArchivePageEntity.Type.MIDDLE,
livePage.start,
livePage.end,
false));
queryRangeBuilder.add(new Range(Range.Order.REVERSE, livePage.start));
queryRangeBuilder.add(new Range(Range.Order.NORMAL, livePage.end));
}
if (livePage != null) {
delete(livePage);
}
return queryRangeBuilder.build();
}
public void submitPage(
final Account account,
final Jid archive,
final Range range,
final ArchiveManager.QueryResult queryResult,
final boolean reachedMaxPagesReversing) {
if (reachedMaxPagesReversing) {
Preconditions.checkState(
range.order == Range.Order.REVERSE,
"reachedMaxPagesReversing can only be true when reversing");
}
final var isComplete = queryResult.isComplete;
final var page = queryResult.page;
final var existingPage =
getPage(
account.id,
archive,
ArchivePageEntity.Type.START,
ArchivePageEntity.Type.MIDDLE);
final boolean isStart = range.order == Range.Order.REVERSE && isComplete;
if (existingPage == null) {
insert(
ArchivePageEntity.of(
account,
archive,
isStart ? ArchivePageEntity.Type.START : ArchivePageEntity.Type.MIDDLE,
page.first,
page.last,
reachedMaxPagesReversing));
} else {
if (range.order == Range.Order.REVERSE) {
Preconditions.checkState(
Objects.equals(range.id, existingPage.start),
"Reversing range did not match start of existing page");
existingPage.start = page.first;
existingPage.type =
isStart ? ArchivePageEntity.Type.START : ArchivePageEntity.Type.MIDDLE;
} else if (range.order == Range.Order.NORMAL) {
Preconditions.checkState(
Objects.equals(range.id, existingPage.end),
"Normal range did not match end of existing page");
existingPage.end = page.last;
} else {
throw new IllegalStateException(String.format("Unknown order %s", range.order));
}
existingPage.reachedMaxPages = existingPage.reachedMaxPages || reachedMaxPagesReversing;
insert(existingPage);
}
final boolean lastIsLive =
(range.order == Range.Order.REVERSE && range.id == null)
|| (range.order == Range.Order.NORMAL && queryResult.isComplete);
if (lastIsLive) {
final var existingLivePage = getPage(account.id, archive, ArchivePageEntity.Type.LIVE);
if (existingLivePage != null) {
existingLivePage.start = page.last;
} else {
insert(
ArchivePageEntity.of(
account,
archive,
ArchivePageEntity.Type.LIVE,
page.last,
page.last,
false));
}
}
}
public void setLivePageStanzaId(final Account account, final StanzaId stanzaId) {
LOGGER.info("set live page stanza id {}", stanzaId);
final var currentLivePage = getPage(account.id, stanzaId.by, ArchivePageEntity.Type.LIVE);
if (currentLivePage != null) {
currentLivePage.end = stanzaId.id;
insert(currentLivePage);
} else {
insert(
ArchivePageEntity.of(
account,
stanzaId.by,
ArchivePageEntity.Type.LIVE,
stanzaId.id,
stanzaId.id,
false));
}
}
@Delete
protected abstract void delete(final ArchivePageEntity entity);
@Upsert
protected abstract void insert(final ArchivePageEntity entity);
@Query(
"SELECT * FROM archive_page WHERE accountId=:account AND archive=:archive AND type"
+ " IN(:type)")
protected abstract ArchivePageEntity getPage(
long account, Jid archive, ArchivePageEntity.Type... type);
}

View file

@ -197,24 +197,28 @@ public abstract class ChatDao {
"SELECT c.id,c.accountId,c.address,c.type,m.sentAt,m.outgoing,m.latestVersion as" "SELECT c.id,c.accountId,c.address,c.type,m.sentAt,m.outgoing,m.latestVersion as"
+ " version,m.toBare,m.toResource,m.fromBare,m.fromResource,(SELECT count(id) FROM" + " version,m.toBare,m.toResource,m.fromBare,m.fromResource,(SELECT count(id) FROM"
+ " message WHERE chatId=c.id) as unread,(SELECT name FROM roster WHERE" + " message WHERE chatId=c.id) as unread,(SELECT name FROM roster WHERE"
+ " roster.address=c.address) as rosterName,(SELECT nick FROM nick WHERE" + " roster.accountId=c.accountId AND roster.address=c.address) as"
+ " rosterName,(SELECT nick FROM nick WHERE nick.accountId=c.accountId AND"
+ " nick.address=c.address) as nick,(SELECT identity.name FROM disco_item JOIN" + " nick.address=c.address) as nick,(SELECT identity.name FROM disco_item JOIN"
+ " disco_identity identity ON disco_item.discoId=identity.discoId WHERE" + " disco_identity identity ON disco_item.discoId=identity.discoId WHERE"
+ " disco_item.address=c.address LIMIT 1) as discoIdentityName,(SELECT name FROM" + " disco_item.accountId=c.accountId AND disco_item.address=c.address LIMIT 1) as"
+ " bookmark WHERE bookmark.address=c.address) as bookmarkName,(CASE WHEN" + " discoIdentityName,(SELECT name FROM bookmark WHERE"
+ " c.type='MUC' THEN (SELECT vCardPhoto FROM presence WHERE address=c.address AND" + " bookmark.accountId=c.accountId AND bookmark.address=c.address) as"
+ " resource='') WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence" + " bookmarkName,(CASE WHEN c.type='MUC' THEN (SELECT vCardPhoto FROM presence"
+ " WHERE address=c.address AND vCardPhoto NOT NULL LIMIT 1) ELSE NULL END) as" + " WHERE presence.accountId=c.accountId AND address=c.address AND resource='')"
+ " vCardPhoto,(SELECT thumb_id FROM avatar WHERE avatar.address=c.address) as" + " WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence WHERE"
+ " avatar FROM CHAT c LEFT JOIN message m ON (c.id=m.chatId) LEFT OUTER JOIN" + " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)"
+ " message m2 ON (c.id = m2.chatId AND (m.receivedAt < m2.receivedAt OR" + " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE"
+ " (m.receivedAt = m2.receivedAt AND m.id < m2.id))) WHERE (:accountId IS NULL OR" + " avatar.accountId=c.accountId AND avatar.address=c.address) as avatar FROM CHAT"
+ " c LEFT JOIN message m ON (m.id = (SELECT id FROM message WHERE chatId=c.id"
+ " ORDER by receivedAt DESC LIMIT 1)) WHERE (:accountId IS NULL OR"
+ " c.accountId=:accountId) AND (:groupId IS NULL OR (c.address IN(SELECT" + " c.accountId=:accountId) AND (:groupId IS NULL OR (c.address IN(SELECT"
+ " roster.address FROM roster JOIN roster_group ON" + " roster.address FROM roster JOIN roster_group ON"
+ " roster.id=roster_group.rosterItemId WHERE roster_group.groupId=:groupId) OR" + " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND"
+ " c.address IN(SELECT address FROM bookmark JOIN bookmark_group ON" + " roster_group.groupId=:groupId) OR c.address IN(SELECT address FROM bookmark"
+ " bookmark.id=bookmark_group.bookmarkId WHERE bookmark_group.groupId=:groupId)))" + " JOIN bookmark_group ON bookmark.id=bookmark_group.bookmarkId WHERE"
+ " AND c.archived=0 AND m2.id IS NULL ORDER by m.receivedAt DESC") + " bookmark.accountId=c.accountId AND bookmark_group.groupId=:groupId))) AND"
+ " c.archived=0 ORDER by m.receivedAt DESC")
public abstract PagingSource<Integer, ChatOverviewItem> getChatOverview( public abstract PagingSource<Integer, ChatOverviewItem> getChatOverview(
final Long accountId, final Long groupId); final Long accountId, final Long groupId);

View file

@ -454,14 +454,14 @@ public abstract class MessageDao {
+ " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId" + " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId"
+ " WHERE id=:id") + " WHERE id=:id")
protected abstract void setInReplyToStanzaId( protected abstract void setInReplyToStanzaId(
final long id, String stanzaId, long inReplyToMessageEntityId); final long id, String stanzaId, Long inReplyToMessageEntityId);
@Query( @Query(
"UPDATE message SET" "UPDATE message SET"
+ " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId" + " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId"
+ " WHERE id=:id") + " WHERE id=:id")
protected abstract void setInReplyToMessageId( protected abstract void setInReplyToMessageId(
final long id, String messageId, long inReplyToMessageEntityId); final long id, String messageId, Long inReplyToMessageEntityId);
@Query( @Query(
"SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND" "SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND"

View file

@ -0,0 +1,63 @@
package im.conversations.android.database.entity;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import im.conversations.android.database.model.Account;
import org.jxmpp.jid.Jid;
@Entity(
tableName = "archive_page",
foreignKeys =
@ForeignKey(
entity = AccountEntity.class,
parentColumns = {"id"},
childColumns = {"accountId"},
onDelete = ForeignKey.CASCADE),
indices = {
@Index(
value = {"accountId", "archive", "type"},
unique = true)
})
public class ArchivePageEntity {
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull public Long accountId;
@NonNull public Jid archive;
@NonNull public Type type;
@NonNull public String start;
@NonNull public String end;
public boolean reachedMaxPages;
public static ArchivePageEntity of(
final Account account,
final Jid archive,
final Type type,
final String start,
final String end,
final boolean reachedMaxPages) {
final var entity = new ArchivePageEntity();
entity.accountId = account.id;
entity.archive = archive;
entity.type = type;
entity.start = start;
entity.end = end;
entity.reachedMaxPages = reachedMaxPages;
return entity;
}
public enum Type {
START,
MIDDLE,
LIVE
}
}

View file

@ -32,9 +32,9 @@ import org.jxmpp.jid.parts.Resourcepart;
onDelete = ForeignKey.SET_NULL), onDelete = ForeignKey.SET_NULL),
}, },
indices = { indices = {
@Index(value = "chatId"),
@Index(value = "latestVersion"), @Index(value = "latestVersion"),
@Index("inReplyToMessageEntityId") @Index("inReplyToMessageEntityId"),
@Index(value = {"chatId", "receivedAt"}),
}) })
public class MessageEntity { public class MessageEntity {

View file

@ -1,5 +1,7 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -25,4 +27,13 @@ public class AddressWithName {
public int hashCode() { public int hashCode() {
return Objects.hashCode(address, name); return Objects.hashCode(address, name);
} }
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("address", address)
.add("name", name)
.toString();
}
} }

View file

@ -1,5 +1,7 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
public class AvatarWithAccount { public class AvatarWithAccount {
@ -10,6 +12,17 @@ public class AvatarWithAccount {
public final AvatarType avatarType; public final AvatarType avatarType;
public final String hash; public final String hash;
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("account", account)
.add("addressWithName", addressWithName)
.add("avatarType", avatarType)
.add("hash", hash)
.toString();
}
public AvatarWithAccount( public AvatarWithAccount(
long account, long account,
final AddressWithName addressWithName, final AddressWithName addressWithName,

View file

@ -1,6 +1,7 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import androidx.room.Relation; import androidx.room.Relation;
import com.google.common.base.Objects;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import im.conversations.android.database.entity.MessageContentEntity; import im.conversations.android.database.entity.MessageContentEntity;
import java.time.Instant; import java.time.Instant;
@ -131,6 +132,56 @@ public class ChatOverviewItem {
return value != null && !value.trim().isEmpty(); return value != null && !value.trim().isEmpty();
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChatOverviewItem that = (ChatOverviewItem) o;
return id == that.id
&& accountId == that.accountId
&& outgoing == that.outgoing
&& version == that.version
&& unread == that.unread
&& Objects.equal(address, that.address)
&& type == that.type
&& Objects.equal(sentAt, that.sentAt)
&& Objects.equal(toBare, that.toBare)
&& Objects.equal(toResource, that.toResource)
&& Objects.equal(fromBare, that.fromBare)
&& Objects.equal(fromResource, that.fromResource)
&& Objects.equal(rosterName, that.rosterName)
&& Objects.equal(nick, that.nick)
&& Objects.equal(discoIdentityName, that.discoIdentityName)
&& Objects.equal(bookmarkName, that.bookmarkName)
&& Objects.equal(vCardPhoto, that.vCardPhoto)
&& Objects.equal(avatar, that.avatar)
&& Objects.equal(contents, that.contents);
}
@Override
public int hashCode() {
return Objects.hashCode(
id,
accountId,
address,
type,
sentAt,
outgoing,
toBare,
toResource,
fromBare,
fromResource,
version,
rosterName,
nick,
discoIdentityName,
bookmarkName,
vCardPhoto,
avatar,
unread,
contents);
}
public sealed interface Sender permits SenderYou, SenderName {} public sealed interface Sender permits SenderYou, SenderName {}
public static final class SenderYou implements Sender {} public static final class SenderYou implements Sender {}

View file

@ -1,5 +1,7 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import com.google.common.base.Objects;
public class MessageContent { public class MessageContent {
public final String language; public final String language;
@ -24,4 +26,20 @@ public class MessageContent {
public static MessageContent file(final String url) { public static MessageContent file(final String url) {
return new MessageContent(null, PartType.FILE, null, url); return new MessageContent(null, PartType.FILE, null, url);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MessageContent that = (MessageContent) o;
return Objects.equal(language, that.language)
&& type == that.type
&& Objects.equal(body, that.body)
&& Objects.equal(url, that.url);
}
@Override
public int hashCode() {
return Objects.hashCode(language, type, body, url);
}
} }

View file

@ -0,0 +1,23 @@
package im.conversations.android.database.model;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import org.jxmpp.jid.Jid;
public class StanzaId {
public final String id;
public final Jid by;
public StanzaId(String id, Jid by) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(by);
this.id = id;
this.by = by;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("id", id).add("by", by).toString();
}
}

View file

@ -1,6 +1,7 @@
package im.conversations.android.transformer; package im.conversations.android.transformer;
import android.content.Context; import android.content.Context;
import im.conversations.android.database.model.StanzaId;
import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
@ -17,8 +18,8 @@ public class TransformationFactory extends XmppConnection.Delegate {
super(context, connection); super(context, connection);
} }
public MessageTransformation create(final Message message, final String stanzaId) { public MessageTransformation create(final Message message, final StanzaId stanzaId) {
return create(message, stanzaId, Instant.now()); return create(message, stanzaId == null ? null : stanzaId.id, Instant.now());
} }
public MessageTransformation create( public MessageTransformation create(
@ -49,7 +50,7 @@ public class TransformationFactory extends XmppConnection.Delegate {
if (message.getType() == Message.Type.GROUPCHAT) { if (message.getType() == Message.Type.GROUPCHAT) {
senderIdentity = null; // TODO discover real jid senderIdentity = null; // TODO discover real jid
} else { } else {
senderIdentity = from == null ? null : from.asBareJid(); senderIdentity = from == null ? boundAddress : from.asBareJid();
} }
return MessageTransformation.of( return MessageTransformation.of(
message, receivedAt, remote, stanzaId, senderIdentity, occupantId); message, receivedAt, remote, stanzaId, senderIdentity, occupantId);

View file

@ -1,7 +1,9 @@
package im.conversations.android.transformer; package im.conversations.android.transformer;
import android.content.Context; import android.content.Context;
import androidx.annotation.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import im.conversations.android.axolotl.AxolotlDecryptionException; import im.conversations.android.axolotl.AxolotlDecryptionException;
import im.conversations.android.axolotl.AxolotlService; import im.conversations.android.axolotl.AxolotlService;
import im.conversations.android.database.ConversationsDatabase; import im.conversations.android.database.ConversationsDatabase;
@ -10,6 +12,9 @@ import im.conversations.android.database.model.ChatIdentifier;
import im.conversations.android.database.model.MessageIdentifier; import im.conversations.android.database.model.MessageIdentifier;
import im.conversations.android.database.model.MessageState; import im.conversations.android.database.model.MessageState;
import im.conversations.android.database.model.Modification; import im.conversations.android.database.model.Modification;
import im.conversations.android.database.model.StanzaId;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.manager.ArchiveManager;
import im.conversations.android.xmpp.model.DeliveryReceipt; 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.correction.Replace;
@ -21,6 +26,7 @@ import im.conversations.android.xmpp.model.retract.Retract;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -53,15 +59,45 @@ public class Transformer {
this.axolotlService = axolotlService; this.axolotlService = axolotlService;
} }
@VisibleForTesting
public boolean transform(final MessageTransformation transformation) { public boolean transform(final MessageTransformation transformation) {
return this.transform(transformation, null);
}
public boolean transform(final MessageTransformation transformation, final StanzaId stanzaId) {
return database.runInTransaction( return database.runInTransaction(
() -> { () -> {
final var sendDeliveryReceipts = transform(database, transformation); final var sendDeliveryReceipts = transform(database, transformation);
axolotlService.executePostDecryptionHook(); axolotlService.executePostDecryptionHook();
if (stanzaId != null) {
database.archiveDao().setLivePageStanzaId(account, stanzaId);
}
return sendDeliveryReceipts; return sendDeliveryReceipts;
}); });
} }
public void transform(
ImmutableList<MessageTransformation> messageTransformations,
final Jid archive,
Range queryRange,
ArchiveManager.QueryResult queryResult,
final boolean reachedMaxPagesReversing) {
database.runInTransaction(
() -> {
for (final MessageTransformation transformation : messageTransformations) {
transform(database, transformation);
}
database.archiveDao()
.submitPage(
account,
archive,
queryRange,
queryResult,
reachedMaxPagesReversing);
axolotlService.executePostDecryptionHook();
});
}
/** /**
* @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

View file

@ -20,6 +20,10 @@ public class ChatOverviewComparator extends DiffUtil.ItemCallback<ChatOverviewIt
@Override @Override
public boolean areContentsTheSame( public boolean areContentsTheSame(
@NonNull ChatOverviewItem oldItem, @NonNull ChatOverviewItem newItem) { @NonNull ChatOverviewItem oldItem, @NonNull ChatOverviewItem newItem) {
return false; final boolean areContentsTheSame = oldItem.equals(newItem);
if (!areContentsTheSame) {
LOGGER.info("chat {} got modified", oldItem.id);
}
return areContentsTheSame;
} }
} }

View file

@ -88,6 +88,7 @@ public final class Namespace {
public static final String REGISTER = "jabber:iq:register"; public static final String REGISTER = "jabber:iq:register";
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register"; public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
public static final String REPLY = "urn:xmpp:reply:0"; public static final String REPLY = "urn:xmpp:reply:0";
public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
public static final String RETRACT = "urn:xmpp:message-retract:0"; public static final String RETRACT = "urn:xmpp:message-retract:0";
public static final String ROSTER = "jabber:iq:roster"; public static final String ROSTER = "jabber:iq:roster";
public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl";

View file

@ -0,0 +1,31 @@
package im.conversations.android.xmpp;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
public class Page {
public final String first;
public final String last;
public final Integer count;
public Page(String first, String last, Integer count) {
this.first = first;
this.last = last;
this.count = count;
}
public static Page emptyWithCount(final String id, final Integer count) {
return new Page(id, id, count);
}
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("first", first)
.add("last", last)
.add("count", count)
.toString();
}
}

View file

@ -0,0 +1,26 @@
package im.conversations.android.xmpp;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
public class Range {
public final Order order;
public final String id;
public Range(final Order order, final String id) {
this.order = order;
this.id = id;
}
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString();
}
public enum Order {
NORMAL,
REVERSE
}
}

View file

@ -1,21 +1,50 @@
package im.conversations.android.xmpp.manager; package im.conversations.android.xmpp.manager;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import im.conversations.android.IDs;
import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.transformer.MessageTransformation;
import im.conversations.android.transformer.TransformationFactory; import im.conversations.android.transformer.TransformationFactory;
import im.conversations.android.transformer.Transformer;
import im.conversations.android.xmpp.Page;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.delay.Delay; import im.conversations.android.xmpp.model.delay.Delay;
import im.conversations.android.xmpp.model.mam.Fin;
import im.conversations.android.xmpp.model.mam.Query;
import im.conversations.android.xmpp.model.mam.Result; import im.conversations.android.xmpp.model.mam.Result;
import im.conversations.android.xmpp.model.rsm.Set;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class ArchiveManager extends AbstractManager { public class ArchiveManager extends AbstractManager {
private static final int MAX_ITEMS_PER_PAGE = 50;
private static final int MAX_PAGES_REVERSING = 20;
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class); private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class);
private final TransformationFactory transformationFactory; private final TransformationFactory transformationFactory;
private final Map<QueryId, RunningQuery> runningQueryMap = new HashMap<>();
public ArchiveManager(Context context, XmppConnection connection) { public ArchiveManager(Context context, XmppConnection connection) {
super(context, connection); super(context, connection);
this.transformationFactory = new TransformationFactory(context, connection); this.transformationFactory = new TransformationFactory(context, connection);
@ -26,9 +55,9 @@ public class ArchiveManager extends AbstractManager {
Preconditions.checkArgument(result != null, "The message needs to contain a MAM result"); Preconditions.checkArgument(result != null, "The message needs to contain a MAM result");
final var from = message.getFrom(); final var from = message.getFrom();
final var stanzaId = result.getId(); final var stanzaId = result.getId();
final var queryId = result.getQueryId(); final var id = result.getQueryId();
final var forwarded = result.getForwarded(); final var forwarded = result.getForwarded();
if (forwarded == null || queryId == null || stanzaId == null) { if (forwarded == null || id == null || stanzaId == null) {
LOGGER.info("Received invalid MAM result from {} ", from); LOGGER.info("Received invalid MAM result from {} ", from);
return; return;
} }
@ -39,10 +68,263 @@ public class ArchiveManager extends AbstractManager {
LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from); LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from);
return; return;
} }
// TODO get query based on queryId and from final Jid archive = from == null ? connection.getBoundAddress().asBareJid() : from;
final RunningQuery runningQuery;
synchronized (this.runningQueryMap) {
runningQuery = this.runningQueryMap.get(new QueryId(archive, id));
}
if (runningQuery == null) {
LOGGER.info("Did not find running query for {}/{}", archive, id);
return;
}
final var transformation = this.transformationFactory.create(message, stanzaId, receivedAt); final var transformation =
this.transformationFactory.create(forwardedMessage, stanzaId, receivedAt);
// TODO only when there is something to transform
runningQuery.addTransformation(transformation);
}
// TODO create transformation; add transformation to Query.Transformer private ListenableFuture<Metadata> fetchMetadata(final Jid archive) {
final var iq = new Iq(Iq.Type.GET);
iq.setTo(archive);
iq.addExtension(new im.conversations.android.xmpp.model.mam.Metadata());
final var metadataFuture = connection.sendIqPacket(iq);
return Futures.transform(
metadataFuture,
result -> {
final var metadata =
result.getExtension(
im.conversations.android.xmpp.model.mam.Metadata.class);
if (metadata == null) {
throw new IllegalStateException("result did not contain metadata");
}
final var start = metadata.getStart();
final var end = metadata.getEnd();
if (start == null && end == null) {
return new Metadata(null, null);
}
final var startId = start == null ? null : start.getId();
final var endId = end == null ? null : end.getId();
if (Strings.isNullOrEmpty(startId) || Strings.isNullOrEmpty(endId)) {
throw new IllegalStateException("metadata had empty start or end id");
}
return new Metadata(startId, endId);
},
MoreExecutors.directExecutor());
}
public void query(final Jid archive, final List<Range> queryRanges) {
final var future = queryAsFuture(archive, queryRanges);
Futures.addCallback(
future,
new FutureCallback<>() {
@Override
public void onSuccess(List<Stats> stats) {
LOGGER.info("Successfully queried {} {}", archive, stats);
}
@Override
public void onFailure(@NonNull Throwable throwable) {
LOGGER.warn("Something went wrong querying {}", archive, throwable);
}
},
MoreExecutors.directExecutor());
}
private ListenableFuture<List<Stats>> queryAsFuture(
final Jid archive, final List<Range> queryRanges) {
final var queryFutures = Lists.transform(queryRanges, qr -> queryAsFuture(archive, qr));
return Futures.allAsList(queryFutures);
}
private ListenableFuture<Stats> queryAsFuture(final Jid archive, final Range queryRange) {
return queryAsFuture(archive, queryRange, Stats.begin());
}
private ListenableFuture<Stats> queryAsFuture(
final Jid archive, final Range queryRange, final Stats stats) {
final var queryId = new QueryId(archive, IDs.medium());
final var runningQuery = new RunningQuery(queryRange);
final var iq = new Iq(Iq.Type.SET);
iq.setTo(archive);
final var query = iq.addExtension(new Query());
query.setQueryId(queryId.id);
query.addExtension(Set.of(queryRange, MAX_ITEMS_PER_PAGE));
synchronized (this.runningQueryMap) {
this.runningQueryMap.put(queryId, runningQuery);
}
final var queryResultFuture = connection.sendIqPacket(iq);
return Futures.transformAsync(
queryResultFuture,
result -> {
final var fin = result.getExtension(Fin.class);
if (fin == null) {
throw new IllegalStateException("Iq response is missing fin element");
}
final var set = fin.getExtension(Set.class);
if (set == null) {
throw new IllegalStateException("Fin element is missing set element");
}
final QueryResult queryResult;
if (set.isEmpty()) {
// we fake an empty page here because on catch up queries we the live page
// to be properly reconfigured
queryResult =
new QueryResult(
true, Page.emptyWithCount(queryRange.id, set.getCount()));
} else {
queryResult = new QueryResult(fin.isComplete(), set.asPage());
}
return processQueryResponse(queryId, queryResult, stats);
},
MoreExecutors.directExecutor());
}
private ListenableFuture<Stats> processQueryResponse(
final QueryId queryId, final QueryResult queryResult, final Stats existingStats) {
final RunningQuery runningQuery;
synchronized (this.runningQueryMap) {
runningQuery = this.runningQueryMap.remove(queryId);
}
if (runningQuery == null) {
return Futures.immediateFailedFuture(
new IllegalStateException(
String.format(
"Could not find running query for %s/%s",
queryId.archive, queryId.id)));
}
final var messageTransformations = runningQuery.transformationBuilder.build();
final var stats = existingStats.countPage(messageTransformations.size());
final boolean reachedMaxPagesReversing =
runningQuery.queryRange.order == Range.Order.REVERSE
&& stats.pages >= MAX_PAGES_REVERSING;
final var database = ConversationsDatabase.getInstance(context);
final var axolotlService = connection.getManager(AxolotlManager.class).getAxolotlService();
final var transformer = new Transformer(getAccount(), database, axolotlService);
transformer.transform(
messageTransformations,
queryId.archive,
runningQuery.queryRange,
queryResult,
reachedMaxPagesReversing);
if (queryResult.isComplete || reachedMaxPagesReversing) {
return Futures.immediateFuture(stats);
} else {
final Range range = queryResult.nextPage(runningQuery.queryRange.order);
return queryAsFuture(queryId.archive, range, stats);
}
}
public static final class Metadata {
public final String start;
public final String end;
public Metadata(String start, String end) {
this.start = start;
this.end = end;
}
@Override
@NonNull
public String toString() {
return MoreObjects.toStringHelper(this).add("start", start).add("end", end).toString();
}
}
public final class QueryResult {
public final boolean isComplete;
public final Page page;
public QueryResult(boolean isComplete, Page page) {
this.isComplete = isComplete;
this.page = page;
}
public Range nextPage(final Range.Order order) {
if (isComplete) {
throw new IllegalStateException("Query was complete. There is no next page");
}
if (order == Range.Order.NORMAL) {
return new Range(Range.Order.NORMAL, page.last);
} else if (order == Range.Order.REVERSE) {
return new Range(Range.Order.REVERSE, page.first);
} else {
throw new IllegalStateException("Unknown order");
}
}
@NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("isComplete", isComplete)
.add("page", page)
.toString();
}
}
public static final class RunningQuery {
public final Range queryRange;
private final ImmutableList.Builder<MessageTransformation> transformationBuilder =
new ImmutableList.Builder<>();
public RunningQuery(final Range queryRange) {
this.queryRange = queryRange;
}
public void addTransformation(final MessageTransformation messageTransformation) {
this.transformationBuilder.add(messageTransformation);
}
}
public static final class QueryId {
public final Jid archive;
public final String id;
public QueryId(Jid archive, String id) {
this.archive = archive;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueryId queryId = (QueryId) o;
return Objects.equal(archive, queryId.archive) && Objects.equal(id, queryId.id);
}
@Override
public int hashCode() {
return Objects.hashCode(archive, id);
}
}
public static class Stats {
public final int pages;
public final int transformations;
private Stats(int pages, int transformations) {
this.pages = pages;
this.transformations = transformations;
}
public static Stats begin() {
return new Stats(0, 0);
}
public Stats countPage(final int transformations) {
return new Stats(this.pages + 1, this.transformations + transformations);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("pages", pages)
.add("transformations", transformations)
.toString();
}
} }
} }

View file

@ -243,9 +243,9 @@ public class JingleConnectionManager extends AbstractManager {
public void handle(final Message message) { public void handle(final Message message) {
final String id = message.getId(); final String id = message.getId();
final String stanzaId = getManager(StanzaIdManager.class).getStanzaId(message); final var stanzaId = getManager(StanzaIdManager.class).getStanzaId(message);
final JingleMessage jingleMessage = message.getExtension(JingleMessage.class); final JingleMessage jingleMessage = message.getExtension(JingleMessage.class);
this.deliverMessage(message.getTo(), message.getFrom(), jingleMessage, id, stanzaId); this.deliverMessage(message.getTo(), message.getFrom(), jingleMessage, id, stanzaId.id);
} }
private void deliverMessage( private void deliverMessage(

View file

@ -14,6 +14,7 @@ import im.conversations.android.database.model.MucWithNick;
import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.IqErrorException;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.error.Condition;
@ -65,7 +66,12 @@ public class MultiUserChatManager extends AbstractManager {
private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) { private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) {
if (infoQuery.hasFeature(Namespace.MUC) if (infoQuery.hasFeature(Namespace.MUC)
&& infoQuery.hasIdentityWithCategory("conference")) { && infoQuery.hasIdentityWithCategory("conference")) {
// TODO check if server has MAM support
final var archive = mucWithNick.address;
final List<Range> queryRanges =
getDatabase().archiveDao().resetLivePage(getAccount(), archive);
sendJoinPresence(mucWithNick); sendJoinPresence(mucWithNick);
getManager(ArchiveManager.class).query(archive, queryRanges);
} else { } else {
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC); getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
} }

View file

@ -1,11 +1,11 @@
package im.conversations.android.xmpp.manager; package im.conversations.android.xmpp.manager;
import android.content.Context; import android.content.Context;
import im.conversations.android.database.model.StanzaId;
import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import im.conversations.android.xmpp.model.unique.StanzaId;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
public class StanzaIdManager extends AbstractManager { public class StanzaIdManager extends AbstractManager {
@ -14,7 +14,7 @@ public class StanzaIdManager extends AbstractManager {
super(context, connection); super(context, connection);
} }
public String getStanzaId(final Message message) { public StanzaId getStanzaId(final Message message) {
final Jid by; final Jid by;
if (message.getType() == Message.Type.GROUPCHAT) { if (message.getType() == Message.Type.GROUPCHAT) {
final var from = message.getFrom(); final var from = message.getFrom();
@ -25,7 +25,7 @@ public class StanzaIdManager extends AbstractManager {
} else { } else {
by = connection.getBoundAddress().asBareJid(); by = connection.getBoundAddress().asBareJid();
} }
if (message.hasExtension(StanzaId.class) if (message.hasExtension(im.conversations.android.xmpp.model.unique.StanzaId.class)
&& getManager(DiscoManager.class) && getManager(DiscoManager.class)
.hasFeature(Entity.discoItem(by), Namespace.STANZA_IDS)) { .hasFeature(Entity.discoItem(by), Namespace.STANZA_IDS)) {
return getStanzaIdBy(message, by); return getStanzaIdBy(message, by);
@ -34,11 +34,12 @@ public class StanzaIdManager extends AbstractManager {
} }
} }
private static String getStanzaIdBy(final Message message, final Jid by) { private static StanzaId getStanzaIdBy(final Message message, final Jid by) {
for (final StanzaId stanzaId : message.getExtensions(StanzaId.class)) { for (final var stanzaId :
message.getExtensions(im.conversations.android.xmpp.model.unique.StanzaId.class)) {
final var id = stanzaId.getId(); final var id = stanzaId.getId();
if (by.equals(stanzaId.getBy()) && id != null) { if (by.equals(stanzaId.getBy()) && id != null) {
return id; return new StanzaId(id, by);
} }
} }
return null; return null;

View file

@ -0,0 +1,15 @@
package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class End extends Extension {
public End() {
super(End.class);
}
public String getId() {
return this.getAttribute("id");
}
}

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Fin extends Extension {
public Fin() {
super(Fin.class);
}
public boolean isComplete() {
return this.getAttributeAsBoolean("complete");
}
}

View file

@ -0,0 +1,20 @@
package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Metadata extends Extension {
public Metadata() {
super(Metadata.class);
}
public Start getStart() {
return this.getExtension(Start.class);
}
public End getEnd() {
return this.getExtension(End.class);
}
}

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Query extends Extension {
public Query() {
super(Query.class);
}
public void setQueryId(final String id) {
this.setAttribute("queryid", id);
}
}

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.mam;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Start extends Extension {
public Start() {
super(Start.class);
}
public String getId() {
return this.getAttribute("id");
}
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class After extends Extension {
public After() {
super(After.class);
}
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Before extends Extension {
public Before() {
super(Before.class);
}
}

View file

@ -0,0 +1,23 @@
package im.conversations.android.xmpp.model.rsm;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Count extends Extension {
public Count() {
super(Count.class);
}
public Integer getCount() {
final var content = getContent();
if (Strings.isNullOrEmpty(content)) {
return null;
} else {
return Ints.tryParse(content);
}
}
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class First extends Extension {
public First() {
super(First.class);
}
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Last extends Extension {
public Last() {
super(Last.class);
}
}

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Max extends Extension {
public Max() {
super(Max.class);
}
public void setMax(final int max) {
this.setContent(String.valueOf(max));
}
}

View file

@ -0,0 +1,55 @@
package im.conversations.android.xmpp.model.rsm;
import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.Page;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Set extends Extension {
public Set() {
super(Set.class);
}
public static Set of(final Range range, final Integer max) {
final var set = new Set();
if (range.order == Range.Order.NORMAL) {
final var after = set.addExtension(new After());
after.setContent(range.id);
} else if (range.order == Range.Order.REVERSE) {
final var before = set.addExtension(new Before());
before.setContent(range.id);
} else {
throw new IllegalArgumentException("Invalid order");
}
if (max != null) {
set.addExtension(new Max()).setMax(max);
}
return set;
}
public Page asPage() {
final var first = this.getExtension(First.class);
final var last = this.getExtension(Last.class);
final var firstId = first == null ? null : first.getContent();
final var lastId = last == null ? null : last.getContent();
if (Strings.isNullOrEmpty(firstId) || Strings.isNullOrEmpty(lastId)) {
throw new IllegalStateException("Invalid page. Missing first or last");
}
return new Page(firstId, lastId, this.getCount());
}
public boolean isEmpty() {
final var first = this.getExtension(First.class);
final var last = this.getExtension(Last.class);
return first == null && last == null;
}
public Integer getCount() {
final var count = this.getExtension(Count.class);
return count == null ? null : count.getCount();
}
}

View file

@ -0,0 +1,5 @@
@XmlPackage(namespace = Namespace.RESULT_SET_MANAGEMENT)
package im.conversations.android.xmpp.model.rsm;
import im.conversations.android.annotation.XmlPackage;
import im.conversations.android.xml.Namespace;

View file

@ -1,21 +1,19 @@
package im.conversations.android.xmpp.processor; package im.conversations.android.xmpp.processor;
import android.content.Context; import android.content.Context;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.Range;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.manager.ArchiveManager;
import im.conversations.android.xmpp.manager.AxolotlManager; import im.conversations.android.xmpp.manager.AxolotlManager;
import im.conversations.android.xmpp.manager.BlockingManager; import im.conversations.android.xmpp.manager.BlockingManager;
import im.conversations.android.xmpp.manager.BookmarkManager; import im.conversations.android.xmpp.manager.BookmarkManager;
import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.manager.HttpUploadManager;
import im.conversations.android.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.manager.RosterManager; import im.conversations.android.xmpp.manager.RosterManager;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import okhttp3.MediaType;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -32,12 +30,14 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
public void accept(final Jid jid) { public void accept(final Jid jid) {
final var account = getAccount(); final var account = getAccount();
final var database = getDatabase(); final var database = getDatabase();
final var archive = jid.asBareJid();
final List<Range> catchUpQueryRanges =
database.runInTransaction( database.runInTransaction(
() -> { () -> {
database.chatDao().resetMucStates(); database.chatDao().resetMucStates();
database.presenceDao().deletePresences(account.id); database.presenceDao().deletePresences(account.id);
database.discoDao().deleteUnused(account.id); database.discoDao().deleteUnused(account.id);
return database.archiveDao().resetLivePage(account, archive);
}); });
getManager(RosterManager.class).fetch(); getManager(RosterManager.class).fetch();
@ -57,24 +57,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
getManager(AxolotlManager.class).publishIfNecessary(); getManager(AxolotlManager.class).publishIfNecessary();
getManager(ArchiveManager.class).query(archive, catchUpQueryRanges);
getManager(PresenceManager.class).sendPresence(); getManager(PresenceManager.class).sendPresence();
final var future =
getManager(HttpUploadManager.class)
.request("foo.jpg", 123, MediaType.get("image/jpeg"));
Futures.addCallback(
future,
new FutureCallback<HttpUploadManager.Slot>() {
@Override
public void onSuccess(HttpUploadManager.Slot result) {
LOGGER.info("requested slot {}", result);
}
@Override
public void onFailure(Throwable t) {
LOGGER.info("could not request slot", t);
}
},
MoreExecutors.directExecutor());
} }
} }

View file

@ -87,7 +87,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
final var axolotlService = final var axolotlService =
connection.getManager(AxolotlManager.class).getAxolotlService(); connection.getManager(AxolotlManager.class).getAxolotlService();
final var transformer = new Transformer(getAccount(), database, axolotlService); final var transformer = new Transformer(getAccount(), database, axolotlService);
sendReceipts = transformer.transform(transformation); sendReceipts = transformer.transform(transformation, stanzaId);
} else { } else {
sendReceipts = true; sendReceipts = true;
} }