Switch to MAM synchronization for MUC rooms when available #siskinim-220
This commit is contained in:
parent
f5c361b432
commit
df7e5cca70
|
@ -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
14
Shared/db-schema-13.sql
Normal 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;
|
|
@ -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 */,
|
||||
|
|
|
@ -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 {
|
||||
|
|
121
SiskinIM/database/DBChatHistorySyncStore.swift
Normal file
121
SiskinIM/database/DBChatHistorySyncStore.swift
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue