Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,31 @@
import Foundation
import Postbox
final class MutableAccessChallengeDataView {
var data: PostboxAccessChallengeData
init(data: PostboxAccessChallengeData) {
self.data = data
}
func replay(updatedData: PostboxAccessChallengeData?) -> Bool {
var updated = false
if let data = updatedData {
if self.data != data {
self.data = data
updated = true
}
}
return updated
}
}
public final class AccessChallengeDataView: PostboxView {
public let data: PostboxAccessChallengeData
init(_ view: MutableAccessChallengeDataView) {
self.data = view.data
}
}
@@ -0,0 +1,57 @@
import Foundation
final class AccountManagerAtomicState<Types: AccountManagerTypes>: Codable {
enum CodingKeys: String, CodingKey {
case records
case currentRecordId
case currentAuthRecord
case accessChallengeData
}
var records: [AccountRecordId: AccountRecord<Types.Attribute>]
var currentRecordId: AccountRecordId?
var currentAuthRecord: AuthAccountRecord<Types.Attribute>?
var accessChallengeData: PostboxAccessChallengeData
init(records: [AccountRecordId: AccountRecord<Types.Attribute>] = [:], currentRecordId: AccountRecordId? = nil, currentAuthRecord: AuthAccountRecord<Types.Attribute>? = nil, accessChallengeData: PostboxAccessChallengeData = .none) {
self.records = records
self.currentRecordId = currentRecordId
self.currentAuthRecord = currentAuthRecord
self.accessChallengeData = accessChallengeData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let records = try? container.decode(Array<AccountRecord<Types.Attribute>>.self, forKey: .records) {
var recordDict: [AccountRecordId: AccountRecord<Types.Attribute>] = [:]
for record in records {
recordDict[record.id] = record
}
self.records = recordDict
} else {
self.records = try container.decode(Dictionary<AccountRecordId, AccountRecord<Types.Attribute>>.self, forKey: .records)
}
if let idString = try? container.decodeIfPresent(String.self, forKey: .currentRecordId), let idValue = Int64(idString) {
self.currentRecordId = AccountRecordId(rawValue: idValue)
} else {
self.currentRecordId = try container.decodeIfPresent(AccountRecordId.self, forKey: .currentRecordId)
}
self.currentAuthRecord = try container.decodeIfPresent(AuthAccountRecord<Types.Attribute>.self, forKey: .currentAuthRecord)
if let accessChallengeData = try? container.decodeIfPresent(PostboxAccessChallengeData.self, forKey: .accessChallengeData) {
self.accessChallengeData = accessChallengeData
} else {
self.accessChallengeData = .none
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let recordsArray: Array<AccountRecord> = Array(self.records.values)
try container.encode(recordsArray, forKey: .records)
let currentRecordIdString: String? = self.currentRecordId.flatMap({ "\($0.rawValue)" })
try container.encodeIfPresent(currentRecordIdString, forKey: .currentRecordId)
try container.encodeIfPresent(self.currentAuthRecord, forKey: .currentAuthRecord)
try container.encode(self.accessChallengeData, forKey: .accessChallengeData)
}
}
@@ -0,0 +1,669 @@
import Foundation
import SwiftSignalKit
import Postbox
public protocol AccountManagerTypes {
associatedtype Attribute: AccountRecordAttribute
}
public typealias SharedPreferencesEntry = PreferencesEntry
public struct AccountManagerModifier<Types: AccountManagerTypes> {
public let getRecords: () -> [AccountRecord<Types.Attribute>]
public let updateRecord: (AccountRecordId, (AccountRecord<Types.Attribute>?) -> (AccountRecord<Types.Attribute>?)) -> Void
public let getCurrent: () -> (AccountRecordId, [Types.Attribute])?
public let setCurrentId: (AccountRecordId) -> Void
public let getCurrentAuth: () -> AuthAccountRecord<Types.Attribute>?
public let createAuth: ([Types.Attribute]) -> AuthAccountRecord<Types.Attribute>?
public let removeAuth: () -> Void
public let createRecord: ([Types.Attribute]) -> AccountRecordId
public let getSharedData: (ValueBoxKey) -> PreferencesEntry?
public let updateSharedData: (ValueBoxKey, (PreferencesEntry?) -> PreferencesEntry?) -> Void
public let getAccessChallengeData: () -> PostboxAccessChallengeData
public let setAccessChallengeData: (PostboxAccessChallengeData) -> Void
public let getVersion: () -> Int32?
public let setVersion: (Int32) -> Void
public let getNotice: (NoticeEntryKey) -> CodableEntry?
public let setNotice: (NoticeEntryKey, CodableEntry?) -> Void
public let clearNotices: () -> Void
public let getStoredLoginTokens: () -> [Data]
public let setStoredLoginTokens: ([Data]) -> Void
}
final class AccountManagerImpl<Types: AccountManagerTypes> {
private let queue: Queue
private let basePath: String
private let atomicStatePath: String
private let loginTokensPath: String
private let temporarySessionId: Int64
private let guardValueBox: ValueBox?
private let valueBox: ValueBox
private var tables: [Table] = []
private var currentAtomicState: AccountManagerAtomicState<Types>
private var currentAtomicStateUpdated = false
private let legacyMetadataTable: AccountManagerMetadataTable<Types.Attribute>
private let legacyRecordTable: AccountManagerRecordTable<Types.Attribute>
let sharedDataTable: AccountManagerSharedDataTable
let noticeTable: NoticeTable
private var currentRecordOperations: [AccountManagerRecordOperation<Types.Attribute>] = []
private var currentMetadataOperations: [AccountManagerMetadataOperation<Types.Attribute>] = []
private var currentUpdatedSharedDataKeys = Set<ValueBoxKey>()
private var currentUpdatedNoticeEntryKeys = Set<NoticeEntryKey>()
private var currentUpdatedAccessChallengeData: PostboxAccessChallengeData?
private var recordsViews = Bag<(MutableAccountRecordsView<Types>, ValuePipe<AccountRecordsView<Types>>)>()
private var sharedDataViews = Bag<(MutableAccountSharedDataView<Types>, ValuePipe<AccountSharedDataView<Types>>)>()
private var noticeEntryViews = Bag<(MutableNoticeEntryView<Types>, ValuePipe<NoticeEntryView<Types>>)>()
private var accessChallengeDataViews = Bag<(MutableAccessChallengeDataView, ValuePipe<AccessChallengeDataView>)>()
static func getCurrentRecords(basePath: String) -> (records: [AccountRecord<Types.Attribute>], currentId: AccountRecordId?) {
let atomicStatePath = "\(basePath)/atomic-state"
do {
let data = try Data(contentsOf: URL(fileURLWithPath: atomicStatePath))
let atomicState = try JSONDecoder().decode(AccountManagerAtomicState<Types>.self, from: data)
return (atomicState.records.sorted(by: { $0.key.int64 < $1.key.int64 }).map({ $1 }), atomicState.currentRecordId)
} catch let e {
postboxLog("decode atomic state error: \(e)")
postboxLogSync()
preconditionFailure()
}
}
fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, temporarySessionId: Int64) {
let startTime = CFAbsoluteTimeGetCurrent()
self.queue = queue
self.basePath = basePath
self.atomicStatePath = "\(basePath)/atomic-state"
self.loginTokensPath = "\(basePath)/login-tokens"
self.temporarySessionId = temporarySessionId
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else {
postboxLog("Could not open guard value box at \(basePath + "/guard_db")")
postboxLogSync()
preconditionFailure()
return nil
}
self.guardValueBox = guardValueBox
var valueBox: SqliteValueBox?
for i in 0 ..< 3 {
if let valueBoxValue = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) {
valueBox = valueBoxValue
break
} else {
postboxLog("Could not open value box at \(basePath + "/db") (try \(i))")
postboxLogSync()
Thread.sleep(forTimeInterval: 0.1 + 0.5 * Double(i))
}
}
guard let valueBox = valueBox else {
postboxLog("Giving up on opening value box at \(basePath + "/db")")
postboxLogSync()
preconditionFailure()
}
self.valueBox = valueBox
self.legacyMetadataTable = AccountManagerMetadataTable<Types.Attribute>(valueBox: self.valueBox, table: AccountManagerMetadataTable<Types.Attribute>.tableSpec(0), useCaches: useCaches)
self.legacyRecordTable = AccountManagerRecordTable<Types.Attribute>(valueBox: self.valueBox, table: AccountManagerRecordTable<Types.Attribute>.tableSpec(1), useCaches: useCaches)
self.sharedDataTable = AccountManagerSharedDataTable(valueBox: self.valueBox, table: AccountManagerSharedDataTable.tableSpec(2), useCaches: useCaches)
self.noticeTable = NoticeTable(valueBox: self.valueBox, table: NoticeTable.tableSpec(3), useCaches: useCaches)
do {
let data = try Data(contentsOf: URL(fileURLWithPath: self.atomicStatePath))
do {
let atomicState = try JSONDecoder().decode(AccountManagerAtomicState<Types>.self, from: data)
self.currentAtomicState = atomicState
} catch let e {
postboxLog("decode atomic state error: \(e)")
postboxLogSync()
if removeDatabaseOnError {
let _ = try? FileManager.default.removeItem(atPath: self.atomicStatePath)
}
preconditionFailure()
}
} catch let e {
postboxLog("load atomic state error: \(e)")
postboxLogSync()
if removeDatabaseOnError {
var legacyRecordDict: [AccountRecordId: AccountRecord<Types.Attribute>] = [:]
for record in self.legacyRecordTable.getRecords() {
legacyRecordDict[record.id] = record
}
self.currentAtomicState = AccountManagerAtomicState(records: legacyRecordDict, currentRecordId: self.legacyMetadataTable.getCurrentAccountId(), currentAuthRecord: self.legacyMetadataTable.getCurrentAuthAccount(), accessChallengeData: self.legacyMetadataTable.getAccessChallengeData())
self.syncAtomicStateToFile()
} else {
preconditionFailure()
}
}
let tableAccessChallengeData = self.legacyMetadataTable.getAccessChallengeData()
if self.currentAtomicState.accessChallengeData != .none {
if tableAccessChallengeData == .none {
self.legacyMetadataTable.setAccessChallengeData(self.currentAtomicState.accessChallengeData)
}
} else if tableAccessChallengeData != .none {
self.currentAtomicState.accessChallengeData = tableAccessChallengeData
self.syncAtomicStateToFile()
}
postboxLog("AccountManager: currentAccountId = \(String(describing: currentAtomicState.currentRecordId))")
self.tables.append(self.legacyMetadataTable)
self.tables.append(self.legacyRecordTable)
self.tables.append(self.sharedDataTable)
self.tables.append(self.noticeTable)
postboxLog("AccountManager initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
}
deinit {
assert(self.queue.isCurrent())
}
fileprivate func transactionSync<T>(ignoreDisabled: Bool, _ f: (AccountManagerModifier<Types>) -> T) -> T {
self.valueBox.begin()
let transaction = AccountManagerModifier<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, updateRecord: { id, update in
let current = self.currentAtomicState.records[id]
let updated = update(current)
if updated != current {
if let updated = updated {
self.currentAtomicState.records[id] = updated
} else {
self.currentAtomicState.records.removeValue(forKey: id)
}
self.currentAtomicStateUpdated = true
self.currentRecordOperations.append(.set(id: id, record: updated))
}
}, getCurrent: {
if let id = self.currentAtomicState.currentRecordId, let record = self.currentAtomicState.records[id] {
return (record.id, record.attributes)
} else {
return nil
}
}, setCurrentId: { id in
self.currentAtomicState.currentRecordId = id
self.currentMetadataOperations.append(.updateCurrentAccountId(id))
self.currentAtomicStateUpdated = true
}, getCurrentAuth: {
if let record = self.currentAtomicState.currentAuthRecord {
return record
} else {
return nil
}
}, createAuth: { attributes in
let record = AuthAccountRecord<Types.Attribute>(id: generateAccountRecordId(), attributes: attributes)
self.currentAtomicState.currentAuthRecord = record
self.currentAtomicStateUpdated = true
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(record))
return record
}, removeAuth: {
self.currentAtomicState.currentAuthRecord = nil
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(nil))
self.currentAtomicStateUpdated = true
}, createRecord: { attributes in
let id = generateAccountRecordId()
let record = AccountRecord<Types.Attribute>(id: id, attributes: attributes, temporarySessionId: nil)
self.currentAtomicState.records[id] = record
self.currentRecordOperations.append(.set(id: id, record: record))
self.currentAtomicStateUpdated = true
return id
}, getSharedData: { key in
return self.sharedDataTable.get(key: key)
}, updateSharedData: { key, f in
let updated = f(self.sharedDataTable.get(key: key))
self.sharedDataTable.set(key: key, value: updated, updatedKeys: &self.currentUpdatedSharedDataKeys)
}, getAccessChallengeData: {
return self.legacyMetadataTable.getAccessChallengeData()
}, setAccessChallengeData: { data in
self.currentUpdatedAccessChallengeData = data
self.currentAtomicStateUpdated = true
self.legacyMetadataTable.setAccessChallengeData(data)
self.currentAtomicState.accessChallengeData = data
}, getVersion: {
return self.legacyMetadataTable.getVersion()
}, setVersion: { version in
self.legacyMetadataTable.setVersion(version)
}, getNotice: { key in
self.noticeTable.get(key: key)
}, setNotice: { key, value in
self.noticeTable.set(key: key, value: value)
self.currentUpdatedNoticeEntryKeys.insert(key)
}, clearNotices: {
self.noticeTable.clear()
}, getStoredLoginTokens: {
return self.getLoginTokens()
}, setStoredLoginTokens: { list in
self.setLoginTokens(list: list)
})
let result = f(transaction)
self.beforeCommit()
self.valueBox.commit()
return result
}
fileprivate func transaction<T>(ignoreDisabled: Bool, _ f: @escaping (AccountManagerModifier<Types>) -> T) -> Signal<T, NoError> {
return Signal { subscriber in
self.queue.justDispatch {
let result = self.transactionSync(ignoreDisabled: ignoreDisabled, f)
subscriber.putNext(result)
subscriber.putCompletion()
}
return EmptyDisposable
}
}
private func syncAtomicStateToFile() {
if let data = try? JSONEncoder().encode(self.currentAtomicState) {
if let _ = try? data.write(to: URL(fileURLWithPath: self.atomicStatePath), options: [.atomic]) {
} else {
postboxLogSync()
preconditionFailure()
}
} else {
postboxLogSync()
preconditionFailure()
}
}
private func getLoginTokens() -> [Data] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: self.loginTokensPath)) else {
return []
}
guard let list = try? JSONDecoder().decode([Data].self, from: data) else {
return []
}
return list
}
private func setLoginTokens(list: [Data]) {
if let data = try? JSONEncoder().encode(list) {
if let _ = try? data.write(to: URL(fileURLWithPath: self.loginTokensPath), options: [.atomic]) {
}
}
}
private func beforeCommit() {
if self.currentAtomicStateUpdated {
self.syncAtomicStateToFile()
}
if !self.currentRecordOperations.isEmpty || !self.currentMetadataOperations.isEmpty {
for (view, pipe) in self.recordsViews.copyItems() {
if view.replay(operations: self.currentRecordOperations, metadataOperations: self.currentMetadataOperations) {
pipe.putNext(AccountRecordsView<Types>(view))
}
}
}
if !self.currentUpdatedSharedDataKeys.isEmpty {
for (view, pipe) in self.sharedDataViews.copyItems() {
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedSharedDataKeys) {
pipe.putNext(AccountSharedDataView<Types>(view))
}
}
}
if !self.currentUpdatedNoticeEntryKeys.isEmpty {
for (view, pipe) in self.noticeEntryViews.copyItems() {
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedNoticeEntryKeys) {
pipe.putNext(NoticeEntryView(view))
}
}
}
if let data = self.currentUpdatedAccessChallengeData {
for (view, pipe) in self.accessChallengeDataViews.copyItems() {
if view.replay(updatedData: data) {
pipe.putNext(AccessChallengeDataView(view))
}
}
}
self.currentRecordOperations.removeAll()
self.currentMetadataOperations.removeAll()
self.currentUpdatedSharedDataKeys.removeAll()
self.currentUpdatedNoticeEntryKeys.removeAll()
self.currentUpdatedAccessChallengeData = nil
self.currentAtomicStateUpdated = false
for table in self.tables {
table.beforeCommit()
}
}
fileprivate func accountRecords() -> Signal<AccountRecordsView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordsView<Types>, NoError> in
return self.accountRecordsInternal(transaction: transaction)
})
|> switchToLatest
}
fileprivate func _internalAccountRecordsSync() -> AccountRecordsView<Types> {
let mutableView = MutableAccountRecordsView<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, currentId: self.currentAtomicState.currentRecordId, currentAuth: self.currentAtomicState.currentAuthRecord)
return AccountRecordsView<Types>(mutableView)
}
fileprivate func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountSharedDataView<Types>, NoError> in
return self.sharedDataInternal(transaction: transaction, keys: keys)
})
|> switchToLatest
}
fileprivate func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<NoticeEntryView<Types>, NoError> in
return self.noticeEntryInternal(transaction: transaction, key: key)
})
|> switchToLatest
}
fileprivate func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccessChallengeDataView, NoError> in
return self.accessChallengeDataInternal(transaction: transaction)
})
|> switchToLatest
}
private func accountRecordsInternal(transaction: AccountManagerModifier<Types>) -> Signal<AccountRecordsView<Types>, NoError> {
assert(self.queue.isCurrent())
let mutableView = MutableAccountRecordsView<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, currentId: self.currentAtomicState.currentRecordId, currentAuth: self.currentAtomicState.currentAuthRecord)
let pipe = ValuePipe<AccountRecordsView<Types>>()
let index = self.recordsViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccountRecordsView<Types>(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccountRecordsView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.recordsViews.remove(index)
}
}
}
}
private func sharedDataInternal(transaction: AccountManagerModifier<Types>, keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
let mutableView = MutableAccountSharedDataView<Types>(accountManagerImpl: self, keys: keys)
let pipe = ValuePipe<AccountSharedDataView<Types>>()
let index = self.sharedDataViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccountSharedDataView<Types>(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccountSharedDataView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.sharedDataViews.remove(index)
}
}
}
}
private func noticeEntryInternal(transaction: AccountManagerModifier<Types>, key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
let mutableView = MutableNoticeEntryView<Types>(accountManagerImpl: self, key: key)
let pipe = ValuePipe<NoticeEntryView<Types>>()
let index = self.noticeEntryViews.add((mutableView, pipe))
let queue = self.queue
return (.single(NoticeEntryView(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<NoticeEntryView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.noticeEntryViews.remove(index)
}
}
}
}
private func accessChallengeDataInternal(transaction: AccountManagerModifier<Types>) -> Signal<AccessChallengeDataView, NoError> {
let mutableView = MutableAccessChallengeDataView(data: transaction.getAccessChallengeData())
let pipe = ValuePipe<AccessChallengeDataView>()
let index = self.accessChallengeDataViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccessChallengeDataView(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccessChallengeDataView, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.accessChallengeDataViews.remove(index)
}
}
}
}
fileprivate func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> in
let current = transaction.getCurrent()
if let _ = current {
} else if allocateIfNotExists {
let id = generateAccountRecordId()
transaction.setCurrentId(id)
transaction.updateRecord(id, { _ in
return AccountRecord(id: id, attributes: [], temporarySessionId: nil)
})
} else {
return .single(nil)
}
let signal = self.accountRecordsInternal(transaction: transaction)
|> map { view -> (AccountRecordId, [Types.Attribute])? in
if let currentRecord = view.currentRecord {
return (currentRecord.id, currentRecord.attributes)
} else {
return nil
}
}
return signal
})
|> switchToLatest
|> distinctUntilChanged(isEqual: { lhs, rhs in
if let lhs = lhs, let rhs = rhs {
if lhs.0 != rhs.0 {
return false
}
if lhs.1.count != rhs.1.count {
return false
}
for i in 0 ..< lhs.1.count {
if !lhs.1[i].isEqual(to: rhs.1[i]) {
return false
}
}
return true
} else if (lhs != nil) != (rhs != nil) {
return false
} else {
return true
}
})
}
func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
let temporarySessionId = self.temporarySessionId
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordId, NoError> in
let id = generateAccountRecordId()
transaction.updateRecord(id, { _ in
return AccountRecord(id: id, attributes: [], temporarySessionId: temporarySessionId)
})
return .single(id)
})
|> switchToLatest
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
}
}
private let sharedQueue = Queue()
public final class AccountManager<Types: AccountManagerTypes> {
public let basePath: String
public let mediaBox: MediaBox
private let queue: Queue
private let impl: QueueLocalObject<AccountManagerImpl<Types>>
public let temporarySessionId: Int64
public static func getCurrentRecords(basePath: String) -> (records: [AccountRecord<Types.Attribute>], currentId: AccountRecordId?) {
return AccountManagerImpl<Types>.getCurrentRecords(basePath: basePath)
}
public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool) {
self.queue = sharedQueue
self.basePath = basePath
var temporarySessionId: Int64 = 0
arc4random_buf(&temporarySessionId, 8)
self.temporarySessionId = temporarySessionId
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
if let value = AccountManagerImpl<Types>(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) {
return value
} else {
postboxLogSync()
preconditionFailure()
}
})
self.mediaBox = MediaBox(basePath: basePath + "/media", isMainProcess: removeDatabaseOnError)
}
public func transaction<T>(ignoreDisabled: Bool = false, _ f: @escaping (AccountManagerModifier<Types>) -> T) -> Signal<T, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.transaction(ignoreDisabled: ignoreDisabled, f).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func accountRecords() -> Signal<AccountRecordsView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.accountRecords().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func _internalAccountRecordsSync() -> AccountRecordsView<Types> {
var result: AccountRecordsView<Types>?
self.impl.syncWith { impl in
result = impl._internalAccountRecordsSync()
}
return result!
}
public func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.sharedData(keys: keys).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.noticeEntry(key: key).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.accessChallengeData().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.currentAccountRecord(allocateIfNotExists: allocateIfNotExists).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.allocatedTemporaryAccountId().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
}
@@ -0,0 +1,224 @@
import Foundation
import Postbox
public struct AccessChallengeAttempts: Equatable {
public let count: Int32
public var bootTimestamp: Int32
public var uptime: Int32
public init(count: Int32, bootTimestamp: Int32, uptime: Int32) {
self.count = count
self.bootTimestamp = bootTimestamp
self.uptime = uptime
}
}
public enum PostboxAccessChallengeData: PostboxCoding, Equatable, Codable {
enum CodingKeys: String, CodingKey {
case numericalPassword
case plaintextPassword
}
case none
case numericalPassword(value: String)
case plaintextPassword(value: String)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) {
case 0:
self = .none
case 1:
self = .numericalPassword(value: decoder.decodeStringForKey("t", orElse: ""))
case 2:
self = .plaintextPassword(value: decoder.decodeStringForKey("t", orElse: ""))
default:
assertionFailure()
self = .none
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case .none:
encoder.encodeInt32(0, forKey: "r")
case let .numericalPassword(text):
encoder.encodeInt32(1, forKey: "r")
encoder.encodeString(text, forKey: "t")
case let .plaintextPassword(text):
encoder.encodeInt32(2, forKey: "r")
encoder.encodeString(text, forKey: "t")
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(String.self, forKey: .numericalPassword) {
self = .numericalPassword(value: value)
} else if let value = try? container.decode(String.self, forKey: .plaintextPassword) {
self = .plaintextPassword(value: value)
} else {
self = .none
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .none:
break
case let .numericalPassword(value):
try container.encode(value, forKey: .numericalPassword)
case let .plaintextPassword(value):
try container.encode(value, forKey: .plaintextPassword)
}
}
public var isLockable: Bool {
if case .none = self {
return false
} else {
return true
}
}
public var lockId: String? {
switch self {
case .none:
return nil
case let .numericalPassword(value):
return "numericalPassword:\(value)"
case let .plaintextPassword(value):
return "plaintextPassword:\(value)"
}
}
}
public struct AuthAccountRecord<Attribute: AccountRecordAttribute>: Codable {
enum CodingKeys: String, CodingKey {
case id
case attributes
}
public let id: AccountRecordId
public let attributes: [Attribute]
init(id: AccountRecordId, attributes: [Attribute]) {
self.id = id
self.attributes = attributes
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(AccountRecordId.self, forKey: .id)
if let attributesData = try? container.decode(Array<Data>.self, forKey: .attributes) {
var attributes: [Attribute] = []
for data in attributesData {
if let attribute = try? AdaptedPostboxDecoder().decode(Attribute.self, from: data) {
attributes.append(attribute)
}
}
self.attributes = attributes
} else {
let attributes = try container.decode([Attribute].self, forKey: .attributes)
self.attributes = attributes
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.attributes, forKey: .attributes)
}
}
enum AccountManagerMetadataOperation<Attribute: AccountRecordAttribute> {
case updateCurrentAccountId(AccountRecordId)
case updateCurrentAuthAccountRecord(AuthAccountRecord<Attribute>?)
}
private enum MetadataKey: Int64 {
case currentAccountId = 0
case currentAuthAccount = 1
case accessChallenge = 2
case version = 3
}
final class AccountManagerMetadataTable<Attribute: AccountRecordAttribute>: Table {
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
}
private func key(_ key: MetadataKey) -> ValueBoxKey {
let result = ValueBoxKey(length: 8)
result.setInt64(0, value: key.rawValue)
return result
}
func getVersion() -> Int32? {
if let value = self.valueBox.get(self.table, key: self.key(.version)) {
var id: Int32 = 0
value.read(&id, offset: 0, length: 4)
return id
} else {
return 0
}
}
func setVersion(_ version: Int32) {
var value: Int32 = version
self.valueBox.set(self.table, key: self.key(.version), value: MemoryBuffer(memory: &value, capacity: 4, length: 4, freeWhenDone: false))
}
func getCurrentAccountId() -> AccountRecordId? {
if let value = self.valueBox.get(self.table, key: self.key(.currentAccountId)) {
var id: Int64 = 0
value.read(&id, offset: 0, length: 8)
return AccountRecordId(rawValue: id)
} else {
return nil
}
}
func setCurrentAccountId(_ id: AccountRecordId, operations: inout [AccountManagerMetadataOperation<Attribute>]) {
var rawValue = id.rawValue
self.valueBox.set(self.table, key: self.key(.currentAccountId), value: MemoryBuffer(memory: &rawValue, capacity: 8, length: 8, freeWhenDone: false))
operations.append(.updateCurrentAccountId(id))
}
func getCurrentAuthAccount() -> AuthAccountRecord<Attribute>? {
if let value = self.valueBox.get(self.table, key: self.key(.currentAuthAccount)) {
let object = try? AdaptedPostboxDecoder().decode(AuthAccountRecord<Attribute>.self, from: value.makeData())
return object
} else {
return nil
}
}
func setCurrentAuthAccount(_ record: AuthAccountRecord<Attribute>?, operations: inout [AccountManagerMetadataOperation<Attribute>]) {
if let record = record {
let data = try! AdaptedPostboxEncoder().encode(record)
self.valueBox.set(self.table, key: self.key(.currentAuthAccount), value: ReadBuffer(data: data))
} else {
self.valueBox.remove(self.table, key: self.key(.currentAuthAccount), secure: false)
}
operations.append(.updateCurrentAuthAccountRecord(record))
}
func getAccessChallengeData() -> PostboxAccessChallengeData {
if let value = self.valueBox.get(self.table, key: self.key(.accessChallenge)) {
return PostboxAccessChallengeData(decoder: PostboxDecoder(buffer: value))
} else {
return .none
}
}
func setAccessChallengeData(_ data: PostboxAccessChallengeData) {
let encoder = PostboxEncoder()
data.encode(encoder)
withExtendedLifetime(encoder, {
self.valueBox.set(self.table, key: self.key(.accessChallenge), value: encoder.readBufferNoCopy())
})
}
}
@@ -0,0 +1,51 @@
import Foundation
import Postbox
enum AccountManagerRecordOperation<Attribute: AccountRecordAttribute> {
case set(id: AccountRecordId, record: AccountRecord<Attribute>?)
}
final class AccountManagerRecordTable<Attribute: AccountRecordAttribute>: Table {
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
}
private func key(_ key: AccountRecordId) -> ValueBoxKey {
let result = ValueBoxKey(length: 8)
result.setInt64(0, value: key.rawValue)
return result
}
func getRecords() -> [AccountRecord<Attribute>] {
var records: [AccountRecord<Attribute>] = []
self.valueBox.scan(self.table, values: { _, value in
if let record = try? AdaptedPostboxDecoder().decode(AccountRecord<Attribute>.self, from: value.makeData()) {
records.append(record)
}
return true
})
return records
}
func getRecord(id: AccountRecordId) -> AccountRecord<Attribute>? {
if let value = self.valueBox.get(self.table, key: self.key(id)) {
if let record = try? AdaptedPostboxDecoder().decode(AccountRecord<Attribute>.self, from: value.makeData()) {
return record
} else {
return nil
}
} else {
return nil
}
}
func setRecord(id: AccountRecordId, record: AccountRecord<Attribute>?, operations: inout [AccountManagerRecordOperation<Attribute>]) {
if let record = record {
let data = try! AdaptedPostboxEncoder().encode(record)
self.valueBox.set(self.table, key: self.key(id), value: ReadBuffer(data: data))
} else {
self.valueBox.remove(self.table, key: self.key(id), secure: false)
}
operations.append(.set(id: id, record: record))
}
}
@@ -0,0 +1,39 @@
import Foundation
import Postbox
final class AccountManagerSharedDataTable: Table {
private var values:[ValueBoxKey : PreferencesEntry] = [:]
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
func get(key: ValueBoxKey) -> PreferencesEntry? {
if let object = self.values[key] {
return object
} else if let value = self.valueBox.get(self.table, key: key) {
return PreferencesEntry(data: value.makeData())
} else {
return nil
}
}
func set(key: ValueBoxKey, value: PreferencesEntry?, updatedKeys: inout Set<ValueBoxKey>) {
if let value = value {
if let current = self.get(key: key), current == value {
return
}
self.valueBox.set(self.table, key: key, value: ReadBuffer(data: value.data))
updatedKeys.insert(key)
self.values[key] = value
} else if self.get(key: key) != nil {
self.valueBox.remove(self.table, key: key, secure: false)
updatedKeys.insert(key)
self.values.removeValue(forKey: key)
}
}
}
@@ -0,0 +1,106 @@
import Foundation
import Postbox
public protocol AccountRecordAttribute: Codable {
func isEqual(to: AccountRecordAttribute) -> Bool
}
public struct AccountRecordId: Comparable, Hashable, Codable {
let rawValue: Int64
public init(rawValue: Int64) {
self.rawValue = rawValue
}
public var int64: Int64 {
return self.rawValue
}
public static func ==(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
return lhs.rawValue == rhs.rawValue
}
public static func <(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
public func generateAccountRecordId() -> AccountRecordId {
var id: Int64 = 0
arc4random_buf(&id, 8)
return AccountRecordId(rawValue: id)
}
public final class AccountRecord<Attribute: AccountRecordAttribute>: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case id
case attributes
case temporarySessionId
}
public let id: AccountRecordId
public let attributes: [Attribute]
public let temporarySessionId: Int64?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let idString = try? container.decode(String.self, forKey: .id), let idValue = Int64(idString) {
self.id = AccountRecordId(rawValue: idValue)
} else {
self.id = try container.decode(AccountRecordId.self, forKey: .id)
}
if let attributesData = try? container.decode(Array<Data>.self, forKey: .attributes) {
var attributes: [Attribute] = []
for data in attributesData {
if let attribute = try? AdaptedPostboxDecoder().decode(Attribute.self, from: data) {
attributes.append(attribute)
}
}
self.attributes = attributes
} else {
let attributes = try container.decode([Attribute].self, forKey: .attributes)
self.attributes = attributes
}
if let temporarySessionIdString = try? container.decodeIfPresent(String.self, forKey: .temporarySessionId), let temporarySessionIdValue = Int64(temporarySessionIdString) {
self.temporarySessionId = temporarySessionIdValue
} else if let temporarySessionInt64 = try? container.decodeIfPresent(Int64.self, forKey: .temporarySessionId) {
self.temporarySessionId = temporarySessionInt64
} else {
self.temporarySessionId = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(String("\(self.id.rawValue)"), forKey: .id)
try container.encode(self.attributes, forKey: .attributes)
let temporarySessionIdString: String? = self.temporarySessionId.flatMap({ "\($0)" })
try container.encodeIfPresent(temporarySessionIdString, forKey: .temporarySessionId)
}
public init(id: AccountRecordId, attributes: [Attribute], temporarySessionId: Int64?) {
self.id = id
self.attributes = attributes
self.temporarySessionId = temporarySessionId
}
public static func ==(lhs: AccountRecord, rhs: AccountRecord) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.attributes.count != rhs.attributes.count {
return false
}
for i in 0 ..< lhs.attributes.count {
if !lhs.attributes[i].isEqual(to: rhs.attributes[i]) {
return false
}
}
if lhs.temporarySessionId != rhs.temporarySessionId {
return false
}
return true
}
}
@@ -0,0 +1,85 @@
import Foundation
final class MutableAccountRecordsView<Types: AccountManagerTypes> {
fileprivate var records: [AccountRecord<Types.Attribute>]
fileprivate var currentId: AccountRecordId?
fileprivate var currentAuth: AuthAccountRecord<Types.Attribute>?
init(getRecords: () -> [AccountRecord<Types.Attribute>], currentId: AccountRecordId?, currentAuth: AuthAccountRecord<Types.Attribute>?) {
self.records = getRecords()
self.currentId = currentId
self.currentAuth = currentAuth
}
func replay(operations: [AccountManagerRecordOperation<Types.Attribute>], metadataOperations: [AccountManagerMetadataOperation<Types.Attribute>]) -> Bool {
var updated = false
for operation in operations {
switch operation {
case let .set(id, record):
if let record = record {
updated = true
var found = false
for i in 0 ..< self.records.count {
if self.records[i].id == id {
self.records[i] = record
found = true
break
}
}
if !found {
self.records.append(record)
self.records.sort(by: { lhs, rhs in
return lhs.id < rhs.id
})
}
} else {
for i in 0 ..< self.records.count {
if self.records[i].id == id {
self.records.remove(at: i)
updated = true
break
}
}
}
}
}
for operation in metadataOperations {
switch operation {
case let .updateCurrentAccountId(id):
updated = true
self.currentId = id
case let .updateCurrentAuthAccountRecord(record):
updated = true
self.currentAuth = record
}
}
return updated
}
}
public final class AccountRecordsView<Types: AccountManagerTypes> {
public let records: [AccountRecord<Types.Attribute>]
public let currentRecord: AccountRecord<Types.Attribute>?
public let currentAuthAccount: AuthAccountRecord<Types.Attribute>?
init(_ view: MutableAccountRecordsView<Types>) {
self.records = view.records
if let currentId = view.currentId {
var currentRecord: AccountRecord<Types.Attribute>?
for record in view.records {
if record.id == currentId {
currentRecord = record
break
}
}
self.currentRecord = currentRecord
} else {
self.currentRecord = nil
}
self.currentAuthAccount = view.currentAuth
}
}
@@ -0,0 +1,37 @@
import Foundation
import Postbox
final class MutableAccountSharedDataView<Types: AccountManagerTypes> {
private let keys: Set<ValueBoxKey>
fileprivate var entries: [ValueBoxKey: PreferencesEntry] = [:]
init(accountManagerImpl: AccountManagerImpl<Types>, keys: Set<ValueBoxKey>) {
self.keys = keys
for key in keys {
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
self.entries[key] = value
}
}
}
func replay(accountManagerImpl: AccountManagerImpl<Types>, updatedKeys: Set<ValueBoxKey>) -> Bool {
var updated = false
for key in updatedKeys.intersection(self.keys) {
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
self.entries[key] = value
} else {
self.entries.removeValue(forKey: key)
}
updated = true
}
return updated
}
}
public final class AccountSharedDataView<Types: AccountManagerTypes> {
public let entries: [ValueBoxKey: PreferencesEntry]
init(_ view: MutableAccountSharedDataView<Types>) {
self.entries = view.entries
}
}
@@ -0,0 +1,32 @@
import Foundation
import Postbox
final class MutableNoticeEntryView<Types: AccountManagerTypes> {
private let key: NoticeEntryKey
fileprivate var value: CodableEntry?
init(accountManagerImpl: AccountManagerImpl<Types>, key: NoticeEntryKey) {
self.key = key
self.value = accountManagerImpl.noticeTable.get(key: key)
}
func replay(accountManagerImpl: AccountManagerImpl<Types>, updatedKeys: Set<NoticeEntryKey>) -> Bool {
if updatedKeys.contains(self.key) {
self.value = accountManagerImpl.noticeTable.get(key: self.key)
return true
}
return false
}
func immutableView() -> NoticeEntryView<Types> {
return NoticeEntryView(self)
}
}
public final class NoticeEntryView<Types: AccountManagerTypes> {
public let value: CodableEntry?
init(_ view: MutableNoticeEntryView<Types>) {
self.value = view.value
}
}