XCode project for app, moved app project to folder

This commit is contained in:
Joe Mattiello
2023-03-01 22:07:19 -05:00
parent 365cadbb31
commit 4c9c5b1a56
371 changed files with 625 additions and 39 deletions

View File

@@ -0,0 +1,83 @@
//
// Keychain.swift
// AltStore
//
// Created by Riley Testut on 6/4/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import KeychainAccess
import AltSign
@propertyWrapper
public struct KeychainItem<Value> {
public let key: String
public var wrappedValue: Value? {
get {
switch Value.self {
case is Data.Type: return try? Keychain.shared.keychain.getData(key) as? Value
case is String.Type: return try? Keychain.shared.keychain.getString(key) as? Value
default: return nil
}
}
set {
switch Value.self {
case is Data.Type: Keychain.shared.keychain[data: key] = newValue as? Data
case is String.Type: Keychain.shared.keychain[key] = newValue as? String
default: break
}
}
}
public init(key: String) {
self.key = key
}
}
public class Keychain {
public static let shared = Keychain()
fileprivate let keychain = KeychainAccess.Keychain(service: Bundle.Info.appbundleIdentifier).accessibility(.afterFirstUnlock).synchronizable(true)
@KeychainItem(key: "appleIDEmailAddress")
public var appleIDEmailAddress: String?
@KeychainItem(key: "appleIDPassword")
public var appleIDPassword: String?
@KeychainItem(key: "signingCertificatePrivateKey")
public var signingCertificatePrivateKey: Data?
@KeychainItem(key: "signingCertificateSerialNumber")
public var signingCertificateSerialNumber: String?
@KeychainItem(key: "signingCertificate")
public var signingCertificate: Data?
@KeychainItem(key: "signingCertificatePassword")
public var signingCertificatePassword: String?
@KeychainItem(key: "patreonAccessToken")
public var patreonAccessToken: String?
@KeychainItem(key: "patreonRefreshToken")
public var patreonRefreshToken: String?
@KeychainItem(key: "patreonCreatorAccessToken")
public var patreonCreatorAccessToken: String?
@KeychainItem(key: "patreonAccountID")
public var patreonAccountID: String?
private init() {}
public func reset() {
appleIDEmailAddress = nil
appleIDPassword = nil
signingCertificatePrivateKey = nil
signingCertificateSerialNumber = nil
}
}

View File

@@ -0,0 +1,30 @@
//
// Date+RelativeDate.swift
// AltStore
//
// Created by Riley Testut on 7/28/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
public extension Date {
func numberOfCalendarDays(since date: Date) -> Int {
let today = Calendar.current.startOfDay(for: self)
let previousDay = Calendar.current.startOfDay(for: date)
let components = Calendar.current.dateComponents([.day], from: previousDay, to: today)
return components.day!
}
func relativeDateString(since date: Date, dateFormatter: DateFormatter) -> String {
let numberOfDays = numberOfCalendarDays(since: date)
switch numberOfDays {
case 0: return NSLocalizedString("Today", comment: "")
case 1: return NSLocalizedString("Yesterday", comment: "")
case 2 ... 7: return String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays))
default: return dateFormatter.string(from: date)
}
}
}

View File

@@ -0,0 +1,32 @@
//
// FileManager+SharedDirectories.swift
// AltStore
//
// Created by Riley Testut on 5/14/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Shared
public extension FileManager {
var altstoreSharedDirectory: URL? {
#if SWIFT_PACKAGE
guard let appGroup = Bundle.main.appGroups.first else { return nil }
#else
guard let appGroup = Bundle.main.appGroups.first else { return nil }
#endif
let sharedDirectoryURL = containerURL(forSecurityApplicationGroupIdentifier: appGroup)
return sharedDirectoryURL
}
var appBackupsDirectory: URL? {
let appBackupsDirectory = altstoreSharedDirectory?.appendingPathComponent("Backups", isDirectory: true)
return appBackupsDirectory
}
func backupDirectoryURL(for app: InstalledApp) -> URL? {
let backupDirectoryURL = appBackupsDirectory?.appendingPathComponent(app.bundleIdentifier, isDirectory: true)
return backupDirectoryURL
}
}

View File

@@ -0,0 +1,59 @@
//
// JSONDecoder+Properties.swift
// Harmony
//
// Created by Riley Testut on 10/3/18.
// Copyright © 2018 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
public extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
}
public final class JSONDecoder: Foundation.JSONDecoder {
@DecoderItem(key: .managedObjectContext)
public var managedObjectContext: NSManagedObjectContext?
@DecoderItem(key: .sourceURL)
public var sourceURL: URL?
}
public extension Decoder {
var managedObjectContext: NSManagedObjectContext? { userInfo[.managedObjectContext] as? NSManagedObjectContext }
var sourceURL: URL? { userInfo[.sourceURL] as? URL }
}
@propertyWrapper
public struct DecoderItem<Value> {
public let key: CodingUserInfoKey
public var wrappedValue: Value? {
get { fatalError("only works on instance properties of classes") }
set { fatalError("only works on instance properties of classes") }
}
public init(key: CodingUserInfoKey) {
self.key = key
}
public static subscript<OuterSelf: JSONDecoder>(
_enclosingInstance decoder: OuterSelf,
wrapped _: ReferenceWritableKeyPath<OuterSelf, Value?>,
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
) -> Value? {
get {
let wrapper = decoder[keyPath: storageKeyPath]
let value = decoder.userInfo[wrapper.key] as? Value
return value
}
set {
let wrapper = decoder[keyPath: storageKeyPath]
decoder.userInfo[wrapper.key] = newValue
}
}
}

View File

@@ -0,0 +1,16 @@
//
// UIApplication+AppExtension.swift
// DeltaCore
//
// Created by Riley Testut on 6/14/18.
// Copyright © 2018 Riley Testut. All rights reserved.
//
import UIKit
public extension UIApplication {
// Cannot normally use UIApplication.shared from extensions, so we get around this by calling value(forKey:).
class var alt_shared: UIApplication? {
UIApplication.value(forKey: "sharedApplication") as? UIApplication
}
}

View File

@@ -0,0 +1,23 @@
//
// UIColor+AltStore.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
public extension UIColor {
private static let colorBundle = Bundle(for: DatabaseManager.self)
static let altPrimary = UIColor(named: "Primary", in: colorBundle, compatibleWith: nil)!
static let deltaPrimary = UIColor(named: "DeltaPrimary", in: colorBundle, compatibleWith: nil)
static let altPink = UIColor(named: "Pink", in: colorBundle, compatibleWith: nil)!
static let refreshRed = UIColor(named: "RefreshRed", in: colorBundle, compatibleWith: nil)!
static let refreshOrange = UIColor(named: "RefreshOrange", in: colorBundle, compatibleWith: nil)!
static let refreshYellow = UIColor(named: "RefreshYellow", in: colorBundle, compatibleWith: nil)!
static let refreshGreen = UIColor(named: "RefreshGreen", in: colorBundle, compatibleWith: nil)!
}

View File

@@ -0,0 +1,71 @@
//
// UIColor+Hex.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
public extension UIColor {
// Borrowed from https://stackoverflow.com/a/26341062
var hexString: String {
let components = cgColor.components
let r: CGFloat = components?[0] ?? 0.0
let g: CGFloat = components?[1] ?? 0.0
let b: CGFloat = components?[2] ?? 0.0
let hexString = String(format: "%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
return hexString
}
}
public extension UIColor {
convenience init?(hexString: String) {
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
let scanner = Scanner(string: hexString)
if hexString.hasPrefix("#") {
scanner.scanLocation = 1
// TODO: Test if this works to replace the above deprecation @JoeMatt
// scanner.currentIndex = .init(utf16Offset: 1, in: hexString)
}
var hexNumber: UInt64 = 0
guard scanner.scanHexInt64(&hexNumber) else {
return nil
}
var alpha: UInt64 = 255
var red: UInt64 = 0
var green: UInt64 = 0
var blue: UInt64 = 0
switch hexString.count {
case 3: // RGB (12-bit)
red = ((hexNumber & 0xF00) >> 8) * 17
green = ((hexNumber & 0x0F0) >> 4) * 17
blue = (hexNumber & 0x00F) * 17
case 6: // RGB (24-bit)
red = (hexNumber & 0xFF0000) >> 16
green = (hexNumber & 0x00FF00) >> 8
blue = hexNumber & 0x0000FF
case 8: // ARGB (32-bit)
alpha = (hexNumber & 0xFF00_0000) >> 24
red = (hexNumber & 0x00FF_0000) >> 16
green = (hexNumber & 0x0000_FF00) >> 8
blue = hexNumber & 0x0000_00FF
default:
return nil
}
self.init(
red: CGFloat(red) / 255,
green: CGFloat(green) / 255,
blue: CGFloat(blue) / 255,
alpha: CGFloat(alpha) / 255
)
}
}

View File

@@ -0,0 +1,79 @@
//
// UserDefaults+AltStore.swift
// AltStore
//
// Created by Riley Testut on 6/4/19.
// Copyright © 2019 SideStore. All rights reserved.
//
import Foundation
import Roxas
public extension UserDefaults {
static let shared: UserDefaults = {
guard let appGroup = Bundle.main.appGroups.first else { return .standard }
let sharedUserDefaults = UserDefaults(suiteName: appGroup)!
return sharedUserDefaults
}()
@NSManaged var firstLaunch: Date?
@NSManaged var requiresAppGroupMigration: Bool
@NSManaged var textServer: Bool
@NSManaged var textInputAnisetteURL: String?
@NSManaged var customAnisetteURL: String?
@NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
@NSManaged var legacySideloadedApps: [String]?
@NSManaged var isLegacyDeactivationSupported: Bool
@NSManaged var activeAppLimitIncludesExtensions: Bool
@NSManaged var localServerSupportsRefreshing: Bool
@NSManaged var patchedApps: [String]?
@NSManaged var patronsRefreshID: String?
@NSManaged var trustedSourceIDs: [String]?
var activeAppsLimit: Int? {
get {
_activeAppsLimit?.intValue
}
set {
if let value = newValue {
_activeAppsLimit = NSNumber(value: value)
} else {
_activeAppsLimit = nil
}
}
}
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
class func registerDefaults() {
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
let ios14 = OperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)
let localServerSupportsRefreshing = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14)
let defaults = [
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true
]
UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults)
}
}

