Commit 4eca6de4 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

database: create DBManager

This patch create layer between services and ring database. It does:

- save message
- get messages
- update message
- remove conversations

Change-Id: I9f4ba857508e0ddbbaec9f7d9bcf8f92069e460a
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent e20e7d69
......@@ -83,13 +83,14 @@
0E0FF1A71FC38070003898C2 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E0FF1A61FC38070003898C2 /* SQLite.framework */; };
0E0FF1AA1FC3843E003898C2 /* RingDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1A91FC3843E003898C2 /* RingDB.swift */; };
0E0FF1AF1FC38CBC003898C2 /* ProfileDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1AE1FC38CBC003898C2 /* ProfileDataHelper.swift */; };
0E0FF1B51FC3947B003898C2 /* DBBridging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B41FC3947B003898C2 /* DBBridging.swift */; };
0E0FF1B51FC3947B003898C2 /* DBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B41FC3947B003898C2 /* DBManager.swift */; };
0E0FF1B71FC398B3003898C2 /* ConversationDataHepler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */; };
0E0FF1B91FC398C5003898C2 /* InteractionDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */; };
0E2D5F531F9145C800D574BF /* LinkNewDeviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2D5F521F9145C800D574BF /* LinkNewDeviceCell.swift */; };
0E2D5F551F9145F200D574BF /* LinkNewDeviceCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E2D5F541F9145F200D574BF /* LinkNewDeviceCell.xib */; };
0E403F811F7D797300C80BC2 /* MessageCellGenerated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */; };
0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */; };
0E570CB81FD5FD5A00A471B9 /* ConversationModel1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E570CB71FD5FD5A00A471B9 /* ConversationModel1.swift */; };
0E6949791FA7E71C0029B60A /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6949781FA7E71C0029B60A /* BaseViewController.swift */; };
0E9D84491FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9D84481FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift */; };
0E9D844B1FA7DBAA00C561EB /* ContactRequestTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9D844A1FA7DBAA00C561EB /* ContactRequestTabBarItem.swift */; };
......@@ -327,13 +328,14 @@
0E0FF1A61FC38070003898C2 /* SQLite.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SQLite.framework; path = Carthage/Build/iOS/SQLite.framework; sourceTree = "<group>"; };
0E0FF1A91FC3843E003898C2 /* RingDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingDB.swift; sourceTree = "<group>"; };
0E0FF1AE1FC38CBC003898C2 /* ProfileDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDataHelper.swift; sourceTree = "<group>"; };
0E0FF1B41FC3947B003898C2 /* DBBridging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBBridging.swift; sourceTree = "<group>"; };
0E0FF1B41FC3947B003898C2 /* DBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBManager.swift; sourceTree = "<group>"; };
0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDataHepler.swift; sourceTree = "<group>"; };
0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionDataHelper.swift; sourceTree = "<group>"; };
0E2D5F521F9145C800D574BF /* LinkNewDeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceCell.swift; sourceTree = "<group>"; };
0E2D5F541F9145F200D574BF /* LinkNewDeviceCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LinkNewDeviceCell.xib; sourceTree = "<group>"; };
0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellGenerated.swift; sourceTree = "<group>"; };
0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellGenerated.xib; sourceTree = "<group>"; };
0E570CB71FD5FD5A00A471B9 /* ConversationModel1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel1.swift; sourceTree = "<group>"; };
0E6949781FA7E71C0029B60A /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = "<group>"; };
0E9D84481FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTabBarItemViewModel.swift; sourceTree = "<group>"; };
0E9D844A1FA7DBAA00C561EB /* ContactRequestTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestTabBarItem.swift; sourceTree = "<group>"; };
......@@ -839,7 +841,7 @@
children = (
0E0FF1AB1FC38BD1003898C2 /* DBHelpers */,
0E0FF1A91FC3843E003898C2 /* RingDB.swift */,
0E0FF1B41FC3947B003898C2 /* DBBridging.swift */,
0E0FF1B41FC3947B003898C2 /* DBManager.swift */,
);
path = Database;
sourceTree = "<group>";
......@@ -1059,6 +1061,7 @@
1A2D18BD1F29180700B2C785 /* ContactModel.swift */,
1A2D18BE1F29180700B2C785 /* ConversationModel.swift */,
1A2D18BF1F29180700B2C785 /* DeviceModel.swift */,
0E570CB71FD5FD5A00A471B9 /* ConversationModel1.swift */,
);
path = Models;
sourceTree = "<group>";
......@@ -1443,7 +1446,7 @@
1ABE07D31F0D8FE800D36361 /* Storyboards.swift in Sources */,
0EDE34C71F868E1200FFA15C /* EditProfileViewController.swift in Sources */,
62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */,
0E0FF1B51FC3947B003898C2 /* DBBridging.swift in Sources */,
0E0FF1B51FC3947B003898C2 /* DBManager.swift in Sources */,
1A2D18E51F29197100B2C785 /* MessageAccessoryView.swift in Sources */,
0E0FF1B91FC398C5003898C2 /* InteractionDataHelper.swift in Sources */,
1A2D18C61F29180700B2C785 /* ConversationModel.swift in Sources */,
......@@ -1489,6 +1492,7 @@
0ED2B6FE1F96A16C001572F0 /* LinkNewDeviceViewModel.swift in Sources */,
1A3D28A91F0EBF0200B524EE /* UIView+Ring.swift in Sources */,
1A2041881F1EA1EA00C08435 /* CreateAccountViewModel.swift in Sources */,
0E570CB81FD5FD5A00A471B9 /* ConversationModel1.swift in Sources */,
62E55B6D1F758E6F00D3FEF4 /* String+Helpers.swift in Sources */,
1ABE07D21F0D8FE800D36361 /* Images.swift in Sources */,
0273C3081E0C68BF00CF00BA /* DesignableButton.swift in Sources */,
......
......@@ -127,7 +127,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private func startDB() {
do {
let dbManager = DBBridging(profileHepler: ProfileDataHelper(),
let dbManager = DBManager(profileHepler: ProfileDataHelper(),
conversationHelper: ConversationDataHelper(),
interactionHepler: InteractionDataHelper())
try dbManager.start()
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@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
enum ProfileType: String {
case ring = "RING"
case sip = "SIP"
}
enum ProfileStatus: String {
case trusted = "TRUSTED"
case untrasted = "UNTRUSTED"
}
enum MessageDirection {
case incoming
case outgoing
}
enum InteractionStatus: String {
case invalid = "INVALID"
case unknown = "UNKNOWN"
case sending = "SENDING"
case failed = "FAILED"
case succeed = "SUCCEED"
case read = "READ"
case unread = "UNREAD"
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 .read:
return MessageStatus.read
case .unread:
return MessageStatus.unknown
}
}
init(status: MessageStatus) {
switch status {
case .unknown:
self = .unknown
case .sending:
self = .sending
case .sent:
self = .succeed
case .read:
self = .read
case .failure:
self = .failed
}
}
}
enum DBBridgingError: Error {
case saveMessageFailed
case getConversationFailed
case updateMessageStatusFailed
case deleteConversationFailed
}
enum InteractionType: String {
case invalid = "INVALID"
case text = "TEXT"
case call = "CALL"
case contact = "CONTACT"
}
class DBManager {
let profileHepler: ProfileDataHelper
let conversationHelper: ConversationDataHelper
let interactionHepler: InteractionDataHelper
// 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) {
self.profileHepler = profileHepler
self.conversationHelper = conversationHelper
self.interactionHepler = interactionHepler
}
func start() throws {
try profileHepler.createTable()
try conversationHelper.createTable()
try interactionHepler.createTable()
}
func saveMessage(for accountUri: String, with contactUri: String, message: MessageModel, type: MessageDirection) -> Completable {
//create completable which will be executed on background thread
return Completable.create { [weak self] completable in
do {
guard let dataBase = RingDB.instance.ringDB else {
throw DataAccessError.datastoreConnectionError
}
//use transaction to lock access to db from other threads while the following queries are executed
try dataBase.transaction {
//profile for account should be creating when creating account
guard let accountProfile = try self?.getProfile(for: accountUri, createIfNotExists: false) else {
throw DBBridgingError.saveMessageFailed
}
guard let contactProfile = try self?.getProfile(for: contactUri, createIfNotExists: true) else {
throw DBBridgingError.saveMessageFailed
}
var author: Int64
switch type {
case .incoming:
author = contactProfile.id
case .outgoing:
author = accountProfile.id
}
guard let conversationsID = try self?.getConversationsIDBetween(accountProfileID: accountProfile.id,
contactProfileID: contactProfile.id,
createIfNotExists: true),
!conversationsID.isEmpty else {
throw DBBridgingError.saveMessageFailed
}
guard let conversationID = conversationsID.first else {
throw DBBridgingError.saveMessageFailed
}
// for now we have only one conversation between two persons(with group chat could be many)
if let success = self?.addMessageTo(conversation: conversationID, account: accountProfile.id, author: author, message: message), success {
completable(.completed)
} else {
completable(.error(DBBridgingError.saveMessageFailed))
}
}
} catch {
completable(.error(DBBridgingError.saveMessageFailed))
}
return Disposables.create { }
}
}
func getConversationsObservable(for accountID: String, accountURI: String) -> Observable<[ConversationModel1]> {
return Observable.create { observable in
do {
guard let dataBase = RingDB.instance.ringDB else {
throw DBBridgingError.getConversationFailed
}
try dataBase.transaction {
let conversations = try self.buildConversationsForAccount(accountUri: accountURI, accountID: accountID)
observable.onNext(conversations)
observable.on(.completed)
}
} catch {
observable.on(.error(DBBridgingError.getConversationFailed))
}
return Disposables.create { }
}
}
func updateMessageStatus(daemonID: String, withStatus status: MessageStatus) -> Completable {
return Completable.create { [unowned self] completable in
let success = self.interactionHepler
.updateInteractionWithDaemonID(interactionDaemonID: daemonID,
interactionStatus: InteractionStatus(status: status).rawValue)
if success {
completable(.completed)
} else {
completable(.error(DBBridgingError.updateMessageStatusFailed))
}
return Disposables.create { }
}
}
func setMessagesAsRead(messagesIDs: [Int64], withStatus status: MessageStatus) -> Completable {
return Completable.create { [unowned self] completable in
var success = true
for messageId in messagesIDs {
if !self.interactionHepler
.updateInteractionWithID(interactionID: messageId,
interactionStatus: InteractionStatus(status: status).rawValue) {
success = false
}
}
if success {
completable(.completed)
} else {
completable(.error(DBBridgingError.saveMessageFailed))
}
return Disposables.create { }
}
}
func removeConversationBetween(accountUri: String, and participantUri: String) -> Completable {
return Completable.create { [unowned self] completable in
do {
guard let dataBase = RingDB.instance.ringDB else {
throw DBBridgingError.deleteConversationFailed
}
try dataBase.transaction {
guard let accountProfile = try self.getProfile(for: accountUri, createIfNotExists: false) else {
throw DBBridgingError.deleteConversationFailed
}
guard let contactProfile = try self.getProfile(for: participantUri, createIfNotExists: false) else {
throw DBBridgingError.deleteConversationFailed
}
guard let conversationsID = try self.getConversationsIDBetween(accountProfileID: accountProfile.id, contactProfileID: contactProfile.id, createIfNotExists: true),
!conversationsID.isEmpty else {
throw DBBridgingError.deleteConversationFailed
}
let successInteraction = self.interactionHepler
.deleteInteractionsForConversation(convID: conversationsID.first!)
if successInteraction {
let successConversations = self.conversationHelper
.deleteConversations(conversationID: conversationsID.first!)
if successConversations {
completable(.completed)
} else {
completable(.error(DBBridgingError.deleteConversationFailed))
}
} else {
completable(.error(DBBridgingError.deleteConversationFailed))
}
}
} catch {
completable(.error(DBBridgingError.deleteConversationFailed))
}
return Disposables.create { }
}
}
// MARK: Private functions
private func buildConversationsForAccount(accountUri: String, accountID: String) throws -> [ConversationModel1] {
var conversationsToReturn = [ConversationModel1]()
guard let accountProfile = try self.getProfile(for: accountUri, createIfNotExists: false) else {
throw DBBridgingError.getConversationFailed
}
guard let conversationsID = try self.selectConversationsForAccount(accountProfile: accountProfile.id),
!conversationsID.isEmpty else {
// if there is no conversation for account return empty list
return conversationsToReturn
}
for conversationID in conversationsID {
guard let participants = try self.getParticipantsForConversation(conversationID: conversationID),
!participants.isEmpty else {
throw DBBridgingError.getConversationFailed
}
guard let participant =
self.filterParticipantsFor(account: accountProfile.id,
participants: participants) else {
throw DBBridgingError.getConversationFailed
}
guard let participantProfile = try self.profileHepler.selectProfile(profileId: participant) else {
throw DBBridgingError.getConversationFailed
}
let conversationModel1 = ConversationModel1(withRecipientRingId: participantProfile.uri,
accountId: accountID)
conversationModel1.participantProfile = participantProfile
var messages = [MessageModel]()
guard let interactions = try self.interactionHepler
.selectInteractionsForConversationWithAccount(conversationID: conversationID,
accountProfileID: accountProfile.id),
!interactions.isEmpty else {
throw DBBridgingError.getConversationFailed
}
for interaction in interactions {
if let message = self.convertToMessage(interaction: interaction, profile: participantProfile) {
messages.append(message)
}
}
conversationModel1.messages = messages
conversationsToReturn.append(conversationModel1)
}
return conversationsToReturn
}
private func selectConversationsForAccount(accountProfile: Int64)throws -> [Int64]? {
guard let accountConversations = try self.conversationHelper.selectConversationsForProfile(profileId: accountProfile) else {
return nil
}
return accountConversations.map({$0.id})
}
private func getParticipantsForConversation(conversationID: Int64) throws -> [Int64]? {
guard let conversations = try self.conversationHelper.selectConversations(conversationId: conversationID) else {
return nil
}
return conversations.map({$0.participantID})
}
private func filterParticipantsFor(account profileID: Int64, participants: [Int64]) -> Int64? {
var participants = participants
guard let accountProfileIndex = participants.index(of: profileID) else {
return nil
}
participants.remove(at: accountProfileIndex)
if participants.isEmpty {
return nil
}
// for now we does not support group chat, so we have only two participant for each conversation
return participants.first
}
private func isGenerated(message: MessageModel) -> Bool {
switch message.content {
case GeneratedMessageType.contactRequestAccepted.rawValue:
return true
case GeneratedMessageType.receivedContactRequest.rawValue:
return true
case GeneratedMessageType.sendContactRequest.rawValue:
return true
default:
return false
}
}
private func convertToMessage(interaction: Interaction, profile: Profile) -> MessageModel? {
let date = Date(timeIntervalSince1970: TimeInterval(interaction.timestamp))
let message = MessageModel(withId: interaction.daemonID,
receivedDate: date,
content: interaction.body,
author: profile.uri)
message.isGenerated = self.isGenerated(message: message)
if let status: InteractionStatus = InteractionStatus(rawValue: interaction.status) {
message.status = status.toMessageStatus()
}
return message
}
private func addMessageTo(conversation conversationID: Int64,
account accountProfileID: Int64,
author authorProfileID: Int64,
message: MessageModel) -> Bool {
let timeInterval = message.receivedDate.timeIntervalSince1970
let interaction = Interaction(defaultID, accountProfileID, authorProfileID,
conversationID, Int64(timeInterval),
message.content, InteractionType.text.rawValue,
InteractionStatus.unknown.rawValue, message.id)
return self.interactionHepler.insert(item: interaction)
}
private func getProfile(for profileUri: String, createIfNotExists: Bool) throws -> Profile? {
if let profile = try self.profileHepler.selectProfile(accountURI: profileUri) {
return profile
}
if !createIfNotExists {
return nil
}
// for now we use template profile
let profile = self.createTemplateRingProfile(account: profileUri)
if self.profileHepler.insert(item: profile) {
return try self.profileHepler.selectProfile(accountURI: profileUri)
}
return nil
}
private func createTemplateRingProfile(account uri: String) -> Profile {
return Profile(defaultID, uri, nil, nil, ProfileType.ring.rawValue,
ProfileStatus.untrasted.rawValue)
}
private func getConversationsIDBetween(accountProfileID: Int64,
contactProfileID: Int64,
createIfNotExists: Bool) throws -> [Int64]? {
if let accountConversations = try self.conversationHelper
.selectConversationsForProfile(profileId: accountProfileID) {
if let contactConversations = try self.conversationHelper
.selectConversationsForProfile(profileId: contactProfileID) {
let result = Array(Set(accountConversations.map({$0.id}))
.intersection(Set(contactConversations.map({$0.id}))))
if !result.isEmpty {
return result
}
}
}
if !createIfNotExists {
return nil
}
let conversationID = Int64(arc4random_uniform(10000000))
let conversationForAccount = Conversation(conversationID, accountProfileID)
let conversationForContact = Conversation(conversationID, contactProfileID)
if !self.conversationHelper.insert(item: conversationForAccount) {
return nil
}
if !self.conversationHelper.insert(item: conversationForContact) {
return nil
}
guard let accountConversations = try self.conversationHelper
.selectConversationsForProfile(profileId: accountProfileID) else {
return nil
}
guard let contactConversations = try self.conversationHelper
.selectConversationsForProfile(profileId: contactProfileID) else {
return nil
}
return Array(Set(accountConversations.map({$0.id}))
.intersection(Set(contactConversations.map({$0.id}))))
}
}
//
// ConversationModel1.swift
// Ring
//
// Created by Kateryna Kostiuk on 2017-12-04.
// Copyright © 2017 Savoir-faire Linux. All rights reserved.
//
import Foundation
class ConversationModel1 {
var messages = [MessageModel]()
var recipientRingId: String = ""
var accountId: String = ""
var participantProfile: Profile?
convenience init(withRecipientRingId recipientRingId: String, accountId: String) {
self.init()
self.recipientRingId = recipientRingId
self.accountId = accountId
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment