AccountsService.swift 16.4 KB
Newer Older
1
/*
2
 *  Copyright (C) 2017 Savoir-faire Linux Inc.
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 *
 *  Authors: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com>
 *           Romain Bertozzi <romain.bertozzi@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.
 */

22
23
import RxCocoa
import RxSwift
24
import RealmSwift
25
import SwiftyBeaver
26

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
27
28
29
30
enum LinkNewDeviceError: Error {
    case unknownError
}

Romain Bertozzi's avatar
Romain Bertozzi committed
31
enum AddAccountError: Error {
32
33
    case templateNotConform
    case unknownError
Romain Bertozzi's avatar
Romain Bertozzi committed
34
35
}

36
37
class AccountsService: AccountAdapterDelegate {
    // MARK: Private members
38
39
40
41
42
43

    /**
     logguer
     */
    private let log = SwiftyBeaver.self

44
    /**
45
     Used to register the service to daemon events, injected by constructor.
46
     */
47
    fileprivate let accountAdapter: AccountAdapter
48

49
50
51
52
53
54
    /**
     Fileprivate Accounts list.
     Can be used for all the operations, but won't be accessed from outside this file.

     - SeeAlso: `accounts`
     */
55
    fileprivate var accountList: [AccountModel]
56

57
58
59
60
61
62
63
    fileprivate let disposeBag = DisposeBag()

    /**
     PublishSubject forwarding AccountRxEvent events.
     This stream is used strictly inside this service.
     External observers should use the public shared responseStream.

64
     - SeeAlso: `ServiceEvent`
65
66
     - SeeAlso: `sharedResponseStream`
     */
67
    fileprivate let responseStream = PublishSubject<ServiceEvent>()
68

69
70
71
72
73
    // MARK: - Public members
    /**
     Accounts list public interface.
     Can be used to access by constant the list of accounts.
     */
74
    var accounts: [AccountModel] {
75
76
77
78
79
80
81
82
83
        set {
            accountList = newValue
        }
        get {
            let lAccounts = accountList
            return lAccounts
        }
    }

84
85
86
87
88
    /**
     Public shared stream forwarding the events of the responseStream.
     External observers must subscribe to this stream to get results.

     - SeeAlso: `responseStream`
89
     - SeeAlso: `ServiceEvent`
90
     */
91
    var sharedResponseStream: Observable<ServiceEvent>
92

93
94
95
96
97
98
99
100
    /**
     Current account computed property

     This will reorganize the order of the accounts. The current account needs to be first.

     - Parameter account: the account to set as current.
     */

101
    var currentAccount: AccountModel? {
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
        get {
            return self.accountList.first
        }

        set {
            //Get the current account from account list if already exists
            let currentAccount = self.accountList.filter({ account in
                return account == newValue
            }).first

            //If current account already exists in the list, move it to the first index
            if let currentAccount = currentAccount {
                let index = self.accountList.index(of: currentAccount)
                self.accountList.remove(at: index!)
                self.accountList.insert(currentAccount, at: 0)
            } else {
                self.accountList.append(newValue!)
            }
        }
    }
Romain Bertozzi's avatar
Romain Bertozzi committed
122

123
    init(withAccountAdapter accountAdapter: AccountAdapter) {
124
125
        self.accountList = []

126
        self.responseStream.disposed(by: disposeBag)
127
128
129

        //~ Create a shared stream based on the responseStream one.
        self.sharedResponseStream = responseStream.share()
130

131
132
133
134
        self.accountAdapter = accountAdapter
        //~ Registering to the accountAdatpter with self as delegate in order to receive delegation
        //~ callbacks.
        AccountAdapter.delegate = self
135
136
137

    }

Hadrien De Sousa's avatar
Hadrien De Sousa committed
138
    fileprivate func loadAccountsFromDaemon() {
139
        for accountId in accountAdapter.getAccountList() {
140
141
142
            if  let id = accountId as? String {
                self.accountList.append(AccountModel(withAccountId: id))
            }
143
144
145
        }

        reloadAccounts()
146
147
    }

Hadrien De Sousa's avatar
Hadrien De Sousa committed
148
149
150
151
152
153
154
    func loadAccounts() -> Single<[AccountModel]> {
        return Single<[AccountModel]>.just({
            loadAccountsFromDaemon()
            return accountList
        }())
    }

155
156
    // MARK: - Methods
    func hasAccounts() -> Bool {
157
        return !accountList.isEmpty
158
159
    }

160
161
162
163
    fileprivate func reloadAccounts() {
        for account in accountList {
            account.details = self.getAccountDetails(fromAccountId: account.id)
            account.volatileDetails = self.getVolatileAccountDetails(fromAccountId: account.id)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
164
            account.devices = getKnownRingDevices(fromAccountId: account.id)
165
166

            do {
167
168
                let credentialDetails = try self.getAccountCredentials(fromAccountId: account.id)
                account.credentialDetails.removeAll()
169
                account.credentialDetails.append(contentsOf: credentialDetails)
170
            } catch {
171
                log.error("\(error)")
172
173
            }
        }
174
175
    }

Romain Bertozzi's avatar
Romain Bertozzi committed
176
177
178
179
180
181
182
    /**
     Entry point to create a brand-new Ring account.

     - Parameter username: the username chosen by the user, if any
     - Parameter password: the password chosen by the user

     */
183
    func addRingAccount(withUsername username: String?, password: String) {
Romain Bertozzi's avatar
Romain Bertozzi committed
184
185
186
        do {
            var ringDetails = try self.getRingInitialAccountDetails()
            if username != nil {
187
                ringDetails.updateValue(username!, forKey: ConfigKey.accountRegisteredName.rawValue)
Romain Bertozzi's avatar
Romain Bertozzi committed
188
            }
189
            ringDetails.updateValue(password, forKey: ConfigKey.archivePassword.rawValue)
Romain Bertozzi's avatar
Romain Bertozzi committed
190
191
            let accountId = self.accountAdapter.addAccount(ringDetails)
            guard accountId != nil else {
192
                throw AddAccountError.unknownError
Romain Bertozzi's avatar
Romain Bertozzi committed
193
194
            }

195
            var account = self.getAccount(fromAccountId: accountId!)
Romain Bertozzi's avatar
Romain Bertozzi committed
196
197
198
199

            if account == nil {
                let details = self.getAccountDetails(fromAccountId: accountId!)
                let volatileDetails = self.getVolatileAccountDetails(fromAccountId: accountId!)
200
                let credentials = try self.getAccountCredentials(fromAccountId: accountId!)
Romain Bertozzi's avatar
Romain Bertozzi committed
201
202
                let devices = getKnownRingDevices(fromAccountId: accountId!)

203
                account = try AccountModel(withAccountId: accountId!,
Romain Bertozzi's avatar
Romain Bertozzi committed
204
205
206
207
208
209
                                                  details: details,
                                                  volatileDetails: volatileDetails,
                                                  credentials: credentials,
                                                  devices: devices)
                //TODO: set registration state as ready for a SIP account

210
                let accountModelHelper = AccountModelHelper(withAccount: account!)
211
212
213
                var accountAddedEvent = ServiceEvent(withEventType: .accountAdded)
                accountAddedEvent.addEventInput(.id, value: account?.id)
                accountAddedEvent.addEventInput(.state, value: accountModelHelper.getRegistrationState())
Romain Bertozzi's avatar
Romain Bertozzi committed
214
215
                self.responseStream.onNext(accountAddedEvent)
            }
216
217

            self.currentAccount = account
218
        } catch {
219
            self.responseStream.onError(error)
Romain Bertozzi's avatar
Romain Bertozzi committed
220
221
222
        }
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
    func linkToRingAccount(withPin pin: String, password: String) {
        do {
            var ringDetails = try self.getRingInitialAccountDetails()
            ringDetails.updateValue(password, forKey: ConfigKey.archivePassword.rawValue)
            ringDetails.updateValue(pin, forKey: ConfigKey.archivePIN.rawValue)
            let accountId = self.accountAdapter.addAccount(ringDetails)
            guard accountId != nil else {
                throw AddAccountError.unknownError
            }

            var account = self.getAccount(fromAccountId: accountId!)

            if account == nil {
                let details = self.getAccountDetails(fromAccountId: accountId!)
                let volatileDetails = self.getVolatileAccountDetails(fromAccountId: accountId!)
                let credentials = try self.getAccountCredentials(fromAccountId: accountId!)
                let devices = getKnownRingDevices(fromAccountId: accountId!)

                account = try AccountModel(withAccountId: accountId!,
                                           details: details,
                                           volatileDetails: volatileDetails,
                                           credentials: credentials,
                                           devices: devices)

                let accountModelHelper = AccountModelHelper(withAccount: account!)
                var accountAddedEvent = ServiceEvent(withEventType: .accountAdded)
                accountAddedEvent.addEventInput(.id, value: account?.id)
                accountAddedEvent.addEventInput(.state, value: accountModelHelper.getRegistrationState())
                self.responseStream.onNext(accountAddedEvent)
            }
            self.currentAccount = account
        } catch {
            self.responseStream.onError(error)
        }
    }

Romain Bertozzi's avatar
Romain Bertozzi committed
259
260
261
262
263
264
    /**
     Entry point to create a brand-new SIP account.

     Not supported yet.
     */
    fileprivate func addSipAccount() {
265
        log.info("Not supported yet")
Romain Bertozzi's avatar
Romain Bertozzi committed
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
    }

    /**
     Gets an account from the list of accounts handled by the application.

     - Parameter id: the id of the account to get.

     - Returns: the account if found, nil otherwise.
     */
    func getAccount(fromAccountId id: String) -> AccountModel? {
        for account in self.accountList {
            if id.compare(account.id) == ComparisonResult.orderedSame {
                return account
            }
        }
        return nil
    }

    /**
     Gets all the details of an account from the daemon.

     - Parameter id: the id of the account.

     - Returns: the details of the accounts.
     */
291
    func getAccountDetails(fromAccountId id: String) -> AccountConfigModel {
Romain Bertozzi's avatar
Romain Bertozzi committed
292
        let details: NSDictionary = accountAdapter.getAccountDetails(id) as NSDictionary
293
        let accountDetailsDict = details as NSDictionary? as? [String: String] ?? nil
294
295
        let accountDetails = AccountConfigModel(withDetails: accountDetailsDict)
        return accountDetails
Romain Bertozzi's avatar
Romain Bertozzi committed
296
297
298
299
300
301
302
303
304
    }

    /**
     Gets all the volatile details of an account from the daemon.

     - Parameter id: the id of the account.

     - Returns: the volatile details of the accounts.
     */
305
    func getVolatileAccountDetails(fromAccountId id: String) -> AccountConfigModel {
Romain Bertozzi's avatar
Romain Bertozzi committed
306
        let details: NSDictionary = accountAdapter.getVolatileAccountDetails(id) as NSDictionary
307
        let accountDetailsDict = details as NSDictionary? as? [String: String] ?? nil
308
309
        let accountDetails = AccountConfigModel(withDetails: accountDetailsDict)
        return accountDetails
Romain Bertozzi's avatar
Romain Bertozzi committed
310
311
312
313
314
315
316
317
318
    }

    /**
     Gets the credentials of an account from the daemon.

     - Parameter id: the id of the account.

     - Returns: the list of credentials.
     */
319
    func getAccountCredentials(fromAccountId id: String) throws -> [AccountCredentialsModel] {
Romain Bertozzi's avatar
Romain Bertozzi committed
320
        let creds: NSArray = accountAdapter.getCredentials(id) as NSArray
321
        let rawCredentials = creds as NSArray? as? [[String: String]] ?? nil
322
323

        if let rawCredentials = rawCredentials {
324
            var credentialsList = [AccountCredentialsModel]()
325
326
327
328
            for rawCredentials in rawCredentials {
                do {
                    let credentials = try AccountCredentialsModel(withRawaData: rawCredentials)
                    credentialsList.append(credentials)
329
                } catch CredentialsError.notEnoughData {
330
                    log.error("Not enough data to build a credential object.")
331
                    throw CredentialsError.notEnoughData
332
                } catch {
333
                    log.error("Unexpected error.")
334
                    throw AccountModelError.unexpectedError
335
336
337
338
                }
            }
            return credentialsList
        } else {
339
            throw AccountModelError.unexpectedError
340
        }
Romain Bertozzi's avatar
Romain Bertozzi committed
341
342
343
344
345
346
347
348
349
    }

    /**
     Gets the known Ring devices of an account from the daemon.

     - Parameter id: the id of the account.

     - Returns: the known Ring devices.
     */
350
351
352
353
    func getKnownRingDevices(fromAccountId id: String) -> [DeviceModel] {
        let knownRingDevices = accountAdapter.getKnownRingDevices(id)! as NSDictionary

        var devices = [DeviceModel]()
354

355
        for key in knownRingDevices.allKeys {
356
357
358
            if let key = key as? String {
                devices.append(DeviceModel(withDeviceId: key))
            }
359
360
361
        }

        return devices
Romain Bertozzi's avatar
Romain Bertozzi committed
362
363
364
365
366
367
368
    }

    /**
     Gathers all the initial default details contained by any accounts, Ring or SIP.

     - Returns the details.
     */
369
370
371
    fileprivate func getInitialAccountDetails() throws -> [String: String] {
        let details: NSMutableDictionary = accountAdapter.getAccountTemplate(AccountType.ring.rawValue)
        var accountDetails = details as NSDictionary? as? [String: String] ?? nil
Romain Bertozzi's avatar
Romain Bertozzi committed
372
        if accountDetails == nil {
373
            throw AddAccountError.templateNotConform
Romain Bertozzi's avatar
Romain Bertozzi committed
374
        }
375
376
        accountDetails!.updateValue("false", forKey: ConfigKey.videoEnabled.rawValue)
        accountDetails!.updateValue("sipinfo", forKey: ConfigKey.accountDTMFType.rawValue)
Romain Bertozzi's avatar
Romain Bertozzi committed
377
378
379
380
381
382
383
384
        return accountDetails!
    }

    /**
     Gathers all the initial default details contained in a Ring accounts.

     - Returns the details.
     */
385
    fileprivate func getRingInitialAccountDetails() throws -> [String: String] {
Romain Bertozzi's avatar
Romain Bertozzi committed
386
387
        do {
            var defaultDetails = try getInitialAccountDetails()
388
389
390
            defaultDetails.updateValue("Ring", forKey: ConfigKey.accountAlias.rawValue)
            defaultDetails.updateValue("bootstrap.ring.cx", forKey: ConfigKey.accountHostname.rawValue)
            defaultDetails.updateValue("true", forKey: ConfigKey.accountUpnpEnabled.rawValue)
Romain Bertozzi's avatar
Romain Bertozzi committed
391
392
393
            return defaultDetails
        } catch {
            throw error
394
395
396
397
398
        }
    }

    func removeAccount(_ row: Int) {
        if row < accountList.count {
399
            self.accountAdapter.removeAccount(accountList[row].id)
400
401
        }
    }
402
403
404

    // MARK: - AccountAdapterDelegate
    func accountsChanged() {
405
        log.debug("Accounts changed.")
406
        reloadAccounts()
407

408
        let event = ServiceEvent(withEventType: .accountsChanged)
409
        self.responseStream.onNext(event)
410
    }
411
412

    func registrationStateChanged(with response: RegistrationResponse) {
413
        log.debug("RegistrationStateChanged.")
414
415
        reloadAccounts()

416
417
        var event = ServiceEvent(withEventType: .registrationStateChanged)
        event.addEventInput(.registrationState, value: response.state)
418
419
420
        self.responseStream.onNext(event)
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
    func knownDevicesChanged(for account: String, devices: [String: String]) {
        reloadAccounts()
        let changedAccount = getAccount(fromAccountId: account)
        if let changedAccount = changedAccount {
            let accountHelper = AccountModelHelper(withAccount: changedAccount)
            if let  uri = accountHelper.ringId {
                var event = ServiceEvent(withEventType: .knownDevicesChanged)
                event.addEventInput(.uri, value: uri)
                self.responseStream.onNext(event)
            }
        }
    }

    func exportOnRing(withPassword password: String)
        -> Completable {
            return Completable.create { [unowned self] completable in
                let export =  self.accountAdapter.export(onRing: self.currentAccount?.id, password: password)
                if export {
                    completable(.completed)
                } else {
                    completable(.error(LinkNewDeviceError.unknownError))
                }
                return Disposables.create { }
            }
    }

    func exportOnRingEndeded(forAccout account: String, state: Int, pin: String) {

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
449
450
451
452
453
454
455
456
457
458
459
460
        let changedAccount = getAccount(fromAccountId: account)
        if let changedAccount = changedAccount {
            let accountHelper = AccountModelHelper(withAccount: changedAccount)
            if let  uri = accountHelper.ringId {
                var event = ServiceEvent(withEventType: .exportOnRingEnded)
                event.addEventInput(.uri, value: uri)
                event.addEventInput(.state, value: state)
                event.addEventInput(.pin, value: pin)
                self.responseStream.onNext(event)
            }
        }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
461
462
    }

463
}