View File

@@ -0,0 +1,60 @@
//
// Account.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
@objc(Account)
public class Account: NSManagedObject, Fetchable {
public var localizedName: String {
var components = PersonNameComponents()
components.givenName = firstName
components.familyName = lastName
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
return name
}
/* Properties */
@NSManaged public var appleID: String
@NSManaged public var identifier: String
@NSManaged public var firstName: String
@NSManaged public var lastName: String
@NSManaged public var isActiveAccount: Bool
/* Relationships */
@NSManaged public var teams: Set<Team>
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(_ account: ALTAccount, context: NSManagedObjectContext) {
super.init(entity: Account.entity(), insertInto: context)
update(account: account)
}
public func update(account: ALTAccount) {
appleID = account.appleID
identifier = account.identifier
firstName = account.firstName
lastName = account.lastName
}
}
public extension Account {
@nonobjc class func fetchRequest() -> NSFetchRequest<Account> {
NSFetchRequest<Account>(entityName: "Account")
}
}

View File

@@ -0,0 +1,47 @@
//
// AppID.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
@objc(AppID)
public class AppID: NSManagedObject, Fetchable {
/* Properties */
@NSManaged public var name: String
@NSManaged public var identifier: String
@NSManaged public var bundleIdentifier: String
@NSManaged public var features: [ALTFeature: Any]
@NSManaged public var expirationDate: Date?
/* Relationships */
@NSManaged public private(set) var team: Team?
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext) {
super.init(entity: AppID.entity(), insertInto: context)
name = appID.name
identifier = appID.identifier
bundleIdentifier = appID.bundleIdentifier
features = appID.features
expirationDate = appID.expirationDate
self.team = team
}
}
public extension AppID {
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID> {
NSFetchRequest<AppID>(entityName: "AppID")
}
}

View File

@@ -0,0 +1,118 @@
//
// AppPermission.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import UIKit
public extension ALTAppPermissionType {
var localizedShortName: String? {
switch self {
case .photos: return NSLocalizedString("Photos", comment: "")
case .backgroundAudio: return NSLocalizedString("Audio (BG)", comment: "")
case .backgroundFetch: return NSLocalizedString("Fetch (BG)", comment: "")
default: return nil
}
}
var localizedName: String? {
switch self {
case .photos: return NSLocalizedString("Photos", comment: "")
case .camera: return NSLocalizedString("Camera", comment: "")
case .location: return NSLocalizedString("Location", comment: "")
case .contacts: return NSLocalizedString("Contacts", comment: "")
case .reminders: return NSLocalizedString("Reminders", comment: "")
case .appleMusic: return NSLocalizedString("Apple Music", comment: "")
case .microphone: return NSLocalizedString("Microphone", comment: "")
case .speechRecognition: return NSLocalizedString("Speech Recognition", comment: "")
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
case .bluetooth: return NSLocalizedString("Bluetooth", comment: "")
case .network: return NSLocalizedString("Network", comment: "")
case .calendars: return NSLocalizedString("Calendars", comment: "")
case .touchID: return NSLocalizedString("Touch ID", comment: "")
case .faceID: return NSLocalizedString("Face ID", comment: "")
case .siri: return NSLocalizedString("Siri", comment: "")
case .motion: return NSLocalizedString("Motion", comment: "")
default: return nil
}
}
var icon: UIImage? {
switch self {
case .photos: return UIImage(systemName: "photo.on.rectangle.angled")
case .camera: return UIImage(systemName: "camera.fill")
case .location: return UIImage(systemName: "location.fill")
case .contacts: return UIImage(systemName: "person.2.fill")
case .reminders: return UIImage(systemName: "checklist")
case .appleMusic: return UIImage(systemName: "music.note")
case .microphone: return UIImage(systemName: "mic.fill")
case .speechRecognition: return UIImage(systemName: "waveform.and.mic")
case .backgroundAudio: return UIImage(systemName: "speaker.fill")
case .backgroundFetch: return UIImage(systemName: "square.and.arrow.down")
case .bluetooth: return UIImage(systemName: "wave.3.right")
case .network: return UIImage(systemName: "network")
case .calendars: return UIImage(systemName: "calendar")
case .touchID: return UIImage(systemName: "touchid")
case .faceID: return UIImage(systemName: "faceid")
case .siri: return UIImage(systemName: "mic.and.signal.meter.fill")
case .motion: return UIImage(systemName: "figure.walk.motion")
default:
return nil
}
}
}
@objc(AppPermission)
public class AppPermission: NSManagedObject, Decodable, Fetchable {
/* Properties */
@NSManaged public var type: ALTAppPermissionType
@NSManaged public var usageDescription: String
/* Relationships */
@NSManaged public private(set) var app: StoreApp!
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey {
case type
case usageDescription
}
public required init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: AppPermission.entity(), insertInto: context)
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
usageDescription = try container.decode(String.self, forKey: .usageDescription)
let rawType = try container.decode(String.self, forKey: .type)
guard let type = ALTAppPermissionType(rawValue: rawType) else {
throw DecodingError.dataCorrupted(
DecodingError.Context(codingPath: [CodingKeys.type],
debugDescription: "Invalid value for `ALTAppPermissionType` \"\(rawType)\""))
}
self.type = type
} catch {
if let context = managedObjectContext {
context.delete(self)
}
throw error
}
}
}
public extension AppPermission {
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission> {
NSFetchRequest<AppPermission>(entityName: "AppPermission")
}
}

View File

@@ -0,0 +1,108 @@
//
// AppVersion.swift
// SideStoreCore
//
// Created by Riley Testut on 8/18/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
@objc(AppVersion)
public class AppVersion: NSManagedObject, Decodable, Fetchable {
/* Properties */
@NSManaged public var version: String
@NSManaged public var date: Date
@NSManaged public var localizedDescription: String?
@NSManaged public var downloadURL: URL
@NSManaged public var size: Int64
@nonobjc public var minOSVersion: OperatingSystemVersion? {
guard let osVersionString = _minOSVersion else { return nil }
let osVersion = OperatingSystemVersion(string: osVersionString)
return osVersion
}
@NSManaged @objc(minOSVersion) private var _minOSVersion: String?
@nonobjc public var maxOSVersion: OperatingSystemVersion? {
guard let osVersionString = _maxOSVersion else { return nil }
let osVersion = OperatingSystemVersion(string: osVersionString)
return osVersion
}
@NSManaged @objc(maxOSVersion) private var _maxOSVersion: String?
@NSManaged public var appBundleID: String
@NSManaged public var sourceID: String?
/* Relationships */
@NSManaged public private(set) var app: StoreApp?
@NSManaged public private(set) var latestVersionApp: StoreApp?
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey {
case version
case date
case localizedDescription
case downloadURL
case size
}
public required init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: AppVersion.entity(), insertInto: context)
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
version = try container.decode(String.self, forKey: .version)
date = try container.decode(Date.self, forKey: .date)
localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
downloadURL = try container.decode(URL.self, forKey: .downloadURL)
size = try container.decode(Int64.self, forKey: .size)
} catch {
if let context = managedObjectContext {
context.delete(self)
}
throw error
}
}
}
public extension AppVersion {
@nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion> {
NSFetchRequest<AppVersion>(entityName: "AppVersion")
}
class func makeAppVersion(
version: String,
date: Date,
localizedDescription: String? = nil,
downloadURL: URL,
size: Int64,
appBundleID: String,
sourceID: String? = nil,
in context: NSManagedObjectContext
) -> AppVersion {
let appVersion = AppVersion(context: context)
appVersion.version = version
appVersion.date = date
appVersion.localizedDescription = localizedDescription
appVersion.downloadURL = downloadURL
appVersion.size = size
appVersion.appBundleID = appBundleID
appVersion.sourceID = sourceID
return appVersion
}
}

View File

