2020-03-15 08:01:40 +00:00
|
|
|
import Foundation
|
|
|
|
import Combine
|
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
public protocol UpdaterProtocol: ObservableObject {
|
2020-03-15 08:01:40 +00:00
|
|
|
|
|
|
|
var update: Release? { get }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
public class Updater: ObservableObject, UpdaterProtocol {
|
2020-03-15 08:01:40 +00:00
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
@Published public var update: Release?
|
2020-03-15 08:01:40 +00:00
|
|
|
|
2020-09-22 06:12:50 +00:00
|
|
|
public init(checkOnLaunch: Bool) {
|
|
|
|
if checkOnLaunch {
|
|
|
|
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
|
|
|
|
checkForUpdates()
|
|
|
|
}
|
2020-03-15 08:01:40 +00:00
|
|
|
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
|
|
|
|
self.checkForUpdates()
|
|
|
|
}
|
|
|
|
timer.tolerance = 60*60
|
|
|
|
}
|
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
public func checkForUpdates() {
|
2020-03-15 08:01:40 +00:00
|
|
|
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
|
|
|
|
guard let data = data else { return }
|
|
|
|
guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
|
|
|
|
self.evaluate(release: release)
|
|
|
|
}.resume()
|
|
|
|
}
|
|
|
|
|
2020-03-22 22:26:29 +00:00
|
|
|
public func ignore(release: Release) {
|
|
|
|
guard !release.critical else { return }
|
|
|
|
defaults.set(true, forKey: release.name)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.update = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Updater {
|
|
|
|
|
2020-03-15 08:01:40 +00:00
|
|
|
func evaluate(release: Release) {
|
2020-03-22 22:26:29 +00:00
|
|
|
guard !userIgnored(release: release) else { return }
|
2020-09-22 06:12:50 +00:00
|
|
|
guard !release.prerelease else { return }
|
2020-09-22 07:17:22 +00:00
|
|
|
let latestVersion = SemVer(release.name)
|
|
|
|
let currentVersion = SemVer(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
|
|
|
|
if latestVersion > currentVersion {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.update = release
|
2020-03-15 08:01:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-22 22:26:29 +00:00
|
|
|
func userIgnored(release: Release) -> Bool {
|
|
|
|
guard !release.critical else { return false }
|
|
|
|
return defaults.bool(forKey: release.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaults: UserDefaults {
|
|
|
|
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
|
|
|
|
}
|
2020-03-15 08:01:40 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 07:17:22 +00:00
|
|
|
struct SemVer {
|
|
|
|
|
|
|
|
let versionNumbers: [Int]
|
|
|
|
|
|
|
|
init(_ version: String) {
|
|
|
|
// Betas have the format 1.2.3_beta1
|
|
|
|
let strippedBeta = version.split(separator: "_").first!
|
|
|
|
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
|
|
|
|
while split.count < 3 {
|
|
|
|
split.append(0)
|
|
|
|
}
|
|
|
|
versionNumbers = split
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension SemVer: Comparable {
|
|
|
|
|
|
|
|
static func < (lhs: SemVer, rhs: SemVer) -> Bool {
|
|
|
|
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
|
|
|
|
if latest < current {
|
|
|
|
return true
|
|
|
|
} else if latest > current {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-03-15 08:01:40 +00:00
|
|
|
extension Updater {
|
|
|
|
|
|
|
|
enum Constants {
|
|
|
|
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
public struct Release: Codable {
|
|
|
|
|
|
|
|
public let name: String
|
2020-09-22 06:12:50 +00:00
|
|
|
public let prerelease: Bool
|
2020-03-22 01:43:26 +00:00
|
|
|
public let html_url: URL
|
|
|
|
public let body: String
|
|
|
|
|
2020-09-22 06:12:50 +00:00
|
|
|
public init(name: String, prerelease: Bool, html_url: URL, body: String) {
|
2020-03-22 01:43:26 +00:00
|
|
|
self.name = name
|
2020-09-22 06:12:50 +00:00
|
|
|
self.prerelease = prerelease
|
2020-03-22 01:43:26 +00:00
|
|
|
self.html_url = html_url
|
|
|
|
self.body = body
|
|
|
|
}
|
|
|
|
|
2020-03-15 08:01:40 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 06:12:50 +00:00
|
|
|
extension Release: Identifiable {
|
|
|
|
|
|
|
|
public var id: String {
|
|
|
|
html_url.absoluteString
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-03-15 08:01:40 +00:00
|
|
|
|
|
|
|
extension Release {
|
|
|
|
|
2020-03-22 01:43:26 +00:00
|
|
|
public var critical: Bool {
|
2020-09-22 06:12:50 +00:00
|
|
|
body.contains(Constants.securityContent)
|
2020-03-15 08:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Release {
|
|
|
|
|
|
|
|
enum Constants {
|
|
|
|
static let securityContent = "Critical Security Update"
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|