| /* |
| * Copyright (C) 2017-2020 Savoir-faire Linux Inc. |
| * |
| * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> |
| * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.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; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| import Foundation |
| import RxSwift |
| import SQLite |
| |
| enum ProfileType: String { |
| case ring = "RING" |
| case sip = "SIP" |
| } |
| |
| enum GeneratedMessage: Int { |
| case outgoingCall |
| case incomingCall |
| case missedOutgoingCall |
| case missedIncomingCall |
| case contactAdded |
| case invitationReceived |
| case invitationAccepted |
| case unknown |
| |
| func toString() -> String { |
| return String(self.rawValue) |
| } |
| init(from: String) { |
| if let intValue = Int(from) { |
| self = GeneratedMessage(rawValue: intValue) ?? .unknown |
| } else { |
| self = .unknown |
| } |
| } |
| func toMessage(with duration: Int) -> String { |
| let time = Date.convertSecondsToTimeString(seconds: Double(duration)) |
| switch self { |
| case .contactAdded: |
| return L10n.GeneratedMessage.contactAdded |
| case .invitationReceived: |
| return L10n.GeneratedMessage.invitationReceived |
| case .invitationAccepted: |
| return L10n.GeneratedMessage.invitationAccepted |
| case .missedOutgoingCall: |
| return L10n.GeneratedMessage.missedOutgoingCall |
| case .missedIncomingCall: |
| return L10n.GeneratedMessage.missedIncomingCall |
| case .outgoingCall: |
| return L10n.GeneratedMessage.outgoingCall + " - " + time |
| case .incomingCall: |
| return L10n.GeneratedMessage.incomingCall + " - " + time |
| default: |
| return "" |
| } |
| } |
| } |
| |
| enum InteractionStatus: String { |
| case invalid = "INVALID" |
| case unknown = "UNKNOWN" |
| case sending = "SENDING" |
| case failed = "FAILED" |
| case succeed = "SUCCEED" |
| case displayed = "DISPLAYED" |
| case unread = "UNREAD" |
| case transferCreated = "TRANSFER_CREATED" |
| case transferAwaiting = "TRANSFER_AWAITING" |
| case transferCanceled = "TRANSFER_CANCELED" |
| case transferOngoing = "TRANSFER_ONGOING" |
| case transferSuccess = "TRANSFER_FINISHED" |
| case transferError = "TRANSFER_ERROR" |
| |
| func toMessageStatus() -> MessageStatus { |
| switch self { |
| case .invalid: return MessageStatus.unknown |
| case .unknown: return MessageStatus.unknown |
| case .sending: return MessageStatus.sending |
| case .failed: return MessageStatus.failure |
| case .succeed: return MessageStatus.sent |
| case .displayed: return MessageStatus.displayed |
| case .unread: return MessageStatus.unknown |
| default: return MessageStatus.unknown |
| } |
| } |
| |
| init(status: MessageStatus) { |
| switch status { |
| case .unknown: self = .unknown |
| case .sending: self = .sending |
| case .sent: self = .succeed |
| case .displayed: self = .displayed |
| case .failure: self = .failed |
| @unknown default: |
| self = .unknown |
| } |
| } |
| |
| func toDataTransferStatus() -> DataTransferStatus { |
| switch self { |
| case .transferCreated: return DataTransferStatus.created |
| case .transferAwaiting: return DataTransferStatus.awaiting |
| case .transferCanceled: return DataTransferStatus.canceled |
| case .transferOngoing: return DataTransferStatus.ongoing |
| case .transferSuccess: return DataTransferStatus.success |
| case .transferError: return DataTransferStatus.error |
| case .displayed: return DataTransferStatus.success |
| default: return DataTransferStatus.unknown |
| } |
| } |
| |
| init(status: DataTransferStatus) { |
| switch status { |
| case .created: self = .transferCreated |
| case .awaiting: self = .transferAwaiting |
| case .canceled: self = .transferCanceled |
| case .ongoing: self = .transferOngoing |
| case .success: self = .transferSuccess |
| case .error: self = .transferError |
| case .unknown: self = .unknown |
| } |
| } |
| } |
| |
| enum DBBridgingError: Error { |
| case saveMessageFailed |
| case getConversationFailed |
| case updateIntercationFailed |
| case deleteConversationFailed |
| case getProfileFailed |
| case deleteMessageFailed |
| } |
| |
| enum InteractionType: String { |
| case invalid = "INVALID" |
| case text = "TEXT" |
| case call = "CALL" |
| case contact = "CONTACT" |
| case iTransfer = "INCOMING_DATA_TRANSFER" |
| case oTransfer = "OUTGOING_DATA_TRANSFER" |
| case location = "LOCATION" |
| |
| func toMessageType() -> MessageType { |
| switch self { |
| case .invalid, .text: |
| return .text |
| case .call: |
| return .call |
| case .contact: |
| return .contact |
| case .iTransfer: |
| return .fileTransfer |
| case .oTransfer: |
| return .fileTransfer |
| case .location: |
| return .location |
| } |
| } |
| } |
| |
| typealias SavedMessageForConversation = (messageID: String, conversationID: String) |
| |
| // swiftlint:disable type_body_length |
| // swiftlint:disable file_length |
| class DBManager { |
| |
| let profileHepler: ProfileDataHelper |
| let conversationHelper: ConversationDataHelper |
| let interactionHepler: InteractionDataHelper |
| let dbConnections: DBContainer |
| let disposeBag = DisposeBag() |
| |
| // used to create object to save to db. When inserting in table defaultID will be replaced by autoincrementedID |
| let defaultID: Int64 = 1 |
| |
| init(profileHepler: ProfileDataHelper, conversationHelper: ConversationDataHelper, |
| interactionHepler: InteractionDataHelper, dbConnections: DBContainer) { |
| self.profileHepler = profileHepler |
| self.conversationHelper = conversationHelper |
| self.interactionHepler = interactionHepler |
| self.dbConnections = dbConnections |
| } |
| |
| func isMigrationToDBv2Needed(accountId: String) -> Bool { |
| return self.dbConnections.isMigrationToDBv2Needed(for: accountId) |
| } |
| |
| func migrateToDbVersion2(accountId: String, accountURI: String) -> Bool { |
| if !accountURI.contains("ring:") { |
| self.dbConnections.createAccountfolder(for: accountId) |
| } |
| if !self.dbConnections.copyDbToAccountFolder(for: accountId) { |
| return false |
| } |
| guard let newDB = self.dbConnections.forAccount(account: accountId) else { |
| return false |
| } |
| // move profiles to vcards |
| do { |
| try newDB.transaction { [weak self] in |
| guard let self = self else { throw DataAccessError.databaseError } |
| if try !self.migrateAccountToVCard(for: accountId, accountURI: accountURI, dataBase: newDB) { |
| throw DataAccessError.databaseError |
| } |
| if try !self.migrateProfilesToVCards(for: accountId, dataBase: newDB) { |
| throw DataAccessError.databaseError |
| } |
| // remove db from documents folder |
| self.dbConnections.removeDBForAccount(account: accountId) |
| newDB.userVersion = 2 |
| } |
| } catch _ as NSError { |
| return false |
| } |
| return true |
| } |
| |
| func createDatabaseForAccount(accountId: String, createFolder: Bool = false) throws -> Bool { |
| if createFolder { |
| self.dbConnections.createAccountfolder(for: accountId) |
| } |
| guard let newDB = self.dbConnections.forAccount(account: accountId) else { |
| return false |
| } |
| do { |
| try newDB.transaction { |
| conversationHelper.createTable(accountDb: newDB) |
| interactionHepler.createTable(accountDb: newDB) |
| } |
| } catch { |
| throw DataAccessError.databaseError |
| } |
| return true |
| } |
| |
| func migrateProfilesToVCards(for accountId: String, dataBase: Connection) throws -> Bool { |
| guard let profiles = try? self.profileHepler.selectAll(dataBase: dataBase) else { |
| return false |
| } |
| for profile in profiles { |
| if self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profile.uri) { |
| continue |
| } |
| guard let profilePath = self.dbConnections.contactProfilePath(accountId: accountId, profileURI: profile.uri, createifNotExists: true) else { return false } |
| try self.saveProfile(profile: profile, path: profilePath) |
| if !self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profile.uri) { |
| return false |
| } |
| } |
| self.profileHepler.dropProfileTable(accountDb: dataBase) |
| return true |
| } |
| |
| func migrateAccountToVCard(for accountId: String, accountURI: String, dataBase: Connection) throws -> Bool { |
| if self.dbConnections.isAccountProfileExists(accountId: accountId) { return true } |
| guard let accountProfile = self.profileHepler.getAccountProfile(dataBase: dataBase) else { |
| return self.dbConnections.isAccountProfileExists(accountId: accountId) |
| } |
| guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return false } |
| let type = accountURI.contains("ring:") ? URIType.ring : URIType.sip |
| let profile = Profile(uri: accountURI, alias: accountProfile.alias, photo: accountProfile.photo, type: type.getString()) |
| try self.saveProfile(profile: profile, path: path) |
| if !self.dbConnections.isAccountProfileExists(accountId: accountId) { |
| return false |
| } |
| self.profileHepler.dropAccountTable(accountDb: dataBase) |
| return true |
| } |
| |
| func removeDBForAccount(accountId: String, removeFolder: Bool) { |
| self.dbConnections.removeDBForAccount(account: accountId, removeFolder: removeFolder) |
| } |
| |
| func createConversationsFor(contactUri: String, accountId: String) -> String { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| return "" |
| } |
| do { |
| let result = try self.getConversationsFor(contactUri: contactUri, |
| createIfNotExists: true, |
| dataBase: dataBase, accountId: accountId) |
| return "\(result ?? -1)" |
| } catch {} |
| return "" |
| } |
| |
| // swiftlint:disable:next function_parameter_count |
| func saveMessage(for accountId: String, with contactUri: String, |
| message: MessageModel, incoming: Bool, |
| interactionType: InteractionType, duration: Int) -> Observable<SavedMessageForConversation> { |
| |
| // create completable which will be executed on background thread |
| return Observable.create { [weak self] observable in |
| do { |
| guard let dataBase = self?.dbConnections.forAccount(account: accountId) else { |
| throw DataAccessError.datastoreConnectionError |
| } |
| try dataBase.transaction { |
| let author: String? = incoming ? contactUri : nil |
| guard let conversationID = try self?.getConversationsFor(contactUri: contactUri, |
| createIfNotExists: true, |
| dataBase: dataBase, accountId: accountId) else { |
| throw DBBridgingError.saveMessageFailed |
| } |
| let result = self?.addMessageTo(conversation: conversationID, author: author, |
| interactionType: interactionType, message: message, |
| duration: duration, dataBase: dataBase) |
| if let messageID = result { |
| let savedMessage = SavedMessageForConversation(messageID, String(conversationID)) |
| observable.onNext(savedMessage) |
| observable.on(.completed) |
| } else { |
| observable.on(.error(DBBridgingError.saveMessageFailed)) |
| } |
| } |
| } catch { |
| observable.on(.error(DBBridgingError.saveMessageFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func getConversationsObservable(for accountId: String) -> Observable<[ConversationModel]> { |
| return Observable.create { observable in |
| do { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| throw DBBridgingError.getConversationFailed |
| } |
| try dataBase.transaction(Connection.TransactionMode.immediate, block: { |
| let conversations = try self.buildConversationsForAccount(accountId: accountId) |
| observable.onNext(conversations) |
| observable.on(.completed) |
| }) |
| } catch { |
| observable.on(.error(DBBridgingError.getConversationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func updateMessageStatus(daemonID: String, withStatus status: InteractionStatus, accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| if let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) { |
| let success = self.interactionHepler |
| .updateInteractionWithDaemonID(interactionDaemonID: daemonID, |
| interactionStatus: status.rawValue, |
| dataBase: dataBase) |
| if success { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func getProfilesForAccount(accountId: String) -> [Profile]? { |
| var profiles = [Profile]() |
| do { |
| guard let path = self.dbConnections.contactsPath(accountId: accountId, |
| createIfNotExists: true) else { return nil } |
| guard let documentURL = URL(string: path) else { return nil } |
| let directoryContents = try FileManager.default.contentsOfDirectory(at: documentURL, includingPropertiesForKeys: nil, options: []) |
| for url in directoryContents { |
| if let profile = getProfileFromPath(path: url.path) { |
| profiles.append(profile) |
| } |
| } |
| } catch { |
| print(error.localizedDescription) |
| } |
| return profiles |
| } |
| |
| func updateFileName(daemonID: String, name: String, accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| if let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) { |
| let success = self.interactionHepler |
| .updateInteractionContentWithID(daemonID: daemonID, content: name, dataBase: dataBase) |
| if success { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func updateTransferStatus(daemonID: String, withStatus transferStatus: DataTransferStatus, accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| if let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) { |
| let success = self.interactionHepler |
| .updateInteractionWithDaemonID(interactionDaemonID: daemonID, |
| interactionStatus: InteractionStatus(status: transferStatus).rawValue, |
| dataBase: dataBase) |
| if success { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| } else { |
| completable(.error(DBBridgingError.updateIntercationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func setMessagesAsRead(messagesIDs: [String], withStatus status: MessageStatus, accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| if let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) { |
| var success = true |
| for messageId in messagesIDs { |
| if !self.interactionHepler |
| .updateInteractionStatusWithID(interactionID: Int64(messageId) ?? -1, |
| interactionStatus: InteractionStatus(status: status).rawValue, |
| dataBase: dataBase) { |
| success = false |
| } |
| } |
| if success { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.saveMessageFailed)) |
| } |
| } else { |
| completable(.error(DBBridgingError.saveMessageFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func deleteMessage(messagesId: Int64, accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| if let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) { |
| if self.interactionHepler.delete(interactionId: messagesId, dataBase: dataBase) { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.deleteMessageFailed)) |
| } |
| } else { |
| completable(.error(DBBridgingError.deleteMessageFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func clearAllHistoryFor(accountId: String) -> Completable { |
| return Completable.create { [weak self] completable in |
| do { |
| guard let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| throw DBBridgingError.deleteConversationFailed |
| } |
| try dataBase.transaction { |
| if !self.interactionHepler |
| .deleteAll(dataBase: dataBase) { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| if !self.conversationHelper |
| .deleteAll(dataBase: dataBase) { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| self.dbConnections.removeContacts(accountId: accountId) |
| completable(.completed) |
| } |
| } catch { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func clearHistoryFor(accountId: String, |
| and participantUri: String, |
| keepConversation: Bool) -> Completable { |
| return Completable.create { [weak self] completable in |
| do { |
| guard let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| throw DBBridgingError.deleteConversationFailed |
| } |
| try dataBase.transaction { |
| guard (try self.getProfile(for: participantUri, createIfNotExists: false, accountId: accountId)) != nil else { |
| throw DBBridgingError.deleteConversationFailed |
| } |
| guard let conversationsId = try self.getConversationsFor(contactUri: participantUri, |
| createIfNotExists: true, |
| dataBase: dataBase, accountId: accountId) else { |
| throw DBBridgingError.deleteConversationFailed |
| } |
| guard let interactions = try self.interactionHepler |
| .selectInteractionsForConversation( |
| conv: conversationsId, |
| dataBase: dataBase) else { |
| throw DBBridgingError.deleteConversationFailed |
| } |
| if !interactions.isEmpty { |
| if !self.interactionHepler |
| .deleteAllInteractions(conv: conversationsId, dataBase: dataBase) { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| } |
| if keepConversation { |
| completable(.completed) |
| } else { |
| let successConversations = self.conversationHelper |
| .deleteConversations(conversationID: conversationsId, dataBase: dataBase) |
| self.dbConnections.removeProfile(accountId: accountId, profileURI: participantUri) |
| if successConversations { |
| completable(.completed) |
| } else { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| } |
| } |
| } catch { |
| completable(.error(DBBridgingError.deleteConversationFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func profileObservable(for profileUri: String, createIfNotExists: Bool, accountId: String) -> Observable<Profile> { |
| return Observable.create { observable in |
| do { |
| if let profile = try self.getProfile(for: profileUri, |
| createIfNotExists: createIfNotExists, |
| accountId: accountId) { |
| observable.onNext(profile) |
| observable.on(.completed) |
| } else { |
| observable.on(.error(DBBridgingError.getProfileFailed)) |
| } |
| } catch { |
| observable.on(.error(DBBridgingError.getProfileFailed)) |
| } |
| return Disposables.create { } |
| } |
| } |
| |
| func accountProfileObservable(for accountId: String) -> Observable<Profile> { |
| return Observable.create { observable in |
| guard let profile = self.accountProfile(for: accountId) else { |
| observable.on(.error(DBBridgingError.getProfileFailed)) |
| return Disposables.create { } |
| } |
| observable.onNext(profile) |
| observable.on(.completed) |
| return Disposables.create { } |
| } |
| } |
| |
| func accountProfile(for accountId: String) -> Profile? { |
| guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return nil } |
| return self.getProfileFromPath(path: path) |
| } |
| |
| func accountVCard(for accountId: String) -> CNContact? { |
| guard let path = self.dbConnections.accountProfilePath(accountId: accountId), |
| let data = FileManager.default.contents(atPath: path) else { return nil } |
| return CNContactVCardSerialization.parseToVCard(data: data) |
| } |
| |
| func createOrUpdateRingProfile(profileUri: String, alias: String?, image: String?, accountId: String) -> Bool { |
| let type = profileUri.contains("ring") ? ProfileType.ring : ProfileType.sip |
| if type == ProfileType.sip { |
| self.dbConnections.createAccountfolder(for: accountId) |
| } |
| guard let path = self.dbConnections.contactProfilePath(accountId: accountId, profileURI: profileUri, createifNotExists: true) else { return false } |
| |
| let profile = Profile(uri: profileUri, alias: alias, photo: image, type: type.rawValue) |
| |
| do { |
| try self.saveProfile(profile: profile, path: path) |
| } catch { |
| return false |
| } |
| return self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profileUri) |
| } |
| |
| func saveAccountProfile(alias: String?, photo: String?, accountId: String, accountURI: String) -> Bool { |
| let type = accountURI.contains("ring") ? ProfileType.ring : ProfileType.sip |
| if type == ProfileType.sip { |
| self.dbConnections.createAccountfolder(for: accountId) |
| } |
| guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return false } |
| let profile = Profile(uri: accountURI, alias: alias, photo: photo, type: type.rawValue) |
| do { |
| try self.saveProfile(profile: profile, path: path) |
| return self.dbConnections.isAccountProfileExists(accountId: accountId) |
| } catch { |
| return false |
| } |
| } |
| |
| // MARK: Private functions |
| private func buildConversationsForAccount(accountId: String) throws -> [ConversationModel] { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| throw DBBridgingError.getConversationFailed |
| } |
| var conversationsToReturn = [ConversationModel]() |
| |
| guard let conversations = try self.conversationHelper.selectAll(dataBase: dataBase), |
| !conversations.isEmpty else { |
| // if there is no conversation for account return empty list |
| return conversationsToReturn |
| } |
| for conversationID in conversations.map({ $0.id }) { |
| guard let participants = try self.getParticipantsForConversation(conversationID: conversationID, |
| dataBase: dataBase), |
| let participant = participants.first else { |
| continue |
| } |
| let type = participant.contains("ring:") ? URIType.ring : URIType.sip |
| let uri = JamiURI.init(schema: type, infoHach: participant) |
| let conversationModel = ConversationModel(withParticipantUri: uri, |
| accountId: accountId) |
| if type == .sip { |
| conversationModel.type = .sip |
| } |
| conversationModel.id = String(conversationID) |
| var messages = [MessageModel]() |
| guard let interactions = try self.interactionHepler |
| .selectInteractionsForConversation( |
| conv: conversationID, |
| dataBase: dataBase) else { |
| continue |
| } |
| for interaction in interactions { |
| let author = interaction.author == participant |
| ? participant : "" |
| if let message = self.convertToMessage(interaction: interaction, author: author) { |
| messages.append(message) |
| } |
| } |
| conversationModel.messages = messages |
| conversationsToReturn.append(conversationModel) |
| } |
| return conversationsToReturn |
| } |
| |
| private func getParticipantsForConversation(conversationID: Int64, dataBase: Connection) throws -> [String]? { |
| guard let conversations = try self.conversationHelper |
| .selectConversations(conversationId: conversationID, |
| dataBase: dataBase) else { |
| return nil |
| } |
| return conversations.map({ $0.participant }) |
| } |
| |
| private func convertToMessage(interaction: Interaction, author: String) -> MessageModel? { |
| if interaction.type != InteractionType.text.rawValue && |
| interaction.type != InteractionType.contact.rawValue && |
| interaction.type != InteractionType.call.rawValue && |
| interaction.type != InteractionType.iTransfer.rawValue && |
| interaction.type != InteractionType.oTransfer.rawValue && |
| interaction.type != InteractionType.location.rawValue { |
| return nil |
| } |
| let content = (interaction.type == InteractionType.call.rawValue |
| || interaction.type == InteractionType.contact.rawValue) ? |
| GeneratedMessage.init(from: interaction.body).toMessage(with: Int(interaction.duration)) |
| : interaction.body |
| let date = Date(timeIntervalSince1970: TimeInterval(interaction.timestamp)) |
| let message = MessageModel(withId: interaction.daemonID, |
| receivedDate: date, |
| content: content, |
| authorURI: author, |
| incoming: interaction.incoming) |
| let isTransfer = interaction.type == InteractionType.iTransfer.rawValue || |
| interaction.type == InteractionType.oTransfer.rawValue |
| message.type = InteractionType(rawValue: interaction.type)?.toMessageType() ?? .text |
| if let status: InteractionStatus = InteractionStatus(rawValue: interaction.status) { |
| if isTransfer { |
| message.transferStatus = status.toDataTransferStatus() |
| } else { |
| message.status = status.toMessageStatus() |
| } |
| } |
| message.id = String(interaction.id) |
| return message |
| } |
| |
| // swiftlint:disable:next function_parameter_count |
| private func addMessageTo(conversation conversationID: Int64, |
| author: String?, |
| interactionType: InteractionType, |
| message: MessageModel, |
| duration: Int, |
| dataBase: Connection) -> String? { |
| var status = InteractionStatus.unknown.rawValue |
| if interactionType == .oTransfer { |
| status = InteractionStatus(status: message.transferStatus).rawValue |
| } |
| let timeInterval = message.receivedDate.timeIntervalSince1970 |
| let interaction = Interaction(id: defaultID, author: author, |
| conversation: conversationID, timestamp: Int64(timeInterval), duration: Int64(duration), |
| body: message.content, type: interactionType.rawValue, |
| status: status, daemonID: message.daemonId, |
| incoming: message.incoming) |
| if let result = self.interactionHepler.insert(item: interaction, dataBase: dataBase) { |
| return String(result) |
| } |
| return nil |
| } |
| |
| func getProfile(for profileUri: String, createIfNotExists: Bool, accountId: String, |
| alias: String? = nil, photo: String? = nil) throws -> Profile? { |
| let type = profileUri.contains("ring") ? ProfileType.ring : ProfileType.sip |
| if createIfNotExists && type == ProfileType.sip { |
| self.dbConnections.createAccountfolder(for: accountId) |
| } |
| guard let profilePath = self.dbConnections |
| .contactProfilePath(accountId: accountId, |
| profileURI: profileUri, |
| createifNotExists: createIfNotExists) else { return nil } |
| if self.dbConnections |
| .isContactProfileExists(accountId: accountId, |
| profileURI: profileUri) || !createIfNotExists { |
| return getProfileFromPath(path: profilePath) |
| } |
| let profile = Profile(uri: profileUri, alias: alias, photo: photo, type: type.rawValue) |
| try self.saveProfile(profile: profile, path: profilePath) |
| return getProfileFromPath(path: profilePath) |
| } |
| |
| private func getProfileFromPath(path: String) -> Profile? { |
| guard let data = FileManager.default.contents(atPath: path), |
| let vCard = CNContactVCardSerialization.parseToVCard(data: data) else { |
| return nil |
| } |
| let profileURI = vCard.phoneNumbers.isEmpty ? "" : vCard.phoneNumbers[0].value.stringValue |
| let type = profileURI.contains("ring") ? ProfileType.ring : ProfileType.sip |
| let imageString = {(data: Data?) -> String in |
| guard let data = data else { return "" } |
| return data.base64EncodedString() |
| }(vCard.imageData) |
| let profile = Profile(uri: profileURI, alias: vCard.familyName, photo: imageString, type: type.rawValue) |
| return profile |
| } |
| |
| private func saveProfile(profile: Profile, path: String) throws { |
| let url = URL(fileURLWithPath: path) |
| let contactCard = CNMutableContact() |
| if let name = profile.alias { |
| contactCard.familyName = name |
| } |
| contactCard.phoneNumbers = [CNLabeledValue(label: CNLabelPhoneNumberiPhone, value: CNPhoneNumber(stringValue: profile.uri))] |
| if let photo = profile.photo { |
| contactCard.imageData = NSData(base64Encoded: photo, |
| options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? |
| } |
| let data = try CNContactVCardSerialization.dataWithImageAndUUID(from: contactCard, andImageCompression: 40000) |
| try data?.write(to: url) |
| } |
| |
| func getConversationsFor(contactUri: String, accountId: String) throws -> Int64? { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { |
| throw DBBridgingError.getConversationFailed |
| } |
| if let contactConversations = try self.conversationHelper |
| .selectConversationsForProfile(profileUri: contactUri, dataBase: dataBase), |
| let conv = contactConversations.first { |
| return conv.id |
| } |
| return nil |
| } |
| |
| private func getConversationsFor(contactUri: String, |
| createIfNotExists: Bool, dataBase: Connection, accountId: String) throws -> Int64? { |
| if let contactConversations = try self.conversationHelper |
| .selectConversationsForProfile(profileUri: contactUri, dataBase: dataBase), |
| let conv = contactConversations.first { |
| return conv.id |
| } |
| if !createIfNotExists { |
| return nil |
| } |
| let conversationID = Int64(arc4random_uniform(10000000)) |
| do { |
| _ = try self.getProfile(for: contactUri, createIfNotExists: true, accountId: accountId) |
| } catch {} |
| let conversationForContact = Conversation(conversationID, contactUri) |
| if !self.conversationHelper.insert(item: conversationForContact, dataBase: dataBase) { |
| return nil |
| } |
| return try self.conversationHelper |
| .selectConversationsForProfile(profileUri: contactUri, dataBase: dataBase)?.first?.id |
| } |
| |
| // MARK: Location sharing |
| func isFirstLocationIncomingUpdate(incoming: Bool, peerUri: String, accountId: String) -> Bool? { |
| do { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { return nil } |
| |
| let conversationId = try self.getConversationsFor(contactUri: peerUri, createIfNotExists: true, dataBase: dataBase, accountId: accountId) |
| let interactions = try self.interactionHepler.selectInteractionsForConversation(conv: conversationId!, dataBase: dataBase) |
| |
| var isFirst = true |
| for (interaction) in interactions! where interaction.type == InteractionType.location.rawValue && interaction.incoming == incoming { |
| isFirst = false |
| break |
| } |
| return isFirst |
| } catch { |
| return nil |
| } |
| } |
| |
| func deleteLocationUpdates(incoming: Bool, peerUri: String, accountId: String) -> Completable { |
| return Completable.create(subscribe: { [weak self] completable in |
| do { |
| guard let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) else { throw DataAccessError.datastoreConnectionError } |
| let conversationId = try self.getConversationsFor(contactUri: peerUri, createIfNotExists: true, dataBase: dataBase, accountId: accountId) |
| |
| let predicat: Expression<Bool> = (self.interactionHepler.conversation == conversationId! && |
| self.interactionHepler.type == InteractionType.location.rawValue && |
| self.interactionHepler.incoming == incoming) |
| |
| _ = try self.interactionHepler.deleteInteractions(where: predicat, dataBase: dataBase) |
| completable(.completed) |
| } catch { |
| completable(.error(DBBridgingError.deleteMessageFailed)) |
| } |
| return Disposables.create { } |
| }) |
| } |
| |
| func deleteAllLocationUpdates(accountIds: [String]) -> Bool { |
| var didNotFailOnce = true |
| for accountId in accountIds { |
| do { |
| guard let dataBase = self.dbConnections.forAccount(account: accountId) else { throw DataAccessError.datastoreConnectionError } |
| |
| let predicat: Expression<Bool> = (self.interactionHepler.type == InteractionType.location.rawValue) |
| |
| _ = try self.interactionHepler.deleteInteractions(where: predicat, dataBase: dataBase) |
| } catch { |
| didNotFailOnce = false |
| } |
| } |
| return didNotFailOnce |
| } |
| } |