@@ -0,0 +1,366 @@
//
// DatabaseManager.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import AltSign
import Roxas
private extension CFNotificationName {
static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
}
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { _, _, _, _, _ in
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
}
private class PersistentContainer: RSTPersistentContainer {
override class func defaultDirectoryURL() -> URL {
guard let sharedDirectoryURL = FileManager.default.altstoreSharedDirectory else { return super.defaultDirectoryURL() }
let databaseDirectoryURL = sharedDirectoryURL.appendingPathComponent("Database")
try? FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil)
return databaseDirectoryURL
}
class func legacyDirectoryURL() -> URL {
super.defaultDirectoryURL()
}
}
public final class DatabaseManager {
public static let shared = DatabaseManager()
public let persistentContainer: RSTPersistentContainer
public private(set) var isStarted = false
private var startCompletionHandlers = [(Error?) -> Void]()
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
private var ignoreWillAccessDatabaseNotification = false
private init() {
persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
persistentContainer.preferredMergePolicy = MergePolicy()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
}
}
public extension DatabaseManager {
func start(completionHandler: @escaping (Error?) -> Void) {
func finish(_ error: Error?) {
dispatchQueue.async {
if error == nil {
self.isStarted = true
}
self.startCompletionHandlers.forEach { $0(error) }
self.startCompletionHandlers.removeAll()
}
}
dispatchQueue.async {
self.startCompletionHandlers.append(completionHandler)
guard self.startCompletionHandlers.count == 1 else { return }
guard !self.isStarted else { return finish(nil) }
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
self.migrateDatabaseToAppGroupIfNeeded { result in
switch result {
case let .failure(error): finish(error)
case .success:
self.persistentContainer.loadPersistentStores { _, error in
guard error == nil else { return finish(error!) }
self.prepareDatabase { result in
switch result {
case let .failure(error): finish(error)
case .success: finish(nil)
}
}
}
}
}
}
}
func signOut(completionHandler: @escaping (Error?) -> Void) {
persistentContainer.performBackgroundTask { context in
if let account = self.activeAccount(in: context) {
account.isActiveAccount = false
}
if let team = self.activeTeam(in: context) {
team.isActiveTeam = false
}
do {
try context.save()
Keychain.shared.reset()
completionHandler(nil)
} catch {
print("Failed to save when signing out.", error)
completionHandler(error)
}
}
}
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
persistentContainer.performBackgroundTask { context in
do {
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
loggedErrors.forEach { context.delete($0) }
try context.save()
completion(.success(()))
} catch {
completion(.failure(error))
}
}
}
}
public extension DatabaseManager {
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account? {
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
let activeAccount = Account.first(satisfying: predicate, in: context)
return activeAccount
}
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team? {
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
let activeTeam = Team.first(satisfying: predicate, in: context)
return activeTeam
}
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount? {
guard let patreonAccountID = Keychain.shared.patreonAccountID else { return nil }
let predicate = NSPredicate(format: "%K == %@", #keyPath(PatreonAccount.identifier), patreonAccountID)
let patreonAccount = PatreonAccount.first(satisfying: predicate, in: context)
return patreonAccount
}
}
private extension DatabaseManager {
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void) {
guard !Bundle.isAppExtension() else { return completionHandler(.success(())) }
let context = persistentContainer.newBackgroundContext()
context.performAndWait {
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
let altStoreSource: Source
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) {
altStoreSource = source
} else {
altStoreSource = Source.makeAltStoreSource(in: context)
}
// Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL
let storeApp: StoreApp
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context) {
storeApp = app
} else {
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.latestVersion?.version = localApp.version
storeApp.source = altStoreSource
}
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp
if let app = storeApp.installedApp {
installedApp = app
} else {
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp
}
/* App Extensions */
var installedExtensions = Set<InstalledExtension>()
for appExtension in localApp.appExtensions {
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: localApp.bundleIdentifier, with: StoreApp.altstoreAppID)
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID }) {
installedExtension = appExtension
} else {
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: context)
}
installedExtension.update(resignedAppExtension: appExtension)
installedExtensions.insert(installedExtension)
}
installedApp.appExtensions = installedExtensions
let fileURL = installedApp.fileURL
#if DEBUG
let replaceCachedApp = true
#else
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
#endif
if replaceCachedApp {
func update(_ bundle: Bundle, bundleID: String) throws {
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
}
FileManager.default.prepareTemporaryURL { temporaryFileURL in
do {
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
try update(appBundle, bundleID: StoreApp.altstoreAppID)
if let tempApp = ALTApplication(fileURL: temporaryFileURL) {
for appExtension in tempApp.appExtensions {
guard let extensionBundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.invalidApp) }
guard let installedExtension = installedExtensions.first(where: { $0.resignedBundleIdentifier == appExtension.bundleIdentifier }) else { throw ALTError(.invalidApp) }
try update(extensionBundle, bundleID: installedExtension.bundleIdentifier)
}
}
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
} catch {
print("Failed to copy AltStore app bundle to its proper location.", error)
}
}
}
let cachedRefreshedDate = installedApp.refreshedDate
let cachedExpirationDate = installedApp.expirationDate
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
if installedApp.refreshedDate < cachedRefreshedDate {
// Embedded provisioning profile has a creation date older than our refreshed date.
// This most likely means we've refreshed the app since then, and profile is now outdated,
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
installedApp.refreshedDate = cachedRefreshedDate
installedApp.expirationDate = cachedExpirationDate
}
do {
try context.save()
completionHandler(.success(()))
} catch {
completionHandler(.failure(error))
}
}
}
func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result<Void, Error>) -> Void) {
guard UserDefaults.shared.requiresAppGroupMigration else { return completion(.success(())) }
func finish(_ result: Result<Void, Error>) {
switch result {
case let .failure(error): completion(.failure(error))
case .success:
UserDefaults.shared.requiresAppGroupMigration = false
completion(.success(()))
}
}
let previousDatabaseURL = PersistentContainer.legacyDirectoryURL().appendingPathComponent("AltStore.sqlite")
let databaseURL = PersistentContainer.defaultDirectoryURL().appendingPathComponent("AltStore.sqlite")
let previousAppsDirectoryURL = InstalledApp.legacyAppsDirectoryURL
let appsDirectoryURL = InstalledApp.appsDirectoryURL
let databaseIntent = NSFileAccessIntent.writingIntent(with: databaseURL, options: [.forReplacing])
let appsIntent = NSFileAccessIntent.writingIntent(with: appsDirectoryURL, options: [.forReplacing])
coordinator.coordinate(with: [databaseIntent, appsIntent], queue: coordinatorQueue) { error in
do {
if let error = error {
throw error
}
let description = NSPersistentStoreDescription(url: previousDatabaseURL)
// Disable WAL to remove extra files automatically during migration.
description.setOption(["journal_mode": "DELETE"] as NSDictionary, forKey: NSSQLitePragmasOption)
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.persistentContainer.managedObjectModel)
// Migrate database
if FileManager.default.fileExists(atPath: previousDatabaseURL.path) {
if FileManager.default.fileExists(atPath: databaseURL.path, isDirectory: nil) {
try FileManager.default.removeItem(at: databaseURL)
}
let previousDatabase = try persistentStoreCoordinator.addPersistentStore(ofType: description.type, configurationName: description.configuration, at: description.url, options: description.options)
// Pass nil options to prevent later error due to self.persistentContainer using WAL.
try persistentStoreCoordinator.migratePersistentStore(previousDatabase, to: databaseURL, options: nil, withType: NSSQLiteStoreType)
try FileManager.default.removeItem(at: previousDatabaseURL)
}
// Migrate apps
if FileManager.default.fileExists(atPath: previousAppsDirectoryURL.path, isDirectory: nil) {
_ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL)
}
finish(.success(()))
} catch {
print("Failed to migrate database to app group:", error)
finish(.failure(error))
}
}
}
func receivedWillAccessDatabaseNotification() {
defer { self.ignoreWillAccessDatabaseNotification = false }
// Ignore notifications sent by the current process.
guard !ignoreWillAccessDatabaseNotification else { return }
exit(104)
}
}

View File

@@ -0,0 +1,320 @@
//
// InstalledApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
import SemanticVersion
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
public let ALTActiveAppsLimit = 3
public protocol InstalledAppProtocol: Fetchable {
var name: String { get }
var bundleIdentifier: String { get }
var resignedBundleIdentifier: String { get }
var version: String { get }
var refreshedDate: Date { get }
var expirationDate: Date { get }
var installedDate: Date { get }
}
@objc(InstalledApp)
public class InstalledApp: NSManagedObject, InstalledAppProtocol {
/* Properties */
@NSManaged public var name: String
@NSManaged public var bundleIdentifier: String
@NSManaged public var resignedBundleIdentifier: String
@NSManaged public var version: String
@NSManaged public var refreshedDate: Date
@NSManaged public var expirationDate: Date
@NSManaged public var installedDate: Date
@NSManaged public var isActive: Bool
@NSManaged public var needsResign: Bool
@NSManaged public var hasAlternateIcon: Bool
@NSManaged public var certificateSerialNumber: String?
/* Transient */
@NSManaged public var isRefreshing: Bool
/* Relationships */
@NSManaged public var storeApp: StoreApp?
@NSManaged public var team: Team?
@NSManaged public var appExtensions: Set<InstalledExtension>
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
public var isSideloaded: Bool {
storeApp == nil
}
@objc public var hasUpdate: Bool {
if storeApp == nil { return false }
if storeApp!.latestVersion == nil { return false }
let currentVersion = SemanticVersion(version)
let latestVersion = SemanticVersion(storeApp!.latestVersion!.version)
if currentVersion == nil || latestVersion == nil {
// One of the versions is not valid SemVer, fall back to comparing the version strings by character
return version < storeApp!.latestVersion!.version
}
return currentVersion! < latestVersion!
}
public var appIDCount: Int {
1 + appExtensions.count
}
public var requiredActiveSlots: Int {
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
return requiredActiveSlots
}
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext) {
super.init(entity: InstalledApp.entity(), insertInto: context)
bundleIdentifier = originalBundleIdentifier
print("InstalledApp `self.bundleIdentifier`: \(bundleIdentifier)")
refreshedDate = Date()
installedDate = Date()
expirationDate = refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
}
public func update(resignedApp: ALTApplication, certificateSerialNumber: String?) {
name = resignedApp.name
resignedBundleIdentifier = resignedApp.bundleIdentifier
version = resignedApp.version
self.certificateSerialNumber = certificateSerialNumber
if let provisioningProfile = resignedApp.provisioningProfile {
update(provisioningProfile: provisioningProfile)
}
}
public func update(provisioningProfile: ALTProvisioningProfile) {
refreshedDate = provisioningProfile.creationDate
expirationDate = provisioningProfile.expirationDate
}
public func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void) {
let hasAlternateIcon = self.hasAlternateIcon
let alternateIconURL = self.alternateIconURL
let fileURL = self.fileURL
DispatchQueue.global().async {
do {
if hasAlternateIcon,
case let data = try Data(contentsOf: alternateIconURL),
let icon = UIImage(data: data) {
return completion(.success(icon))
}
let application = ALTApplication(fileURL: fileURL)
completion(.success(application?.icon))
} catch {
completion(.failure(error))
}
}
}
}
public extension InstalledApp {
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp> {
NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
}
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp> {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
return fetchRequest
}
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp> {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
print("Active Apps Fetch Request: \(String(describing: fetchRequest.predicate))")
return fetchRequest
}
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp? {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Fetch 'AltStore' Predicate: \(String(describing: predicate))")
let altStore = InstalledApp.first(satisfying: predicate, in: context)
return altStore
}
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp] {
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
return activeApps
}
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] {
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
// {
// // No additional predicate
// }
// else
// {
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
// }
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
in: context)
if let altStoreApp = InstalledApp.fetchAltStore(in: context) {
// Refresh AltStore last since it causes app to quit.
installedApps.append(altStoreApp)
}
return installedApps
}
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp] {
// Date 6 hours before now.
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.isActive),
#keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))")
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
// {
// // No additional predicate
// }
// else
// {
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
// }
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
in: context)
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date {
// Refresh AltStore last since it may cause app to quit.
installedApps.append(altStoreApp)
}
return installedApps
}
}
public extension InstalledApp {
var openAppURL: URL {
let openAppURL = URL(string: "altstore-" + bundleIdentifier + "://")!
return openAppURL
}
class func openAppURL(for app: AppProtocol) -> URL {
let openAppURL = URL(string: "altstore-" + app.bundleIdentifier + "://")!
return openAppURL
}
}
public extension InstalledApp {
class var appsDirectoryURL: URL {
let baseDirectory = FileManager.default.altstoreSharedDirectory ?? FileManager.default.applicationSupportDirectory
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) } catch { print("Creating App Directory Error: \(error)") }
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL
}
class var legacyAppsDirectoryURL: URL {
let baseDirectory = FileManager.default.applicationSupportDirectory
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL
}
class func fileURL(for app: AppProtocol) -> URL {
let appURL = directoryURL(for: app).appendingPathComponent("App.app")
return appURL
}
class func refreshedIPAURL(for app: AppProtocol) -> URL {
let ipaURL = directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
print("`ipaURL`: \(ipaURL.absoluteString)")
return ipaURL
}
class func directoryURL(for app: AppProtocol) -> URL {
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) } catch { print(error) }
return directoryURL
}
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String {
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
return installedAppUTI
}
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String {
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
return installedBackupAppUTI
}
class func alternateIconURL(for app: AppProtocol) -> URL {
let installedBackupAppUTI = directoryURL(for: app).appendingPathComponent("AltIcon.png")
return installedBackupAppUTI
}
var directoryURL: URL {
InstalledApp.directoryURL(for: self)
}
var fileURL: URL {
InstalledApp.fileURL(for: self)
}
var refreshedIPAURL: URL {
InstalledApp.refreshedIPAURL(for: self)
}
var installedAppUTI: String {
InstalledApp.installedAppUTI(forBundleIdentifier: resignedBundleIdentifier)
}
var installedBackupAppUTI: String {
InstalledApp.installedBackupAppUTI(forBundleIdentifier: resignedBundleIdentifier)
}
var alternateIconURL: URL {
InstalledApp.alternateIconURL(for: self)
}
}

View File

@@ -0,0 +1,67 @@
//
// InstalledExtension.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
@objc(InstalledExtension)
public class InstalledExtension: NSManagedObject, InstalledAppProtocol {
/* Properties */
@NSManaged public var name: String
@NSManaged public var bundleIdentifier: String
@NSManaged public var resignedBundleIdentifier: String
@NSManaged public var version: String
@NSManaged public var refreshedDate: Date
@NSManaged public var expirationDate: Date
@NSManaged public var installedDate: Date
/* Relationships */
@NSManaged public var parentApp: InstalledApp?
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext) {
super.init(entity: InstalledExtension.entity(), insertInto: context)
bundleIdentifier = originalBundleIdentifier
refreshedDate = Date()
installedDate = Date()
expirationDate = refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
update(resignedAppExtension: resignedAppExtension)
}
public func update(resignedAppExtension: ALTApplication) {
name = resignedAppExtension.name
resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
version = resignedAppExtension.version
if let provisioningProfile = resignedAppExtension.provisioningProfile {
update(provisioningProfile: provisioningProfile)
}
}
public func update(provisioningProfile: ALTProvisioningProfile) {
refreshedDate = provisioningProfile.creationDate
expirationDate = provisioningProfile.expirationDate
}
}
public extension InstalledExtension {
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension> {
NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
}
}

View File

@@ -0,0 +1,117 @@
//
// LoggedError.swift
// SideStoreCore
//
// Created by Riley Testut on 9/6/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
public extension LoggedError {
enum Operation: String {
case install
case update
case refresh
case activate
case deactivate
case backup
case restore
}
}
@objc(LoggedError)
public class LoggedError: NSManagedObject, Fetchable {
/* Properties */
@NSManaged public private(set) var date: Date
@nonobjc public var operation: Operation? {
guard let rawOperation = _operation else { return nil }
let operation = Operation(rawValue: rawOperation)
return operation
}
@NSManaged @objc(operation) private var _operation: String?
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var code: Int32
@NSManaged public private(set) var userInfo: [String: Any]
@NSManaged public private(set) var appName: String
@NSManaged public private(set) var appBundleID: String
/* Relationships */
@NSManaged public private(set) var storeApp: StoreApp?
@NSManaged public private(set) var installedApp: InstalledApp?
private static let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none
return dateFormatter
}()
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(error: Error, app: AppProtocol, date: Date = Date(), operation: Operation? = nil, context: NSManagedObjectContext) {
super.init(entity: LoggedError.entity(), insertInto: context)
self.date = date
_operation = operation?.rawValue
let nsError = error as NSError
domain = nsError.domain
code = Int32(nsError.code)
userInfo = nsError.userInfo
appName = app.name
appBundleID = app.bundleIdentifier
switch app {
case let storeApp as StoreApp: self.storeApp = storeApp
case let installedApp as InstalledApp: self.installedApp = installedApp
default: break
}
}
}
public extension LoggedError {
var app: AppProtocol {
// `as AppProtocol` needed to fix "cannot convert AnyApp to StoreApp" compiler error with Xcode 14.
let app = installedApp ?? storeApp ?? AnyApp(name: appName, bundleIdentifier: appBundleID, url: nil) as AppProtocol
return app
}
var error: Error {
let nsError = NSError(domain: domain, code: Int(code), userInfo: userInfo)
return nsError
}
@objc
var localizedDateString: String {
let localizedDateString = LoggedError.dateFormatter.string(from: date)
return localizedDateString
}
var localizedFailure: String? {
guard let operation = operation else { return nil }
switch operation {
case .install: return String(format: NSLocalizedString("Install %@ Failed", comment: ""), appName)
case .update: return String(format: NSLocalizedString("Update %@ Failed", comment: ""), appName)
case .refresh: return String(format: NSLocalizedString("Refresh %@ Failed", comment: ""), appName)
case .activate: return String(format: NSLocalizedString("Activate %@ Failed", comment: ""), appName)
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), appName)
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), appName)
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), appName)
}
}
}
public extension LoggedError {
@nonobjc class func fetchRequest() -> NSFetchRequest<LoggedError> {
NSFetchRequest<LoggedError>(entityName: "LoggedError")
}
}

View File

@@ -0,0 +1,32 @@
//
// ManagedPatron.swift
// AltStoreCore
//
// Created by Riley Testut on 4/18/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
@objc(ManagedPatron)
public class ManagedPatron: NSManagedObject, Fetchable {
@NSManaged public var name: String
@NSManaged public var identifier: String
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(patron: Patron, context: NSManagedObjectContext) {
super.init(entity: ManagedPatron.entity(), insertInto: context)
name = patron.name
identifier = patron.identifier
}
}
public extension ManagedPatron {
@nonobjc class func fetchRequest() -> NSFetchRequest<ManagedPatron> {
NSFetchRequest<ManagedPatron>(entityName: "Patron")
}
}

View File

@@ -0,0 +1,99 @@
//
// MergePolicy.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Roxas
open class MergePolicy: RSTRelationshipPreservingMergePolicy {
override open func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws {
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
for conflict in conflicts {
switch conflict.conflictingObjects.first {
case is StoreApp where conflict.conflictingObjects.count == 2:
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp {
// Delete previous permissions (same as below).
for permission in previousApp.permissions {
permission.managedObjectContext?.delete(permission)
}
// Delete previous versions (different than below).
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil {
appVersion.managedObjectContext?.delete(appVersion)
}
}
case is AppVersion where conflict.conflictingObjects.count == 2:
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion }) {
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
}
default:
// Unknown context-level conflict.
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
}
}
try super.resolve(constraintConflicts: conflicts)
return
}
for conflict in conflicts {
switch conflict.databaseObject {
case let databaseObject as StoreApp:
// Delete previous permissions
for permission in databaseObject.permissions {
permission.managedObjectContext?.delete(permission)
}
if let contextApp = conflict.conflictingObjects.first as? StoreApp {
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version) {
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
appVersion.managedObjectContext?.delete(appVersion)
}
}
case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
for app in databaseObject.apps {
if !bundleIdentifiers.contains(app.bundleIdentifier) {
// No longer listed in Source, so remove it from database.
app.managedObjectContext?.delete(app)
}
}
for newsItem in databaseObject.newsItems {
if !newsItemIdentifiers.contains(newsItem.identifier) {
// No longer listed in Source, so remove it from database.
newsItem.managedObjectContext?.delete(newsItem)
}
}
default: break
}
}
try super.resolve(constraintConflicts: conflicts)
}
}

View File

@@ -0,0 +1,84 @@
//
// NewsItem.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import UIKit
@objc(NewsItem)
public class NewsItem: NSManagedObject, Decodable, Fetchable {
/* Properties */
@NSManaged public var identifier: String
@NSManaged public var date: Date
@NSManaged public var title: String
@NSManaged public var caption: String
@NSManaged public var tintColor: UIColor
@NSManaged public var sortIndex: Int32
@NSManaged public var isSilent: Bool
@NSManaged public var imageURL: URL?
@NSManaged public var externalURL: URL?
@NSManaged public var appID: String?
@NSManaged public var sourceIdentifier: String?
/* Relationships */
@NSManaged public var storeApp: StoreApp?
@NSManaged public var source: Source?
private enum CodingKeys: String, CodingKey {
case identifier
case date
case title
case caption
case tintColor
case imageURL
case externalURL = "url"
case appID
case notify
}
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public required init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: NewsItem.entity(), insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
identifier = try container.decode(String.self, forKey: .identifier)
date = try container.decode(Date.self, forKey: .date)
title = try container.decode(String.self, forKey: .title)
caption = try container.decode(String.self, forKey: .caption)
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor) {
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
appID = try container.decodeIfPresent(String.self, forKey: .appID)
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
isSilent = !notify
}
}
public extension NewsItem {
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem> {
NSFetchRequest<NewsItem>(entityName: "NewsItem")
}
}

View File

@@ -0,0 +1,65 @@
//
// PatreonAccount.swift
// AltStore
//
// Created by Riley Testut on 8/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
extension PatreonAPI {
struct AccountResponse: Decodable {
struct Data: Decodable {
struct Attributes: Decodable {
var first_name: String?
var full_name: String
}
var id: String
var attributes: Attributes
}
var data: Data
var included: [PatronResponse]?
}
}
@objc(PatreonAccount)
public class PatreonAccount: NSManagedObject, Fetchable {
@NSManaged public var identifier: String
@NSManaged public var name: String
@NSManaged public var firstName: String?
@NSManaged public var isPatron: Bool
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext) {
super.init(entity: PatreonAccount.entity(), insertInto: context)
identifier = response.data.id
name = response.data.attributes.full_name
firstName = response.data.attributes.first_name
// if let patronResponse = response.included?.first
// {
// let patron = Patron(response: patronResponse)
// self.isPatron = (patron.status == .active)
// }
// else
// {
// self.isPatron = false
// }
isPatron = true
}
}
public extension PatreonAccount {
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount> {
NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
}
}

View File

@@ -0,0 +1,50 @@
//
// RefreshAttempt.swift
// AltStore
//
// Created by Riley Testut on 7/31/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
@objc(RefreshAttempt)
public class RefreshAttempt: NSManagedObject, Fetchable {
@NSManaged public var identifier: String
@NSManaged public var date: Date
@NSManaged public var isSuccess: Bool
@NSManaged public var errorDescription: String?
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(identifier: String, result: Result<[String: Result<InstalledApp, Error>], Error>, context: NSManagedObjectContext) {
super.init(entity: RefreshAttempt.entity(), insertInto: context)
self.identifier = identifier
date = Date()
do {
let results = try result.get()
for (_, result) in results {
guard case let .failure(error) = result else { continue }
throw error
}
isSuccess = true
errorDescription = nil
} catch {
isSuccess = false
errorDescription = error.localizedDescription
}
}
}
public extension RefreshAttempt {
@nonobjc class func fetchRequest() -> NSFetchRequest<RefreshAttempt> {
NSFetchRequest<RefreshAttempt>(entityName: "RefreshAttempt")
}
}

View File

@@ -0,0 +1,24 @@
//
// SecureValueTransformer.swift
// AltStore
//
// Created by Riley Testut on 8/18/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
@objc(ALTSecureValueTransformer)
public final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer {
public static let name = NSValueTransformerName(rawValue: "ALTSecureValueTransformer")
override public static var allowedTopLevelClasses: [AnyClass] {
let allowedClasses = super.allowedTopLevelClasses + [NSError.self]
return allowedClasses
}
public static func register() {
let transformer = SecureValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}

View File

@@ -0,0 +1,289 @@
//
// Source.swift
// AltStore
//
// Created by Riley Testut on 7/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import UIKit
public extension Source {
#if ALPHA
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
#else
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
#endif
#if STAGING
#if ALPHA
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
#else
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
#endif
#else
#if ALPHA
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
#else
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
#endif
#endif
}
public struct AppPermissionFeed: Codable {
let type: String // ALTAppPermissionType
let usageDescription: String
enum CodingKeys: String, CodingKey {
case type
case usageDescription
}
}
public struct AppVersionFeed: Codable {
/* Properties */
let version: String
let date: Date
let localizedDescription: String?
let downloadURL: URL
let size: Int64
enum CodingKeys: String, CodingKey {
case version
case date
case localizedDescription
case downloadURL
case size
}
}
public struct PlatformURLFeed: Codable {
/* Properties */
let platform: Platform
let downloadURL: URL
private enum CodingKeys: String, CodingKey {
case platform
case downloadURL
}
}
public struct StoreAppFeed: Codable {
let name: String
let bundleIdentifier: String
let subtitle: String?
let developerName: String
let localizedDescription: String
let size: Int64
let iconURL: URL
let screenshotURLs: [URL]
let version: String
let versionDate: Date
let versionDescription: String?
let downloadURL: URL
let platformURLs: [PlatformURLFeed]?
let tintColor: String? // UIColor?
let isBeta: Bool
// let source: Source?
let appPermission: [AppPermissionFeed]
let versions: [AppVersionFeed]
enum CodingKeys: String, CodingKey {
case bundleIdentifier
case developerName
case downloadURL
case iconURL
case isBeta = "beta"
case localizedDescription
case name
case appPermission = "permissions"
case platformURLs
case screenshotURLs
case size
case subtitle
case tintColor
case version
case versionDate
case versionDescription
case versions
}
}
public struct NewsItemFeed: Codable {
let identifier: String
let date: Date
let title: String
let caption: String
let tintColor: String // UIColor
let notify: Bool
let imageURL: URL?
let externalURL: URL?
let appID: String?
private enum CodingKeys: String, CodingKey {
case identifier
case date
case title
case caption
case tintColor
case imageURL
case externalURL = "url"
case appID
case notify
}
}
public struct SourceJSON: Codable {
let name: String
let identifier: String
let sourceURL: URL
let userInfo: [String: String]? // [ALTSourceUserInfoKey:String]?
let apps: [StoreAppFeed]
let news: [NewsItemFeed]
enum CodingKeys: String, CodingKey {
case name
case identifier
case sourceURL
case userInfo
case apps
case news
}
}
@objc(Source)
public class Source: NSManagedObject, Fetchable, Decodable {
/* Properties */
@NSManaged public var name: String
@NSManaged public var identifier: String
@NSManaged public var sourceURL: URL
@NSManaged public var error: NSError?
/* Non-Core Data Properties */
public var userInfo: [ALTSourceUserInfoKey: String]?
/* Relationships */
@objc(apps) @NSManaged public private(set) var _apps: NSOrderedSet
@objc(newsItems) @NSManaged public private(set) var _newsItems: NSOrderedSet
@nonobjc public var apps: [StoreApp] {
get {
_apps.array as! [StoreApp]
}
set {
_apps = NSOrderedSet(array: newValue)
}
}
@nonobjc public var newsItems: [NewsItem] {
get {
_newsItems.array as! [NewsItem]
}
set {
_newsItems = NSOrderedSet(array: newValue)
}
}
private enum CodingKeys: String, CodingKey {
case name
case identifier
case sourceURL
case userInfo
case apps
case news
}
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public required init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
super.init(entity: Source.entity(), insertInto: context)
do {
self.sourceURL = sourceURL
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
identifier = try container.decode(String.self, forKey: .identifier)
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
self.userInfo = userInfo?.reduce(into: [:]) {
guard let infoKey: ALTSourceUserInfoKey = ALTSourceUserInfoKey(rawValue: $1.key) else {
return
}
$0[infoKey] = $1.value
}
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { a, _ in a })
for (index, app) in apps.enumerated() {
app.sourceIdentifier = identifier
app.sortIndex = Int32(index)
}
_apps = NSMutableOrderedSet(array: apps)
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
for (index, item) in newsItems.enumerated() {
item.sourceIdentifier = identifier
item.sortIndex = Int32(index)
}
for newsItem in newsItems {
guard let appID = newsItem.appID else { continue }
if let storeApp = appsByID[appID] {
newsItem.storeApp = storeApp
} else {
newsItem.storeApp = nil
}
}
_newsItems = NSMutableOrderedSet(array: newsItems)
} catch {
if let context = managedObjectContext {
context.delete(self)
}
throw error
}
}
}
public extension Source {
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
NSFetchRequest<Source>(entityName: "Source")
}
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source {
let source = Source(context: context)
source.name = "SideStore Offical"
source.identifier = Source.altStoreIdentifier
source.sourceURL = Source.altStoreSourceURL
return source
}
class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source? {
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
return source
}
}

View File

@@ -0,0 +1,351 @@
//
// StoreApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
import Roxas
public extension StoreApp {
#if SWIFT_PACKAGE
#if ALPHA
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#elseif BETA
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#else
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#endif
#else
#if ALPHA
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#elseif BETA
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#else
static let altstoreAppID = Bundle.Info.appbundleIdentifier
#endif
#endif
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
}
@objc
public enum Platform: UInt, Codable {
case ios
case tvos
case macos
}
@objc
public final class PlatformURL: NSManagedObject, Decodable {
/* Properties */
@NSManaged public private(set) var platform: Platform
@NSManaged public private(set) var downloadURL: URL
private enum CodingKeys: String, CodingKey {
case platform
case downloadURL
}
public init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
// Must initialize with context in order for child context saves to work correctly.
super.init(entity: PlatformURL.entity(), insertInto: context)
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
platform = try container.decode(Platform.self, forKey: .platform)
downloadURL = try container.decode(URL.self, forKey: .downloadURL)
} catch {
if let context = managedObjectContext {
context.delete(self)
}
throw error
}
}
}
extension PlatformURL: Comparable {
public static func < (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
lhs.platform.rawValue < rhs.platform.rawValue
}
public static func > (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
lhs.platform.rawValue > rhs.platform.rawValue
}
public static func <= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
lhs.platform.rawValue <= rhs.platform.rawValue
}
public static func >= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
lhs.platform.rawValue >= rhs.platform.rawValue
}
}
public typealias PlatformURLs = [PlatformURL]
@objc(StoreApp)
public class StoreApp: NSManagedObject, Decodable, Fetchable {
/* Properties */
@NSManaged public private(set) var name: String
@NSManaged public private(set) var bundleIdentifier: String
@NSManaged public private(set) var subtitle: String?
@NSManaged public private(set) var developerName: String
@NSManaged public private(set) var localizedDescription: String
@NSManaged @objc(size) internal var _size: Int32
@NSManaged public private(set) var iconURL: URL
@NSManaged public private(set) var screenshotURLs: [URL]
@NSManaged @objc(version) internal var _version: String
@NSManaged @objc(versionDate) internal var _versionDate: Date
@NSManaged @objc(versionDescription) internal var _versionDescription: String?
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
@NSManaged public private(set) var platformURLs: PlatformURLs?
@NSManaged public private(set) var tintColor: UIColor?
@NSManaged public private(set) var isBeta: Bool
@objc public internal(set) var sourceIdentifier: String? {
get {
willAccessValue(forKey: #keyPath(sourceIdentifier))
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
let sourceIdentifier = primitiveSourceIdentifier
return sourceIdentifier
}
set {
willChangeValue(forKey: #keyPath(sourceIdentifier))
primitiveSourceIdentifier = newValue
didChangeValue(forKey: #keyPath(sourceIdentifier))
for version in versions {
version.sourceID = newValue
}
}
}
@NSManaged private var primitiveSourceIdentifier: String?
@NSManaged public var sortIndex: Int32
/* Relationships */
@NSManaged public var installedApp: InstalledApp?
@NSManaged public var newsItems: Set<NewsItem>
@NSManaged @objc(source) public var _source: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged public private(set) var latestVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@nonobjc public var source: Source? {
set {
_source = newValue
sourceIdentifier = newValue?.identifier
}
get {
_source
}
}
@nonobjc public var permissions: [AppPermission] {
_permissions.array as! [AppPermission]
}
@nonobjc public var versions: [AppVersion] {
_versions.array as! [AppVersion]
}
@nonobjc public var size: Int64? {
guard let version = latestVersion else { return nil }
return version.size
}
@nonobjc public var version: String? {
guard let version = latestVersion else { return nil }
return version.version
}
@nonobjc public var versionDescription: String? {
guard let version = latestVersion else { return nil }
return version.localizedDescription
}
@nonobjc public var versionDate: Date? {
guard let version = latestVersion else { return nil }
return version.date
}
@nonobjc public var downloadURL: URL? {
guard let version = self.latestVersion else { return nil }
return version.downloadURL
}
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey {
case name
case bundleIdentifier
case developerName
case localizedDescription
case version
case versionDescription
case versionDate
case iconURL
case screenshotURLs
case downloadURL
case platformURLs
case tintColor
case subtitle
case permissions
case size
case isBeta = "beta"
case versions
}
public required init(from decoder: Decoder) throws {
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
// Must initialize with context in order for child context saves to work correctly.
super.init(entity: StoreApp.entity(), insertInto: context)
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
developerName = try container.decode(String.self, forKey: .developerName)
localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
iconURL = try container.decode(URL.self, forKey: .iconURL)
screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
let downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
if let platformURLs = platformURLs {
self.platformURLs = platformURLs
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
if let first = platformURLs.sorted().first {
_downloadURL = first.downloadURL
} else {
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
}
} else if let downloadURL = downloadURL {
_downloadURL = downloadURL
} else {
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
}
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor) {
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
_permissions = NSOrderedSet(array: permissions)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions) {
// TODO: Throw error if there isn't at least one version.
for version in versions {
version.appBundleID = bundleIdentifier
}
setVersions(versions)
} else {
let version = try container.decode(String.self, forKey: .version)
let versionDate = try container.decode(Date.self, forKey: .versionDate)
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
let size = try container.decode(Int32.self, forKey: .size)
let appVersion = AppVersion.makeAppVersion(version: version,
date: versionDate,
localizedDescription: versionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: bundleIdentifier,
in: context)
setVersions([appVersion])
}
} catch {
if let context = managedObjectContext {
context.delete(self)
}
throw error
}
}
}
private extension StoreApp {
func setVersions(_ versions: [AppVersion]) {
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self.latestVersion = latestVersion
_versions = NSOrderedSet(array: versions)
// Preserve backwards compatibility by assigning legacy property values.
_version = latestVersion.version
_versionDate = latestVersion.date
_versionDescription = latestVersion.localizedDescription
_downloadURL = latestVersion.downloadURL
_size = Int32(latestVersion.size)
}
}
public extension StoreApp {
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp> {
NSFetchRequest<StoreApp>(entityName: "StoreApp")
}
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp {
let app = StoreApp(context: context)
app.name = "SideStore"
app.bundleIdentifier = StoreApp.altstoreAppID
app.developerName = "Side Team"
app.localizedDescription = "SideStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = []
app.sourceIdentifier = Source.altStoreIdentifier
let appVersion = AppVersion.makeAppVersion(version: "0.3.0",
date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!,
size: 0,
appBundleID: app.bundleIdentifier,
sourceID: Source.altStoreIdentifier,
in: context)
app.setVersions([appVersion])
print("makeAltStoreApp StoreApp: \(String(describing: app))")
#if BETA
app.isBeta = true
#endif
return app
}
}

View File

@@ -0,0 +1,71 @@
//
// Team.swift
// AltStore
//
// Created by Riley Testut on 5/31/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Foundation
import AltSign
public extension ALTTeamType {
var localizedDescription: String {
switch self {
case .free: return NSLocalizedString("Free Developer Account", comment: "")
case .individual: return NSLocalizedString("Developer", comment: "")
case .organization: return NSLocalizedString("Organization", comment: "")
case .unknown: fallthrough
@unknown default: return NSLocalizedString("Unknown", comment: "")
}
}
}
public extension Team {
static let maximumFreeAppIDs = 10
}
@objc(Team)
public class Team: NSManagedObject, Fetchable {
/* Properties */
@NSManaged public var name: String
@NSManaged public var identifier: String
@NSManaged public var type: ALTTeamType
@NSManaged public var isActiveTeam: Bool
/* Relationships */
@NSManaged public private(set) var account: Account!
@NSManaged public var installedApps: Set<InstalledApp>
@NSManaged public private(set) var appIDs: Set<AppID>
public var altTeam: ALTTeam?
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext) {
super.init(entity: Team.entity(), insertInto: context)
self.account = account
update(team: team)
}
public func update(team: ALTTeam) {
altTeam = team
name = team.name
identifier = team.identifier
type = team.type
}
}
public extension Team {
@nonobjc class func fetchRequest() -> NSFetchRequest<Team> {
NSFetchRequest<Team>(entityName: "Team")
}
}

View File

@@ -0,0 +1,24 @@
//
// Benefit.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI {
struct BenefitResponse: Decodable {
var id: String
}
}
public struct Benefit: Hashable {
public var type: ALTPatreonBenefitType
init?(response: PatreonAPI.BenefitResponse) {
guard let type = ALTPatreonBenefitType(rawValue: response.id) else { return nil }
self.type = type
}
}

View File

@@ -0,0 +1,23 @@
//
// Campaign.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI {
struct CampaignResponse: Decodable {
var id: String
}
}
public struct Campaign {
public var identifier: String
init(response: PatreonAPI.CampaignResponse) {
identifier = response.id
}
}

View File

@@ -0,0 +1,367 @@
//
// PatreonAPI.swift
// AltStore
//
// Created by Riley Testut on 8/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import AuthenticationServices
import CoreData
import Foundation
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
private let campaignID = "2863968"
extension PatreonAPI {
enum Error: LocalizedError {
case unknown
case notAuthenticated
case invalidAccessToken
var errorDescription: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
}
}
}
enum AuthorizationType {
case none
case user
case creator
}
enum AnyResponse: Decodable {
case tier(TierResponse)
case benefit(BenefitResponse)
enum CodingKeys: String, CodingKey {
case type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "tier":
let tier = try TierResponse(from: decoder)
self = .tier(tier)
case "benefit":
let benefit = try BenefitResponse(from: decoder)
self = .benefit(benefit)
default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Patreon response type.")
}
}
}
}
public class PatreonAPI: NSObject {
public static let shared = PatreonAPI()
public var isAuthenticated: Bool {
Keychain.shared.patreonAccessToken != nil
}
private var authenticationSession: ASWebAuthenticationSession?
private let session = URLSession(configuration: .ephemeral)
private let baseURL = URL(string: "https://www.patreon.com/")!
override private init() {
super.init()
}
}
public extension PatreonAPI {
func authenticate(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void) {
var components = URLComponents(string: "/oauth2/authorize")!
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")]
let requestURL = components.url(relativeTo: baseURL)!
authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { callbackURL, error in
do {
let callbackURL = try Result(callbackURL, error).get()
guard
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value
else { throw Error.unknown }
self.fetchAccessToken(oauthCode: code) { result in
switch result {
case let .failure(error): completion(.failure(error))
case let .success((accessToken, refreshToken)):
Keychain.shared.patreonAccessToken = accessToken
Keychain.shared.patreonRefreshToken = refreshToken
self.fetchAccount(completion: completion)
}
}
} catch {
completion(.failure(error))
}
}
if #available(iOS 13.0, *) {
self.authenticationSession?.presentationContextProvider = self
}
authenticationSession?.start()
}
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void) {
var components = URLComponents(string: "/api/oauth2/v2/identity")!
components.queryItems = [URLQueryItem(name: "include", value: "memberships"),
URLQueryItem(name: "fields[user]", value: "first_name,full_name"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
let requestURL = components.url(relativeTo: baseURL)!
let request = URLRequest(url: requestURL)
send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in
switch result {
case .failure(Error.notAuthenticated):
self.signOut { _ in
completion(.failure(Error.notAuthenticated))
}
case let .failure(error): completion(.failure(error))
case let .success(response):
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let account = PatreonAccount(response: response, context: context)
Keychain.shared.patreonAccountID = account.identifier
completion(.success(account))
}
}
}
}
func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void) {
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
URLQueryItem(name: "fields[tier]", value: "title"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
URLQueryItem(name: "page[size]", value: "1000")]
let requestURL = components.url(relativeTo: baseURL)!
struct Response: Decodable {
var data: [PatronResponse]
var included: [AnyResponse]
var links: [String: URL]?
}
var allPatrons = [Patron]()
func fetchPatrons(url: URL) {
let request = URLRequest(url: url)
send(request, authorizationType: .creator) { (result: Result<Response, Swift.Error>) in
switch result {
case let .failure(error): completion(.failure(error))
case let .success(response):
let tiers = response.included.compactMap { response -> Tier? in
switch response {
case let .tier(tierResponse): return Tier(response: tierResponse)
case .benefit: return nil
}
}
let tiersByIdentifier = Dictionary(tiers.map { ($0.identifier, $0) }, uniquingKeysWith: { a, _ in a })
let patrons = response.data.map { response -> Patron in
let patron = Patron(response: response)
for tierID in response.relationships?.currently_entitled_tiers.data ?? [] {
guard let tier = tiersByIdentifier[tierID.id] else { continue }
patron.benefits.formUnion(tier.benefits)
}
return patron
}.filter { $0.benefits.contains(where: { $0.type == .credits }) }
allPatrons.append(contentsOf: patrons)
if let nextURL = response.links?["next"] {
fetchPatrons(url: nextURL)
} else {
completion(.success(allPatrons))
}
}
}
}
fetchPatrons(url: requestURL)
}
func signOut(completion: @escaping (Result<Void, Swift.Error>) -> Void) {
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
do {
let accounts = PatreonAccount.all(in: context, requestProperties: [\.returnsObjectsAsFaults: true])
accounts.forEach(context.delete(_:))
self.deactivateBetaApps(in: context)
try context.save()
Keychain.shared.patreonAccessToken = nil
Keychain.shared.patreonRefreshToken = nil
Keychain.shared.patreonAccountID = nil
completion(.success(()))
} catch {
completion(.failure(error))
}
}
}
func refreshPatreonAccount() {
guard PatreonAPI.shared.isAuthenticated else { return }
PatreonAPI.shared.fetchAccount { (result: Result<PatreonAccount, Swift.Error>) in
do {
let account = try result.get()
// if let context = account.managedObjectContext, !account.isPatron
// {
// Deactivate all beta apps now that we're no longer a patron.
// self.deactivateBetaApps(in: context)
// }
try account.managedObjectContext?.save()
} catch {
print("Failed to fetch Patreon account.", error)
}
}
}
}
private extension PatreonAPI {
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void) {
let encodedRedirectURI = ("https://rileytestut.com/patreon/altstore" as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
let encodedOauthCode = (oauthCode as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
let body = "code=\(encodedOauthCode)&grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(encodedRedirectURI)"
let requestURL = URL(string: "/api/oauth2/token", relativeTo: baseURL)!
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = body.data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
struct Response: Decodable {
var access_token: String
var refresh_token: String
}
send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
switch result {
case let .failure(error): completion(.failure(error))
case let .success(response): completion(.success((response.access_token, response.refresh_token)))
}
}
}
func refreshAccessToken(completion: @escaping (Result<Void, Swift.Error>) -> Void) {
guard let refreshToken = Keychain.shared.patreonRefreshToken else { return }
var components = URLComponents(string: "/api/oauth2/token")!
components.queryItems = [URLQueryItem(name: "grant_type", value: "refresh_token"),
URLQueryItem(name: "refresh_token", value: refreshToken),
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "client_secret", value: clientSecret)]
let requestURL = components.url(relativeTo: baseURL)!
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
struct Response: Decodable {
var access_token: String
var refresh_token: String
}
send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
switch result {
case let .failure(error): completion(.failure(error))
case let .success(response):
Keychain.shared.patreonAccessToken = response.access_token
Keychain.shared.patreonRefreshToken = response.refresh_token
completion(.success(()))
}
}
}
func send<ResponseType: Decodable>(_ request: URLRequest, authorizationType: AuthorizationType, completion: @escaping (Result<ResponseType, Swift.Error>) -> Void) {
var request = request
switch authorizationType {
case .none: break
case .creator:
guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user:
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
let task = session.dataTask(with: request) { data, response, error in
do {
let data = try Result(data, error).get()
if let response = response as? HTTPURLResponse, response.statusCode == 401 {
switch authorizationType {
case .creator: completion(.failure(Error.invalidAccessToken))
case .none: completion(.failure(Error.notAuthenticated))
case .user:
self.refreshAccessToken { result in
switch result {
case let .failure(error): completion(.failure(error))
case .success: self.send(request, authorizationType: authorizationType, completion: completion)
}
}
}
return
}
let response = try JSONDecoder().decode(ResponseType.self, from: data)
completion(.success(response))
} catch {
completion(.failure(error))
}
}
task.resume()
}
func deactivateBetaApps(in context: NSManagedObjectContext) {
let predicate = NSPredicate(format: "%K != %@ AND %K != nil AND %K == YES",
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID, #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))
let installedApps = InstalledApp.all(satisfying: predicate, in: context)
installedApps.forEach { $0.isActive = false }
}
}
@available(iOS 13.0, *)
extension PatreonAPI: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for _: ASWebAuthenticationSession) -> ASPresentationAnchor {
UIApplication.alt_shared?.keyWindow ?? UIWindow()
}
}

View File

@@ -0,0 +1,65 @@
//
// Patron.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI {
struct PatronResponse: Decodable {
struct Attributes: Decodable {
var full_name: String
var patron_status: String?
}
struct Relationships: Decodable {
struct Tiers: Decodable {
struct TierID: Decodable {
var id: String
var type: String
}
var data: [TierID]
}
var currently_entitled_tiers: Tiers
}
var id: String
var attributes: Attributes
var relationships: Relationships?
}
}
public extension Patron {
enum Status: String, Decodable {
case active = "active_patron"
case declined = "declined_patron"
case former = "former_patron"
case unknown
}
}
public class Patron {
public var name: String
public var identifier: String
public var status: Status
public var benefits: Set<Benefit> = []
init(response: PatreonAPI.PatronResponse) {
name = response.attributes.full_name
identifier = response.id
if let status = response.attributes.patron_status {
self.status = Status(rawValue: status) ?? .unknown
} else {
status = .unknown
}
}
}

View File

@@ -0,0 +1,43 @@
//
// Tier.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI {
struct TierResponse: Decodable {
struct Attributes: Decodable {
var title: String
}
struct Relationships: Decodable {
struct Benefits: Decodable {
var data: [BenefitResponse]
}
var benefits: Benefits
}
var id: String
var attributes: Attributes
var relationships: Relationships
}
}
public struct Tier {
public var name: String
public var identifier: String
public var benefits: [Benefit] = []
init(response: PatreonAPI.TierResponse) {
name = response.attributes.title
identifier = response.id
benefits = response.relationships.benefits.data.compactMap(Benefit.init(response:))
}
}

View File

@@ -0,0 +1,46 @@
//
// AppProtocol.swift
// AltStore
//
// Created by Riley Testut on 7/26/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import AltSign
import Foundation
public protocol AppProtocol {
var name: String { get }
var bundleIdentifier: String { get }
var url: URL? { get }
}
public struct AnyApp: AppProtocol {
public var name: String
public var bundleIdentifier: String
public var url: URL?
public init(name: String, bundleIdentifier: String, url: URL?) {
self.name = name
self.bundleIdentifier = bundleIdentifier
self.url = url
}
}
extension ALTApplication: AppProtocol {
public var url: URL? {
fileURL
}
}
extension StoreApp: AppProtocol {
public var url: URL? {
downloadURL
}
}
extension InstalledApp: AppProtocol {
public var url: URL? {
fileURL
}
}

View File

