diff --git a/Ring/Cartfile b/Ring/Cartfile index 6c85a7b9b23181e901f2217bb372af2a8b6552fa..a49a381e1c971d11e171dc333b88f6896580e1f3 100644 --- a/Ring/Cartfile +++ b/Ring/Cartfile @@ -6,3 +6,4 @@ github "SwiftyBeaver/SwiftyBeaver" ~> 1.0 github "ViccAlexander/Chameleon" github "andreamazz/AMPopTip" github "ashleymills/Reachability.swift" +github "stephencelis/SQLite.swift" ~> 0.11.4 diff --git a/Ring/Cartfile.resolved b/Ring/Cartfile.resolved index b92731203e51a47bcf3cdcd9ebf6459a9cfece24..c02fcfa120af6af18637c026330ae283e34e1372 100644 --- a/Ring/Cartfile.resolved +++ b/Ring/Cartfile.resolved @@ -8,3 +8,4 @@ github "andreamazz/AMPopTip" "3.0.2" github "ashleymills/Reachability.swift" "v4.1.0" github "pkluz/PKHUD" "5.0.0" github "realm/realm-cocoa" "v3.0.1" +github "stephencelis/SQLite.swift" "0.11.4" diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 110d3833157572fc1352cdab1ebf6e35a610e87d..637a573d4e3cce8294108ea5057e97997c1912ac 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -80,6 +80,12 @@ 04399B141D1C341A00E99CD9 /* libx264.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE21D1C341A00E99CD9 /* libx264.a */; }; 04399B151D1C341A00E99CD9 /* libyaml-cpp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */; }; 0586C94B1F684DF600613517 /* UIImage+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0586C94A1F684DF600613517 /* UIImage+Helpers.swift */; }; + 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 */; }; + 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 */; }; @@ -318,6 +324,12 @@ 04399AE21D1C341A00E99CD9 /* libx264.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libx264.a; path = ../fat/lib/libx264.a; sourceTree = ""; }; 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libyaml-cpp.a"; path = "../fat/lib/libyaml-cpp.a"; sourceTree = ""; }; 0586C94A1F684DF600613517 /* UIImage+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helpers.swift"; sourceTree = ""; }; + 0E0FF1A61FC38070003898C2 /* SQLite.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SQLite.framework; path = Carthage/Build/iOS/SQLite.framework; sourceTree = ""; }; + 0E0FF1A91FC3843E003898C2 /* RingDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingDB.swift; sourceTree = ""; }; + 0E0FF1AE1FC38CBC003898C2 /* ProfileDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDataHelper.swift; sourceTree = ""; }; + 0E0FF1B41FC3947B003898C2 /* DBBridging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBBridging.swift; sourceTree = ""; }; + 0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDataHepler.swift; sourceTree = ""; }; + 0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionDataHelper.swift; sourceTree = ""; }; 0E2D5F521F9145C800D574BF /* LinkNewDeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceCell.swift; sourceTree = ""; }; 0E2D5F541F9145F200D574BF /* LinkNewDeviceCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LinkNewDeviceCell.xib; sourceTree = ""; }; 0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellGenerated.swift; sourceTree = ""; }; @@ -466,6 +478,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0E0FF1A71FC38070003898C2 /* SQLite.framework in Frameworks */, 5C093F011FB495830011D90E /* Differentiator.framework in Frameworks */, 0ED666101F9FED1C00743D42 /* AMPopTip.framework in Frameworks */, 0EEFBA3C1F83DA21000EDBAD /* libsecp256k1.a in Frameworks */, @@ -576,6 +589,7 @@ 02AED8171DD4C4B000F740BA /* Frameworks */ = { isa = PBXGroup; children = ( + 0E0FF1A61FC38070003898C2 /* SQLite.framework */, 5C093F001FB495830011D90E /* Differentiator.framework */, 0ED6660F1F9FED1C00743D42 /* AMPopTip.framework */, 62DFAB2B1F9FF030002D6F9C /* Reachability.framework */, @@ -710,6 +724,7 @@ 043999F51D1C2D9D00E99CD9 /* Ring */ = { isa = PBXGroup; children = ( + 0E0FF1A81FC38409003898C2 /* Database */, 02E1A0261DDE4C2E00D75B59 /* Services */, 1A2D18B81F2916BA00B2C785 /* Models */, 564C44571E8D7F68000F92B1 /* Constants */, @@ -819,6 +834,26 @@ name = SYS_DEPS; sourceTree = ""; }; + 0E0FF1A81FC38409003898C2 /* Database */ = { + isa = PBXGroup; + children = ( + 0E0FF1AB1FC38BD1003898C2 /* DBHelpers */, + 0E0FF1A91FC3843E003898C2 /* RingDB.swift */, + 0E0FF1B41FC3947B003898C2 /* DBBridging.swift */, + ); + path = Database; + sourceTree = ""; + }; + 0E0FF1AB1FC38BD1003898C2 /* DBHelpers */ = { + isa = PBXGroup; + children = ( + 0E0FF1AE1FC38CBC003898C2 /* ProfileDataHelper.swift */, + 0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */, + 0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */, + ); + path = DBHelpers; + sourceTree = ""; + }; 0E5AFE0A1F8EBC040040D539 /* Cells */ = { isa = PBXGroup; children = ( @@ -1317,6 +1352,7 @@ "$(SRCROOT)/Carthage/build/iOS/AMPopTip.framework", "$(SRCROOT)/Carthage/Build/iOS/Reachability.framework", "$(SRCROOT)/Carthage/Build/iOS/Differentiator.framework", + "$(SRCROOT)/Carthage/Build/iOS/SQLite.framework", ); name = "⚙️ Copy Frameworks"; outputPaths = ( @@ -1332,6 +1368,7 @@ "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Chameleon.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/AMPopTip.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Differentiator.framework", + "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SQLite.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1406,9 +1443,12 @@ 1ABE07D31F0D8FE800D36361 /* Storyboards.swift in Sources */, 0EDE34C71F868E1200FFA15C /* EditProfileViewController.swift in Sources */, 62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */, + 0E0FF1B51FC3947B003898C2 /* DBBridging.swift in Sources */, 1A2D18E51F29197100B2C785 /* MessageAccessoryView.swift in Sources */, + 0E0FF1B91FC398C5003898C2 /* InteractionDataHelper.swift in Sources */, 1A2D18C61F29180700B2C785 /* ConversationModel.swift in Sources */, 0EE1B54E1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift in Sources */, + 0E0FF1AA1FC3843E003898C2 /* RingDB.swift in Sources */, 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */, 1A3CA32D1F13DA7200283748 /* Chameleon+Ring.swift in Sources */, 0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */, @@ -1436,6 +1476,7 @@ 1A2D18B71F29164700B2C785 /* SmartlistViewModel.swift in Sources */, 04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */, 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */, + 0E0FF1AF1FC38CBC003898C2 /* ProfileDataHelper.swift in Sources */, 563AEC771EA664C0003A5641 /* RegistrationResponse.m in Sources */, 564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */, 0EB479951FA28A7300106AFD /* ButtonTransparentBackground.swift in Sources */, @@ -1459,6 +1500,7 @@ 564C44621E943DE6000F92B1 /* NameService.swift in Sources */, 1A5DC02C1F3565250075E8EF /* MeViewController.swift in Sources */, 1A2041801F1E903B00C08435 /* CreateProfileViewController.swift in Sources */, + 0E0FF1B71FC398B3003898C2 /* ConversationDataHepler.swift in Sources */, 62DFAB2E1F9FF0D0002D6F9C /* NetworkService.swift in Sources */, 0EDE34C91F8691BB00FFA15C /* EditProfileViewModel.swift in Sources */, 04399AAD1D1C304300E99CD9 /* DRingAdapter.mm in Sources */, diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift index 7a73e47b9d156025c16bbf182da89562e5849a1d..1ef1b75ef3c39f82832a322ff0ee958e639004ab 100644 --- a/Ring/Ring/AppDelegate.swift +++ b/Ring/Ring/AppDelegate.swift @@ -91,7 +91,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.window?.rootViewController = self.appCoordinator.rootViewController self.window?.makeKeyAndVisible() self.appCoordinator.start() - + self.startDB() return true } @@ -124,4 +124,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { log.error("Unknown error in Daemon stop.") } } + + private func startDB() { + do { + let dbManager = DBBridging(profileHepler: ProfileDataHelper(), + conversationHelper: ConversationDataHelper(), + interactionHepler: InteractionDataHelper()) + try dbManager.start() + } catch { + let time = DispatchTime.now() + 1 + DispatchQueue.main.asyncAfter(deadline: time) { + self.appCoordinator.showDatabaseError() + } + } + } } diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift index dc9a981c2e81866421be1150209abea6995ce75a..572d91893c6fd01da690fc53532eab63ce1fce9f 100644 --- a/Ring/Ring/Constants/Generated/Strings.swift +++ b/Ring/Ring/Constants/Generated/Strings.swift @@ -30,6 +30,10 @@ enum L10n { static let accountNoNetworkMessage = L10n.tr("Localizable", "alerts.accountNoNetworkMessage") /// Can't connect to the network static let accountNoNetworkTitle = L10n.tr("Localizable", "alerts.accountNoNetworkTitle") + /// Please close application and try to open it again + static let dbFailedMessage = L10n.tr("Localizable", "alerts.dbFailedMessage") + /// An error happned when launching Ring + static let dbFailedTitle = L10n.tr("Localizable", "alerts.dbFailedTitle") /// Cancel static let profileCancelPhoto = L10n.tr("Localizable", "alerts.profileCancelPhoto") /// Take photo diff --git a/Ring/Ring/Coordinators/AppCoordinator.swift b/Ring/Ring/Coordinators/AppCoordinator.swift index 000c0bcf361f50b70bb449c0f6839b0e6592b50e..e431c56eb357f949294276224b44be62fbf60e39 100644 --- a/Ring/Ring/Coordinators/AppCoordinator.swift +++ b/Ring/Ring/Coordinators/AppCoordinator.swift @@ -81,6 +81,13 @@ class AppCoordinator: Coordinator, StateableResponsive { }).disposed(by: self.disposeBag) } + func showDatabaseError() { + let alertController = UIAlertController(title: L10n.Alerts.dbFailedTitle, + message: L10n.Alerts.dbFailedMessage, + preferredStyle: .alert) + self.present(viewController: alertController, withStyle: .present, withAnimation: false) + } + private func showWalkthrough () { let walkthroughCoordinator = WalkthroughCoordinator(with: self.injectionBag) self.addChildCoordinator(childCoordinator: walkthroughCoordinator) diff --git a/Ring/Ring/Database/DBBridging.swift b/Ring/Ring/Database/DBBridging.swift new file mode 100644 index 0000000000000000000000000000000000000000..1b92bb8c519346c070e0db260977a8e860ad6f02 --- /dev/null +++ b/Ring/Ring/Database/DBBridging.swift @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Kateryna Kostiuk + * + * 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 + } + } +} + +enum InteractionType: String { + case invalid = "INVALID" + case text = "TEXT" + case call = "CALL" + case contact = "CONTACT" +} + +final class DBBridging { + + 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() + } +} diff --git a/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift b/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift new file mode 100644 index 0000000000000000000000000000000000000000..bf890f80f57a7dccee2b0bfcb959a04101c48d53 --- /dev/null +++ b/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Kateryna Kostiuk + * + * 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 SQLite + +typealias Conversation = ( + id: Int64, + participantID: Int64 +) + +final class ConversationDataHelper { + let table = RingDB.instance.tableConversations + let id = Expression("id") + let participantId = Expression("participant_id") + + func createTable() throws { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + do { + try dataBase.run(table.create(ifNotExists: true) { table in + table.column(id) + table.column(participantId) + table.foreignKey(participantId, references: RingDB.instance.tableProfiles, id, delete: .noAction) + }) + + } catch _ { + print("Table already exists") + } + + } + + func insert(item: Conversation) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let query = table.insert(id <- item.id, + participantId <- item.participantID) + do { + let rowId = try dataBase.run(query) + guard rowId > 0 else { + return false + } + return true + } catch _ { + return false + } + } + + func delete (item: Conversation) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let conversationId = item.id + let query = table.filter(id == conversationId) + do { + let deletedRows = try dataBase.run(query.delete()) + guard deletedRows == 1 else { + return false + } + return true + } catch _ { + return false + } + } + + func selectConversations (conversationId: Int64) throws -> [Conversation]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(id == conversationId) + var conversations = [Conversation]() + let items = try dataBase.prepare(query) + for item in items { + conversations.append(Conversation(id: item[id], participantID: item[participantId])) + } + return conversations + } + + func selectAll() throws -> [Conversation]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + var conversations = [Conversation]() + let items = try dataBase.prepare(table) + for item in items { + conversations.append(Conversation(id: item[id], participantID: item[participantId])) + } + return conversations + } + + func selectConversationsForProfile(profileId: Int64) throws -> [Conversation]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + var conversations = [Conversation]() + let query = table.filter(participantId == profileId) + let items = try dataBase.prepare(query) + for item in items { + conversations.append(Conversation(id: item[id], participantID: item[participantId])) + } + return conversations + } + + func deleteConversations(conversationID: Int64) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let query = table.filter(id == conversationID) + do { + if try dataBase.run(query.delete()) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } +} diff --git a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift new file mode 100644 index 0000000000000000000000000000000000000000..b871c564acaa82e2bc1e6d496d3ac90097a98a83 --- /dev/null +++ b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Kateryna Kostiuk + * + * 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 SQLite + +typealias Interaction = ( + id: Int64, + accountID: Int64, + authorID: Int64, + conversationID: Int64, + timestamp: Int64, + body: String, + type: String, + status: String, + daemonID: String +) + +final class InteractionDataHelper { + + let table = RingDB.instance.tableInteractionss + let id = Expression("id") + let accountId = Expression("account_id") + let authorId = Expression("author_id") + let conversationId = Expression("conversation_id") + let timestamp = Expression("timestamp") + let body = Expression("body") + let type = Expression("type") + let status = Expression("status") + let daemonId = Expression("daemon_id") + + func createTable() throws { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + do { + try dataBase.run(table.create(ifNotExists: true) { table in + table.column(id, primaryKey: .autoincrement) + table.column(accountId) + table.column(authorId) + table.column(conversationId) + table.column(timestamp) + table.column(body) + table.column(type) + table.column(status) + table.column(daemonId) + table.foreignKey(accountId, + references: RingDB.instance.tableProfiles, id, delete: .noAction) + table.foreignKey(authorId, + references: RingDB.instance.tableProfiles, id, delete: .noAction) + table.foreignKey(conversationId, + references: RingDB.instance.tableConversations, id, delete: .noAction) + }) + + } catch _ { + print("Table already exists") + } + } + + func insert(item: Interaction) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + + let query = table.insert(accountId <- item.accountID, + authorId <- item.authorID, + conversationId <- item.conversationID, + timestamp <- item.timestamp, + body <- item.body, + type <- item.type, + status <- item.status, + daemonId <- item.daemonID) + do { + let rowId = try dataBase.run(query) + guard rowId > 0 else { + return false + } + return true + } catch _ { + return false + } + } + + func delete (item: Interaction) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let profileId = item.id + let query = table.filter(id == profileId) + do { + let deletedRows = try dataBase.run(query.delete()) + guard deletedRows == 1 else { + return false + } + return true + } catch _ { + return false + } + } + + func selectInteraction (interactionId: Int64) throws -> Interaction? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(id == interactionId) + let items = try dataBase.prepare(query) + for item in items { + return Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId]) + } + return nil + } + + func selectAll() throws -> [Interaction]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + var interactions = [Interaction]() + let items = try dataBase.prepare(table) + for item in items { + interactions.append(Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId])) + } + return interactions + } + + func selectInteractionsForAccount (accountID: Int64) throws -> [Interaction]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(accountId == accountID) + var interactions = [Interaction]() + let items = try dataBase.prepare(query) + for item in items { + interactions.append(Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId])) + } + return interactions + } + + func selectInteractionsForConversation (conversationID: Int64) throws -> [Interaction]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(conversationId == conversationID) + var interactions = [Interaction]() + let items = try dataBase.prepare(query) + for item in items { + interactions.append(Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId])) + } + return interactions + } + + func selectInteractionsForConversationWithAccount (conversationID: Int64, + accountProfileID: Int64) throws -> [Interaction]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(conversationId == conversationID && (accountId == accountProfileID)) + var interactions = [Interaction]() + let items = try dataBase.prepare(query) + for item in items { + interactions.append(Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId])) + } + return interactions + } + + func selectInteractionWithDaemonId(interactionDaemonID: String) throws -> Interaction? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(daemonId == interactionDaemonID) + var interactions = [Interaction]() + let items = try dataBase.prepare(query) + for item in items { + interactions.append(Interaction(id: item[id], + accountID: item[accountId], + authorID: item[authorId], + conversationID: item[conversationId], + timestamp: item[timestamp], + body: item[body], + type: item[type], + status: item[status], + daemonID: item[daemonId])) + } + if interactions.isEmpty { + return nil + } + + if interactions.count > 1 { + return nil + } + + return interactions.first + } + + func updateInteractionWithDaemonID(interactionDaemonID: String, interactionStatus: String) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let query = table.filter(daemonId == interactionDaemonID) + do { + if try dataBase.run(query.update(status <- interactionStatus)) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } + + func updateInteractionWithID(interactionID: Int64, interactionStatus: String) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let query = table.filter(id == interactionID) + do { + if try dataBase.run(query.update(status <- interactionStatus)) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } + + func deleteInteractionsForConversation(convID: Int64) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let query = table.filter(conversationId == convID) + do { + if try dataBase.run(query.delete()) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } +} diff --git a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift new file mode 100644 index 0000000000000000000000000000000000000000..4d31d444e7b513683ec8f2220ebcc920d14e8bd6 --- /dev/null +++ b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Kateryna Kostiuk + * + * 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 SQLite +import SwiftyBeaver + +typealias Profile = ( + id: Int64, + uri: String, + alias: String?, + photo: String?, + type: String, + status: String +) + +final class ProfileDataHelper { + let table = RingDB.instance.tableProfiles + let id = Expression("id") + let uri = Expression("uri") + let alias = Expression("alias") + let photo = Expression("photo") + let type = Expression("type") + let status = Expression("status") + private let log = SwiftyBeaver.self + + func createTable() throws { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + do { + try dataBase.run(table.create(ifNotExists: true) { table in + table.column(id, primaryKey: .autoincrement) + table.column(uri) + table.column(alias) + table.column(photo) + table.column(type) + table.column(status) + }) + } catch _ { + log.error("Table exists") + } + } + + func insert(item: Profile) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + + let query = table.insert(uri <- item.uri, + alias <- item.alias, + photo <- item.photo, + type <- item.type, + status <- item.status) + do { + let rowId = try dataBase.run(query) + guard rowId > 0 else { + return false + } + return true + } catch _ { + return false + } + + } + + func delete(item: Profile) -> Bool { + guard let dataBase = RingDB.instance.ringDB else { + return false + } + let profileId = item.id + let query = table.filter(id == profileId) + do { + let deletedRows = try dataBase.run(query.delete()) + guard deletedRows == 1 else { + return false + } + return true + } catch _ { + return false + } + } + + func selectProfile(profileId: Int64) throws -> Profile? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(id == profileId) + let items = try dataBase.prepare(query) + for item in items { + return Profile(id: item[id], uri: item[uri], alias: item[alias], + photo: item[photo], type: item[type], status: item[status]) + } + return nil + } + + func selectAll() throws -> [Profile]? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + var profiles = [Profile]() + let items = try dataBase.prepare(table) + for item in items { + profiles.append(Profile(id: item[id], uri: item[uri], alias: item[alias], + photo: item[photo], type: item[type], status: item[status])) + } + return profiles + } + + func selectProfile(accountURI: String) throws -> Profile? { + guard let dataBase = RingDB.instance.ringDB else { + throw DataAccessError.datastoreConnectionError + } + let query = table.filter(uri == accountURI) + let items = try dataBase.prepare(query) + // for one URI we should have only one profile + for item in items { + return Profile(id: item[id], uri: item[uri], alias: item[alias], + photo: item[photo], type: item[type], status: item[status]) + } + return nil + } +} diff --git a/Ring/Ring/Database/RingDB.swift b/Ring/Ring/Database/RingDB.swift new file mode 100644 index 0000000000000000000000000000000000000000..8fce78b5046b18243ae32f0bd6ab75a19bc4da12 --- /dev/null +++ b/Ring/Ring/Database/RingDB.swift @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Kateryna Kostiuk + * + * 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 SQLite +import SwiftyBeaver + +enum DataAccessError: Error { + case datastoreConnectionError + case databaseError +} + +final class RingDB { + static let instance = RingDB() + let ringDB: Connection? + private let log = SwiftyBeaver.self + private let dbName = "ring.db" + + //tables + var tableProfiles = Table("profiles") + var tableConversations = Table("conversations") + var tableInteractionss = Table("interactions") + + private init() { + let path = NSSearchPathForDirectoriesInDomains( + .documentDirectory, .userDomainMask, true + ).first! + + do { + ringDB = try Connection("\(path)/" + dbName) + } catch { + ringDB = nil + log.error("Unable to open database") + } + } +} diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings index 9e6303b54b2292448990a28b456a53b828a676e0..965333534bb85e80a5755f917124ee15bc8eb337 100644 --- a/Ring/Ring/Resources/en.lproj/Localizable.strings +++ b/Ring/Ring/Resources/en.lproj/Localizable.strings @@ -81,6 +81,8 @@ "alerts.profileUploadPhoto" = "Upload photo"; "alerts.profileCancelPhoto" = "Cancel"; "alerts.accountLinkedTitle" = "Linking account"; +"alerts.dbFailedTitle" = "An error happned when launching Ring"; +"alerts.dbFailedMessage" = "Please close application and try to open it again"; //Account Page "accountPage.devicesListHeader" = "Devices";