Switch to MAM synchronization for MUC rooms when available #siskinim-220

This commit is contained in:
Andrzej Wójcik 2020-07-21 18:39:24 +02:00
parent f5c361b432
commit df7e5cca70
No known key found for this signature in database
GPG key ID: 2BE28BB9C1B5FF02
8 changed files with 209 additions and 15 deletions

View file

@ -24,7 +24,7 @@ import TigaseSwift
public class DBSchemaManager {
static let CURRENT_VERSION = 12;
static let CURRENT_VERSION = 13;
fileprivate let dbConnection: DBConnection;

14
Shared/db-schema-13.sql Normal file
View file

@ -0,0 +1,14 @@
BEGIN;
CREATE TABLE IF NOT EXISTS chat_history_sync (
id TEXT NOT NULL COLLATE NOCASE,
account TEXT NOT NULL COLLATE NOCASE,
component TEXT COLLATE NOCASE,
from_timestamp INTEGER NOT NULL,
from_id TEXT,
to_timestamp INTEGER NOT NULL
);
COMMIT;
PRAGMA user_version = 13;

View file

@ -153,6 +153,8 @@
FEB28A9123CB5ADD00F876B7 /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FEB28A8F23CB5AD600F876B7 /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FEB5EC9D1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB5EC9C1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift */; };
FEB62C501DA80956001500D5 /* AvatarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB62C4F1DA80956001500D5 /* AvatarStore.swift */; };
FEBC12F224C70DE000689475 /* db-schema-13.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEBC12F124C70DE000689475 /* db-schema-13.sql */; };
FEBC12F524C70E7F00689475 /* DBChatHistorySyncStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBC12F324C70E2900689475 /* DBChatHistorySyncStore.swift */; };
FEC514241CEB2FF7003AF765 /* MucJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514231CEB2FF7003AF765 /* MucJoinViewController.swift */; };
FEC514261CEB74F8003AF765 /* BaseChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514251CEB74F8003AF765 /* BaseChatViewController.swift */; };
FEC514281CEB82E9003AF765 /* MucChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514271CEB82E9003AF765 /* MucChatViewController.swift */; };
@ -425,6 +427,8 @@
FEB28A8F23CB5AD600F876B7 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebRTC.framework; sourceTree = "<group>"; };
FEB5EC9C1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift; sourceTree = "<group>"; };
FEB62C4F1DA80956001500D5 /* AvatarStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarStore.swift; sourceTree = "<group>"; };
FEBC12F124C70DE000689475 /* db-schema-13.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-13.sql"; sourceTree = "<group>"; };
FEBC12F324C70E2900689475 /* DBChatHistorySyncStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBChatHistorySyncStore.swift; sourceTree = "<group>"; };
FEC514231CEB2FF7003AF765 /* MucJoinViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MucJoinViewController.swift; sourceTree = "<group>"; };
FEC514251CEB74F8003AF765 /* BaseChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewController.swift; sourceTree = "<group>"; };
FEC514271CEB82E9003AF765 /* MucChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MucChatViewController.swift; sourceTree = "<group>"; };
@ -602,6 +606,7 @@
FEC79194241ABEF4007BE572 /* MessageState.swift */,
FEE49DCF2426485500900BBB /* DBConnection_main.swift */,
FE3E387B242766A900D3A8E8 /* DBChannelStore.swift */,
FEBC12F324C70E2900689475 /* DBChatHistorySyncStore.swift */,
);
path = database;
sourceTree = "<group>";
@ -736,6 +741,7 @@
FE10BCF223FD4EF000E214F3 /* db-schema-10.sql */,
FEC79198241BE89E007BE572 /* db-schema-11.sql */,
FE3BA0BF24B61583000C80D4 /* db-schema-12.sql */,
FEBC12F124C70DE000689475 /* db-schema-13.sql */,
);
path = Shared;
sourceTree = "<group>";
@ -1128,6 +1134,7 @@
FE759FF12371F21C001E78D9 /* db-schema-5.sql in Resources */,
FE759FF523741527001E78D9 /* db-schema-8.sql in Resources */,
FE759FEE2371F217001E78D9 /* db-schema-1.sql in Resources */,
FEBC12F224C70DE000689475 /* db-schema-13.sql in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1246,6 +1253,7 @@
FE507A211CDB7B3B001A015C /* SettingsViewController.swift in Sources */,
FE507A191CDB7B3B001A015C /* DBChatHistoryStore.swift in Sources */,
FEA8D65D1F2F6AF60077C12F /* VCardEntryTypeAwareTableViewCell.swift in Sources */,
FEBC12F524C70E7F00689475 /* DBChatHistorySyncStore.swift in Sources */,
FEDC6790238B05E4005C0FAB /* BlockedContactsController.swift in Sources */,
FE2332E1242CCDB400008ED4 /* InvitationChatTableViewCell.swift in Sources */,
FE3E38862428C21100D3A8E8 /* OSLog.swift in Sources */,

View file

@ -206,7 +206,7 @@ public class DBChatHistoryStore {
let (authorNickname, authorJid, recipientNickname, participantId) = MessageEventHandler.extractRealAuthor(from: message, for: account, with: jidFull);
let state = MessageEventHandler.calculateState(direction: MessageEventHandler.calculateDirection(direction: direction, for: account, with: jid, authorNickname: authorNickname, authorJid: authorJid), isError: (message.type ?? .chat) == .error, isUnread: !fromArchive);
let state = MessageEventHandler.calculateState(direction: MessageEventHandler.calculateDirection(direction: direction, for: account, with: jid, authorNickname: authorNickname, authorJid: authorJid), isError: (message.type ?? .chat) == .error, isFromArchive: fromArchive, isMuc: message.type == .groupchat && message.mix == nil);
var appendix: AppendixProtocol? = nil;
if itemType == .message, let mixInvitation = mixInvitation {

View file

@ -0,0 +1,121 @@
//
// DBChatHistorySyncStore.swift
//
// Siskin IM
// Copyright (C) 2020 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import TigaseSwift
import Shared
import os
class DBChatHistorySyncStore {
static let instance = DBChatHistorySyncStore()
private let addSyncPeriod: DBStatement;
private let loadSyncPeriods: DBStatement;
private let loadSyncPeriodsWith: DBStatement;
private let removeSyncPeriod: DBStatement;
private let updateSyncPeriodAfter: DBStatement;
private let updateSyncPeriodTo: DBStatement;
init() {
addSyncPeriod = try! DBConnection.main.prepareStatement("INSERT INTO chat_history_sync (id, account, component, from_timestamp, from_id, to_timestamp) VALUES (:id, :account, :component, :from_timestamp, :from_id, :to_timestamp)");
loadSyncPeriods = try! DBConnection.main.prepareStatement("SELECT id, account, from_timestamp, from_id, to_timestamp FROM chat_history_sync WHERE account = :account AND component IS NULL ORDER BY from_timestamp ASC");
loadSyncPeriodsWith = try! DBConnection.main.prepareStatement("SELECT id, account, component, from_timestamp, from_id, to_timestamp FROM chat_history_sync WHERE account = :account AND component = :component ORDER BY from_timestamp ASC");
removeSyncPeriod = try! DBConnection.main.prepareStatement("DELETE FROM chat_history_sync WHERE id = :id");
updateSyncPeriodAfter = try! DBConnection.main.prepareStatement("UPDATE chat_history_sync SET from_id = :after WHERE id = :id");
updateSyncPeriodTo = try! DBConnection.main.prepareStatement("UPDATE chat_history_sync SET to_timestamp = :to_timestamp WHERE id = :id");
NotificationCenter.default.addObserver(self, selector: #selector(accountChanged(_:)), name: AccountManager.ACCOUNT_CHANGED, object: nil);
}
@objc func accountChanged(_ notification: Notification) {
guard let acc = notification.object as? AccountManager.Account, AccountManager.getAccount(for: acc.name) == nil else {
return;
}
removeSyncPeriods(forAccount: acc.name);
}
func addSyncPeriod(_ period: Period) {
if let last = loadSyncPeriods(forAccount: period.account, component: period.component).last, last.from <= period.from && last.to >= period.from {
// we only need to update `to` value
os_log("updating sync period to for account %s and component %s", log: .chatHistorySync, type: .debug, period.account.stringValue, period.component?.stringValue ?? "nil");
_ = try! updateSyncPeriodTo.update(["id": last.id.uuidString, "to_timestamp": max(last.to, period.to)] as [String: Any?]);
return;
}
os_log("adding sync period %s for account %s and component %s from %{time_t}d to %{time_t}d", log: .chatHistorySync, type: .debug, period.id.uuidString, period.account.stringValue, period.component?.stringValue ?? "nil", time_t(period.from.timeIntervalSince1970), time_t(period.to.timeIntervalSince1970));
_ = try! addSyncPeriod.insert(["id": period.id.uuidString, "account": period.account, "component": period.component, "from_timestamp": period.from, "to_timestamp": period.to] as [String: Any?]);
}
func loadSyncPeriods(forAccount account: BareJID, component: BareJID?) -> [Period] {
// how about periods with less than a few minutes/seconds apart? should we merge them?
if let component = component {
let periods = try! loadSyncPeriodsWith.query(["account": account, "component": component] as [String: Any?], map: {
return Period(id: UUID(uuidString: $0["id"]!)!, account: $0["account"]!, component: $0["component"], from: $0["from_timestamp"]!, after: $0["from_id"], to: $0["to_timestamp"]!);
});
os_log("loaded %d sync periods for account %s and component %s", log: .chatHistorySync, type: .debug, periods.count, account.stringValue, component.stringValue);
return periods;
}
else {
let periods = try! loadSyncPeriods.query(["account": account] as [String: Any?], map: {
return Period(id: UUID(uuidString: $0["id"]!)!, account: $0["account"]!, from: $0["from_timestamp"]!, after: $0["from_id"], to: $0["to_timestamp"]!);
});
os_log("loaded %d sync periods for account %s", log: .chatHistorySync, type: .debug, periods.count, account.stringValue);
return periods;
}
}
func removeSyncPerod(_ period: Period) {
os_log("removing sync period %s for account %s and component %s", log: .chatHistorySync, type: .debug, period.id.uuidString, period.account.stringValue, period.component?.stringValue ?? "nil");
_ = try! removeSyncPeriod.update(["id": period.id.uuidString] as [String: Any?]);
}
func removeSyncPeriods(forAccount account: BareJID, component: BareJID? = nil) {
if let component = component {
_ = try! DBConnection.main.prepareStatement("DELETE FROM chat_history_sync WHERE account = :account AND component = :component").update(["account": account, "component": component] as [String: Any?]);
} else {
_ = try! DBConnection.main.prepareStatement("DELETE FROM chat_history_sync WHERE account = :account").update(["account": account] as [String: Any?]);
}
}
func updatePeriod(_ period: Period, after: String) {
os_log("updating sync period %s for account %s and component %s to after %s", log: .chatHistorySync, type: .debug, period.id.uuidString, period.account.stringValue, period.component?.stringValue ?? "nil", after);
_ = try! updateSyncPeriodAfter.update(["id": period.id.uuidString, "after": after] as [String: Any?]);
}
class Period {
let id: UUID;
let account: BareJID;
let component: BareJID?;
let from: Date;
var after: String?;
let to: Date;
init(id: UUID = UUID(), account: BareJID, component: BareJID? = nil, from: Date, after: String?, to: Date = Date()) {
self.id = id;
self.account = account;
self.component = component;
self.from = from;
self.after = after;
self.to = to;
}
}
}

View file

@ -22,6 +22,7 @@
import Foundation
import TigaseSwift
import TigaseSwiftOMEMO
import os
class MessageEventHandler: XmppServiceEventHandler {
@ -313,7 +314,8 @@ class MessageEventHandler: XmppServiceEventHandler {
return direction;
}
static func calculateState(direction: MessageDirection, isError error: Bool, isUnread unread: Bool) -> MessageState {
static func calculateState(direction: MessageDirection, isError error: Bool, isFromArchive archived: Bool, isMuc: Bool) -> MessageState {
let unread = (!archived) || isMuc;
if direction == .incoming {
if error {
return unread ? .incoming_error_unread : .incoming_error;
@ -344,6 +346,7 @@ class MessageEventHandler: XmppServiceEventHandler {
} else {
syncSinceQueue.async {
syncSince.removeValue(forKey: account);
DBChatHistorySyncStore.instance.removeSyncPeriods(forAccount: account);
}
}
}
@ -357,20 +360,44 @@ class MessageEventHandler: XmppServiceEventHandler {
}
}
static func syncMessages(for account: BareJID, since: Date, rsmQuery: RSM.Query? = nil) {
guard let mamModule: MessageArchiveManagementModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(MessageArchiveManagementModule.ID) else {
static func syncMessages(for account: BareJID, version: MessageArchiveManagementModule.Version? = nil, componentJID: JID? = nil, since: Date, rsmQuery: RSM.Query? = nil) {
let period = DBChatHistorySyncStore.Period(account: account, component: componentJID?.bareJid, from: since, after: nil);
DBChatHistorySyncStore.instance.addSyncPeriod(period);
syncMessagePeriods(for: account, version: version, componentJID: componentJID?.bareJid)
}
static func syncMessagePeriods(for account: BareJID, version: MessageArchiveManagementModule.Version? = nil, componentJID jid: BareJID? = nil) {
guard let first = DBChatHistorySyncStore.instance.loadSyncPeriods(forAccount: account, component: jid).first else {
return;
}
syncSinceQueue.async {
syncMessages(forPeriod: first, version: version);
}
}
static func syncMessages(forPeriod period: DBChatHistorySyncStore.Period, version: MessageArchiveManagementModule.Version? = nil, rsmQuery: RSM.Query? = nil) {
guard let mamModule: MessageArchiveManagementModule = XmppService.instance.getClient(for: period.account)?.modulesManager.getModule(MessageArchiveManagementModule.ID) else {
return;
}
let start = Date();
let queryId = UUID().uuidString;
mamModule.queryItems(start: since, queryId: queryId, rsm: rsmQuery ?? RSM.Query(max: 200), completionHandler: { (result) in
mamModule.queryItems(version: version, componentJid: period.component == nil ? nil : JID(period.component!), start: period.from, end: period.to, queryId: queryId, rsm: rsmQuery ?? RSM.Query(after: period.after, max: 150), completionHandler: { (result) in
switch result {
case .success(_, _, let rsmResponse):
if rsmResponse != nil && rsmResponse!.index != 0 && rsmResponse?.first != nil {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
self.syncMessages(for: account, since: since, rsmQuery: rsmResponse?.next(200));
case .success(_, let complete, let rsmResponse):
if complete || rsmResponse == nil {
DBChatHistorySyncStore.instance.removeSyncPerod(period);
syncMessagePeriods(for: period.account, version: version, componentJID: period.component);
} else {
if let last = rsmResponse?.last, UUID(uuidString: last) != nil {
DBChatHistorySyncStore.instance.updatePeriod(period, after: last);
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
self.syncMessages(forPeriod: period, rsmQuery: rsmResponse?.next(150));
}
}
os_log("for account %s fetch with id %s executed in %f s", log: .chatHistorySync, type: .debug, period.account.stringValue, queryId, Date().timeIntervalSince(start));
case .failure(let errorCondition, let response):
print("could not synchronize message archive for:", errorCondition, "got", response as Any);
}

View file

@ -39,10 +39,34 @@ class MucEventHandler: XmppServiceEventHandler {
guard !XmppService.instance.isFetch else {
return;
}
if let mucModule: MucModule = XmppService.instance.getClient(for: e.sessionObject.userBareJid!)?.modulesManager.getModule(MucModule.ID) {
if let client = XmppService.instance.getClient(for: e.sessionObject.userBareJid!), let mucModule: MucModule = client.modulesManager.getModule(MucModule.ID) {
mucModule.roomsManager.getRooms().forEach { (room) in
_ = room.rejoin();
NotificationCenter.default.post(name: MucEventHandler.ROOM_STATUS_CHANGED, object: room);
// first we need to check if room supports MAM
if let discoModule: DiscoveryModule = client.modulesManager.getModule(DiscoveryModule.ID), let mamModule: MessageArchiveManagementModule = client.modulesManager.getModule(MessageArchiveManagementModule.ID) {
discoModule.getInfo(for: room.jid, completionHandler: { result in
var mamVersions: [MessageArchiveManagementModule.Version] = [];
switch result {
case .success(_, _, let features):
mamVersions = features.map({ MessageArchiveManagementModule.Version(rawValue: $0) }).filter({ $0 != nil}).map({ $0! });
default:
break;
}
if let timestamp = room.lastMessageDate, !mamVersions.isEmpty {
let account = e.sessionObject.userBareJid!;
room.onRoomJoined = { room in
MessageEventHandler.syncMessages(for: account, version: mamVersions.contains(.MAM2) ? .MAM2 : .MAM1, componentJID: room.jid, since: timestamp);
}
_ = room.rejoin(skipHistoryFetch: true);
NotificationCenter.default.post(name: MucEventHandler.ROOM_STATUS_CHANGED, object: room);
} else {
_ = room.rejoin();
NotificationCenter.default.post(name: MucEventHandler.ROOM_STATUS_CHANGED, object: room);
}
});
} else {
_ = room.rejoin();
NotificationCenter.default.post(name: MucEventHandler.ROOM_STATUS_CHANGED, object: room);
}
}
}
case let e as MucModule.YouJoinedEvent:

View file

@ -26,5 +26,5 @@ extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!;
static let chatStore = OSLog(subsystem: subsystem, category: "ChatStore");
static let chatHistorySync = OSLog(subsystem: subsystem, category: "mam-sync");
}