@@ -0,0 +1,64 @@
//
// NSManagedObject+Conveniences.swift
// AltStore
//
// Created by Riley Testut on 6/6/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
public typealias FetchRequest = NSFetchRequest<NSFetchRequestResult>
public protocol Fetchable: NSManagedObject {}
public extension Fetchable {
static func first(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext,
requestProperties: [PartialKeyPath<FetchRequest>: Any?] = [:]) -> Self? {
let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: true)
return managedObjects.first
}
static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext,
requestProperties: [PartialKeyPath<FetchRequest>: Any?] = [:]) -> [Self] {
let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: false)
return managedObjects
}
static func fetch(_ fetchRequest: NSFetchRequest<Self>, in context: NSManagedObjectContext) -> [Self] {
do {
let managedObjects = try context.fetch(fetchRequest)
return managedObjects
} catch {
print("Failed to fetch managed objects. Fetch Request: \(fetchRequest). Error: \(error).")
return []
}
}
private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, requestProperties: [PartialKeyPath<FetchRequest>: Any?], returnFirstResult: Bool) -> [Self] {
let registeredObjects = context.registeredObjects.lazy.compactMap { $0 as? Self }.filter { predicate?.evaluate(with: $0) != false }
if let managedObject = registeredObjects.first, returnFirstResult {
return [managedObject]
}
let fetchRequest = self.fetchRequest() as! NSFetchRequest<Self>
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = sortDescriptors
fetchRequest.returnsObjectsAsFaults = false
for (keyPath, value) in requestProperties {
// Still no easy way to cast PartialKeyPath back to usable WritableKeyPath :(
guard let objcKeyString = keyPath._kvcKeyPathString else { continue }
fetchRequest.setValue(value, forKey: objcKeyString)
}
let fetchedObjects = fetch(fetchRequest, in: context)
if let fetchedObject = fetchedObjects.first, returnFirstResult {
return [fetchedObject]
} else {
return fetchedObjects
}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 11.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E230" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="268"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="89"/>
<element name="Patron" positionX="-36" positionY="153" width="128" height="59"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="appBundleID"/>
<constraint value="version"/>
<constraint value="sourceID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
<attribute name="appName" attributeType="String"/>
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="operation" optional="YES" attributeType="String"/>
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String" syncable="YES"/>
<attribute name="firstName" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="lastName" attributeType="String" syncable="YES"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String" syncable="YES"/>
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="caption" attributeType="String" syncable="YES"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="externalURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="imageURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="title" attributeType="String" syncable="YES"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source" syncable="YES"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="developerName" attributeType="String" syncable="YES"/>
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
<attribute name="iconURL" attributeType="URI" syncable="YES"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="screenshotURLs" attributeType="Transformable" syncable="YES"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem" syncable="YES"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
</elements>
</model>

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="133"/>
</elements>
</model>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
</elements>
</model>

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="118"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19G73" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17505" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="224"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17505" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="268"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String" syncable="YES"/>
<attribute name="firstName" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="lastName" attributeType="String" syncable="YES"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String" syncable="YES"/>
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="developerName" attributeType="String" syncable="YES"/>
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
<attribute name="iconName" attributeType="String" syncable="YES"/>
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
</elements>
</model>

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.969",
"green" : "0.157",
"red" : "0.545"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.698",
"green" : "0.255",
"red" : "0.925"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "250",
"green" : "5",
"red" : "164"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.349",
"green" : "0.780",
"red" : "0.204"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.584",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.188",
"green" : "0.231",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.800",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@@ -0,0 +1,50 @@
//
// InstalledAppPolicy.swift
// AltStore
//
// Created by Riley Testut on 1/24/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import AltSign
import CoreData
@objc(InstalledAppToInstalledAppMigrationPolicy)
class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy {
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
// Entity must be in manager.destinationContext.
let entity = NSEntityDescription.entity(forEntityName: "Team", in: manager.destinationContext)
let fetchRequest = NSFetchRequest<NSManagedObject>()
fetchRequest.entity = entity
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
let teams = try manager.destinationContext.fetch(fetchRequest)
// Cannot use NSManagedObject subclasses during migration, so fallback to using KVC instead.
dInstance.setValue(teams.first, forKey: #keyPath(InstalledApp.team))
}
@objc(defaultIsActiveForBundleID:team:)
func defaultIsActive(for bundleID: String, team: NSManagedObject?) -> NSNumber {
let isActive: Bool
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion) {
isActive = true
} else if let team = team, let type = team.value(forKey: #keyPath(Team.type)) as? Int16, type != ALTTeamType.free.rawValue {
isActive = true
} else {
// AltStore should always be active, but deactivate all other apps.
isActive = (bundleID == StoreApp.altstoreAppID)
// We can assume there is an active app limit,
// but will confirm next time user authenticates.
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
}
return NSNumber(value: isActive)
}
}

View File

@@ -0,0 +1,107 @@
//
// StoreApp10ToStoreApp11Policy.swift
// AltStoreCore
//
// Created by Riley Testut on 9/13/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
// Can't use NSManagedObject subclasses, so add convenience accessors for KVC.
private extension NSManagedObject {
var storeAppBundleID: String? {
let bundleID = value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String
return bundleID
}
var storeAppSourceID: String? {
let sourceID = value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String
return sourceID
}
var storeAppVersion: String? {
let version = value(forKey: #keyPath(StoreApp._version)) as? String
return version
}
var storeAppVersionDate: Date? {
let versionDate = value(forKey: #keyPath(StoreApp._versionDate)) as? Date
return versionDate
}
var storeAppVersionDescription: String? {
let versionDescription = value(forKey: #keyPath(StoreApp._versionDescription)) as? String
return versionDescription
}
var storeAppSize: NSNumber? {
let size = value(forKey: #keyPath(StoreApp._size)) as? NSNumber
return size
}
var storeAppDownloadURL: URL? {
let downloadURL = value(forKey: #keyPath(StoreApp._downloadURL)) as? URL
return downloadURL
}
func setStoreAppLatestVersion(_ appVersion: NSManagedObject) {
setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion))
let versions = NSOrderedSet(array: [appVersion])
setValue(versions, forKey: #keyPath(StoreApp._versions))
}
class func makeAppVersion(version: String,
date: Date,
localizedDescription: String?,
downloadURL: URL,
size: Int64,
appBundleID: String,
sourceID: String,
in context: NSManagedObjectContext) -> NSManagedObject {
let appVersion = NSEntityDescription.insertNewObject(forEntityName: AppVersion.entity().name!, into: context)
appVersion.setValue(version, forKey: #keyPath(AppVersion.version))
appVersion.setValue(date, forKey: #keyPath(AppVersion.date))
appVersion.setValue(localizedDescription, forKey: #keyPath(AppVersion.localizedDescription))
appVersion.setValue(downloadURL, forKey: #keyPath(AppVersion.downloadURL))
appVersion.setValue(size, forKey: #keyPath(AppVersion.size))
appVersion.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
appVersion.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
return appVersion
}
}
@objc(StoreApp10ToStoreApp11Policy)
class StoreApp10ToStoreApp11Policy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
guard let appBundleID = sInstance.storeAppBundleID,
let sourceID = sInstance.storeAppSourceID,
let version = sInstance.storeAppVersion,
let versionDate = sInstance.storeAppVersionDate,
// let versionDescription = sInstance.storeAppVersionDescription, // Optional
let downloadURL = sInstance.storeAppDownloadURL,
let size = sInstance.storeAppSize as? Int64
else { return }
guard
let destinationStoreApp = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first,
let context = destinationStoreApp.managedObjectContext
else { fatalError("A destination StoreApp and its managedObjectContext must exist.") }
let appVersion = NSManagedObject.makeAppVersion(
version: version,
date: versionDate,
localizedDescription: sInstance.storeAppVersionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: appBundleID,
sourceID: sourceID,
in: context
)
destinationStoreApp.setStoreAppLatestVersion(appVersion)
}
}

View File

@@ -0,0 +1,22 @@
//
// StoreAppPolicy.swift
// AltStore
//
// Created by Riley Testut on 9/14/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
@objc(StoreAppToStoreAppMigrationPolicy)
class StoreAppToStoreAppMigrationPolicy: NSEntityMigrationPolicy {
@objc(migrateIconURL)
func migrateIconURL() -> URL {
URL(string: "https://via.placeholder.com/150")!
}
@objc(migrateScreenshotURLs)
func migrateScreenshotURLs() -> NSCopying {
[] as NSArray
}
}

View File

@@ -0,0 +1,92 @@
//
// ALTAppPermission.swift
// SideStore
//
// Created by Joseph Mattiello on 2/28/23.
// Copyright © 2923 Joseph Mattiello. All rights reserved.
//
import Foundation
@objc
public enum ALTAppPermissionType: Int, CaseIterable {
case photos
case camera
case location
case contacts
case reminders
case appleMusic = 6
case microphone
case speechRecognition
case backgroundAudio
case backgroundFetch
case bluetooth
case network
case calendars
case touchID
case faceID
case siri
case motion
public init?(rawValue: String) {
switch rawValue {
case "photos": self = .photos
case "camera": self = .camera
case "location": self = .location
case "contacts": self = .contacts
case "reminders": self = .reminders
case "appleMusic", "music": self = .appleMusic
case "microphone": self = .microphone
case "speechRecognition", "speech-recognition": self = .speechRecognition
case "backgroundAudio", "background-audio": self = .backgroundAudio
case "backgroundFetch", "background-fetch": self = .backgroundFetch
case "bluetooth": self = .bluetooth
case "network": self = .network
case "calendars": self = .calendars
case "touchID", "touchid": self = .touchID
case "faceID", "faceid": self = .faceID
case "siri": self = .siri
case "motion": self = .motion
default: return nil
}
}
public var stringValue: String {
switch self {
case .photos:
return "photos"
case .camera:
return "camera"
case .location:
return "location"
case .contacts:
return "contacts"
case .reminders:
return "reminders"
case .appleMusic:
return "music"
case .microphone:
return "microphone"
case .speechRecognition:
return "speech-recognition"
case .backgroundAudio:
return "background-audio"
case .backgroundFetch:
return "background-fetch"
case .bluetooth:
return "bluetooth"
case .network:
return "network"
case .calendars:
return "calendars"
case .touchID:
return "touchid"
case .faceID:
return "faceid"
case .siri:
return "siri"
case .motion:
return "motion"
}
}
}

View File

@@ -0,0 +1,14 @@
//
// ALTPatreonBenefitType.swift
// SideStore
//
// Created by Joseph Mattiello on 2/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
import Foundation
public enum ALTPatreonBenefitType: String, RawRepresentable {
case betaAccess = "1186336"
case credits = "1186340"
}

View File

@@ -0,0 +1,28 @@
//
// ALTSourceUserInfoKey.swift
// SideStore
//
// Created by Joseph Mattiello on 02/28/23.
// Copyright © 2023 Joseph Mattiello. All rights reserved.
//
import Foundation
@objc
public enum ALTSourceUserInfoKey: Int, CaseIterable {
case patreonAccessToken
public init?(rawValue: String) {
switch rawValue {
case Self.patreonAccessToken.stringValue: self = .patreonAccessToken
default: return nil
}
}
public var stringValue: String {
switch self {
case .patreonAccessToken:
return "patreonAccessToken"
}
}
}