// // Constants.swift // Passepartout // // Created by Davide De Rosa on 8/26/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import Foundation import PassepartoutKit import SwiftUI public struct Constants: Decodable, Sendable { private static var bundleConfiguration: BundleConfiguration? { .failableMain } public struct Identifiers: Decodable, Sendable { public var appId: String { bundleConfiguration?.string(for: .appId) ?? "1234567890" } public var appStoreId: String { bundleConfiguration?.string(for: .appStoreId) ?? "11223344" } public var groupId: String { bundleConfiguration?.string(for: .groupId) ?? "fake-group-id" } public var iapBundlePrefix: String { bundleConfiguration?.string(for: .iapBundlePrefix) ?? "fake-iap-prefix" } public var displayName: String { bundleConfiguration?.displayName ?? "DisplayName" } public var versionString: String { bundleConfiguration?.versionString ?? "1.0.0 (1000)" } } public struct Websites: Decodable, Sendable { public let home: URL public var faq: URL { home.appendingPathComponent("faq/") } public var disclaimer: URL { home.appendingPathComponent("disclaimer/") } public var privacyPolicy: URL { home.appendingPathComponent("privacy/") } public var donate: URL { home.appendingPathComponent("donate/") } public let subreddit: URL public let githubSponsors: URL } public struct Emails: Decodable, Sendable { private struct Recipients: Decodable, Sendable { let issues: String let beta: String } public let domain: String private let recipients: Recipients public var issues: String { email(to: recipients.issues) } public var beta: String { email(to: recipients.beta) } private func email(to: String) -> String { [to, domain].joined(separator: "@") } } public struct Formats: Decodable, Sendable { public let timestamp: String } public struct Connection: Decodable, Sendable { public let refreshInterval: TimeInterval } public struct Log: Decodable, Sendable { public struct Formatter: Decodable, Sendable { enum CodingKeys: CodingKey { case timestamp case message } private let timestampFormatter: DateFormatter private let message: String public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let timestampFormat = try container.decode(String.self, forKey: .timestamp) timestampFormatter = DateFormatter() timestampFormatter.dateFormat = timestampFormat message = try container.decode(String.self, forKey: .message) } public func formattedLine(_ line: DebugLog.Line) -> String { let formattedTimestamp = timestampFormatter.string(from: line.timestamp) return String(format: message, formattedTimestamp, line.message) } } public let formatter: Formatter public let appPath: String public let tunnelPath: String public let sinceLast: TimeInterval public let maxLevel: DebugLog.Level public let maxNumberOfLines: Int public let maxAge: TimeInterval? } public let identifiers: Identifiers public let websites: Websites public let emails: Emails public let formats: Formats public let connection: Connection public let log: Log } extension Constants { public var urlForReview: URL { URL(string: "https://apps.apple.com/app/id\(identifiers.appStoreId)?action=write-review")! } public var urlForAppLog: URL { cachesURL.appending(path: log.appPath) } public var urlForTunnelLog: URL { cachesURL.appending(path: log.tunnelPath) } private var cachesURL: URL { guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifiers.groupId) else { fatalError("Unable to access App Group container") } return url.appending(components: "Library", "Caches") } }