diff --git a/qStatus/App/AppDelegate.swift b/qStatus/App/AppDelegate.swift index 7c6a9c2..69770f6 100644 --- a/qStatus/App/AppDelegate.swift +++ b/qStatus/App/AppDelegate.swift @@ -4,9 +4,6 @@ import SwiftUI @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { - private static let pendingMastodonAccountKey = "qstatus.pending-mastodon-account" - private static let pendingMastodonErrorKey = "qstatus.pending-mastodon-account-error" - let appState = AppState() let accountStore = AccountStore() var floatingPanel: FloatingPanel? @@ -203,7 +200,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { guard let code = components.queryItems?.first(where: { $0.name == "code" })?.value else { return } Task { - guard let accountData = UserDefaults.standard.data(forKey: Self.pendingMastodonAccountKey), + guard let accountData = UserDefaults.standard.data(forKey: OAuthKeys.pendingMastodonAccount), var account = try? JSONDecoder().decode(Account.self, from: accountData), let clientID = account.mastodonClientID, let clientSecret = account.mastodonClientSecret @@ -226,14 +223,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { account.displayName = "@\(username)@\(account.instanceURL.replacingOccurrences(of: "https://", with: ""))" try accountStore.addAccount(account, token: token) - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonAccountKey) - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonErrorKey) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonAccount) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonError) } catch { UserDefaults.standard.set( error.localizedDescription, - forKey: Self.pendingMastodonErrorKey + forKey: OAuthKeys.pendingMastodonError ) - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonAccountKey) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonAccount) } } } diff --git a/qStatus/Models/PostingService.swift b/qStatus/Models/PostingService.swift index ecff779..7e6551f 100644 --- a/qStatus/Models/PostingService.swift +++ b/qStatus/Models/PostingService.swift @@ -10,3 +10,18 @@ struct PostingError: LocalizedError { let message: String var errorDescription: String? { message } } + +enum OAuthKeys { + static let pendingMastodonAccount = "qstatus.pending-mastodon-account" + static let pendingMastodonError = "qstatus.pending-mastodon-account-error" +} + +func mimeTypeForImage(_ filename: String) -> String { + let ext = (filename as NSString).pathExtension.lowercased() + switch ext { + case "png": return "image/png" + case "gif": return "image/gif" + case "webp": return "image/webp" + default: return "image/jpeg" + } +} diff --git a/qStatus/Services/MastodonClient.swift b/qStatus/Services/MastodonClient.swift index c0fa645..df8e5e6 100644 --- a/qStatus/Services/MastodonClient.swift +++ b/qStatus/Services/MastodonClient.swift @@ -3,27 +3,25 @@ import Foundation struct MastodonClient: PostingService { let account: Account private let token: String - private let rawBaseURL: String + private let baseURL: String - init(account: Account, token: String) { + init(account: Account, token: String) throws { self.account = account self.token = token - self.rawBaseURL = account.instanceURL + self.baseURL = try URLNormalizer.normalizeBaseURL( + account.instanceURL, fieldName: "Mastodon instance URL" + ) } func uploadMedia(imageData: Data, filename: String, altText: String?) async throws -> String { - let url = try URLNormalizer.endpointURL( - baseURL: rawBaseURL, - path: "/api/v2/media", - fieldName: "Mastodon instance URL" - ) + let url = try URLNormalizer.endpointURL(base: baseURL, path: "/api/v2/media") var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 30 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") var form = MultipartFormData() - form.addFile(name: "file", filename: filename, mimeType: mimeTypeFor(filename), data: imageData) + form.addFile(name: "file", filename: filename, mimeType: mimeTypeForImage(filename), data: imageData) if let altText, !altText.isEmpty { form.addField(name: "description", value: altText) } @@ -52,11 +50,7 @@ struct MastodonClient: PostingService { } func createPost(text: String, mediaIDs: [String]) async throws -> URL { - let url = try URLNormalizer.endpointURL( - baseURL: rawBaseURL, - path: "/api/v1/statuses", - fieldName: "Mastodon instance URL" - ) + let url = try URLNormalizer.endpointURL(base: baseURL, path: "/api/v1/statuses") var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 30 @@ -226,14 +220,4 @@ struct MastodonClient: PostingService { return username } - - private func mimeTypeFor(_ filename: String) -> String { - let ext = (filename as NSString).pathExtension.lowercased() - switch ext { - case "png": return "image/png" - case "gif": return "image/gif" - case "webp": return "image/webp" - default: return "image/jpeg" - } - } } diff --git a/qStatus/Services/MicroblogClient.swift b/qStatus/Services/MicroblogClient.swift index cc4e154..431748f 100644 --- a/qStatus/Services/MicroblogClient.swift +++ b/qStatus/Services/MicroblogClient.swift @@ -22,7 +22,7 @@ struct MicroblogClient: PostingService { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") var form = MultipartFormData() - form.addFile(name: "file", filename: filename, mimeType: mimeTypeFor(filename), data: imageData) + form.addFile(name: "file", filename: filename, mimeType: mimeTypeForImage(filename), data: imageData) request.setValue(form.contentType, forHTTPHeaderField: "Content-Type") request.httpBody = form.finalized @@ -139,14 +139,4 @@ struct MicroblogClient: PostingService { return "Micro.blog" } - - private func mimeTypeFor(_ filename: String) -> String { - let ext = (filename as NSString).pathExtension.lowercased() - switch ext { - case "png": return "image/png" - case "gif": return "image/gif" - case "webp": return "image/webp" - default: return "image/jpeg" - } - } } diff --git a/qStatus/Services/PostingManager.swift b/qStatus/Services/PostingManager.swift index 4cf59ef..c251419 100644 --- a/qStatus/Services/PostingManager.swift +++ b/qStatus/Services/PostingManager.swift @@ -35,7 +35,7 @@ struct PostingManager { switch account.serviceType { case .mastodon: - let client = MastodonClient(account: account, token: token) + let client = try MastodonClient(account: account, token: token) var mediaIDs: [String] = [] for img in imageData { let id = try await client.uploadMedia(imageData: img.data, filename: img.filename, altText: nil) @@ -44,7 +44,7 @@ struct PostingManager { return try await client.createPost(text: text, mediaIDs: mediaIDs) case .wordpress: - let client = WordPressClient(account: account, token: token) + let client = try WordPressClient(account: account, token: token) var mediaIDs: [String] = [] for img in imageData { let id = try await client.uploadMedia(imageData: img.data, filename: img.filename, altText: nil) diff --git a/qStatus/Services/WordPressClient.swift b/qStatus/Services/WordPressClient.swift index eb7c482..551fb81 100644 --- a/qStatus/Services/WordPressClient.swift +++ b/qStatus/Services/WordPressClient.swift @@ -3,12 +3,14 @@ import Foundation struct WordPressClient: PostingService { let account: Account private let token: String - private let rawBaseURL: String + private let baseURL: String - init(account: Account, token: String) { + init(account: Account, token: String) throws { self.account = account self.token = token // format: "username:application_password" - self.rawBaseURL = account.instanceURL + self.baseURL = try URLNormalizer.normalizeBaseURL( + account.instanceURL, fieldName: "WordPress site URL" + ) } private var authHeader: String { @@ -16,16 +18,12 @@ struct WordPressClient: PostingService { } func uploadMedia(imageData: Data, filename: String, altText: String?) async throws -> String { - let url = try URLNormalizer.endpointURL( - baseURL: rawBaseURL, - path: "/wp-json/wp/v2/media", - fieldName: "WordPress site URL" - ) + let url = try URLNormalizer.endpointURL(base: baseURL, path: "/wp-json/wp/v2/media") var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 30 request.setValue(authHeader, forHTTPHeaderField: "Authorization") - request.setValue(mimeTypeFor(filename), forHTTPHeaderField: "Content-Type") + request.setValue(mimeTypeForImage(filename), forHTTPHeaderField: "Content-Type") request.setValue("attachment; filename=\"\(filename)\"", forHTTPHeaderField: "Content-Disposition") request.httpBody = imageData @@ -59,11 +57,7 @@ struct WordPressClient: PostingService { } private func updateMediaAltText(mediaID: Int, altText: String) async throws { - let url = try URLNormalizer.endpointURL( - baseURL: rawBaseURL, - path: "/wp-json/wp/v2/media/\(mediaID)", - fieldName: "WordPress site URL" - ) + let url = try URLNormalizer.endpointURL(base: baseURL, path: "/wp-json/wp/v2/media/\(mediaID)") var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 30 @@ -83,11 +77,7 @@ struct WordPressClient: PostingService { func createPost(text: String, mediaIDs: [String]) async throws -> URL { // mediaIDs are source_url strings for WordPress - let url = try URLNormalizer.endpointURL( - baseURL: rawBaseURL, - path: "/wp-json/wp/v2/posts", - fieldName: "WordPress site URL" - ) + let url = try URLNormalizer.endpointURL(base: baseURL, path: "/wp-json/wp/v2/posts") var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 30 @@ -204,14 +194,4 @@ struct WordPressClient: PostingService { throw PostingError(message: "Unexpected response (\(statusCode)): \(body)") } } - - private func mimeTypeFor(_ filename: String) -> String { - let ext = (filename as NSString).pathExtension.lowercased() - switch ext { - case "png": return "image/png" - case "gif": return "image/gif" - case "webp": return "image/webp" - default: return "image/jpeg" - } - } } diff --git a/qStatus/Utilities/URLNormalizer.swift b/qStatus/Utilities/URLNormalizer.swift index 418cda9..87de03f 100644 --- a/qStatus/Utilities/URLNormalizer.swift +++ b/qStatus/Utilities/URLNormalizer.swift @@ -39,4 +39,11 @@ enum URLNormalizer { } return url } + + static func endpointURL(base normalizedBase: String, path: String) throws -> URL { + guard let url = URL(string: normalizedBase + path) else { + throw PostingError(message: "Failed to build API URL.") + } + return url + } } diff --git a/qStatus/Views/SettingsView.swift b/qStatus/Views/SettingsView.swift index 4a2c46b..723deec 100644 --- a/qStatus/Views/SettingsView.swift +++ b/qStatus/Views/SettingsView.swift @@ -73,9 +73,6 @@ struct SettingsView: View { } struct AddAccountView: View { - private static let pendingMastodonAccountKey = "qstatus.pending-mastodon-account" - private static let pendingMastodonErrorKey = "qstatus.pending-mastodon-account-error" - let accountStore: AccountStore @Binding var isPresented: Bool @State private var selectedService: ServiceType = .mastodon @@ -133,8 +130,8 @@ struct AddAccountView: View { Button(waitingForOAuth ? "Cancel Auth" : "Cancel") { if waitingForOAuth { waitingForOAuth = false - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonAccountKey) - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonErrorKey) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonAccount) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonError) } isPresented = false } @@ -271,8 +268,8 @@ struct AddAccountView: View { account.mastodonClientSecret = clientSecret let accountData = try JSONEncoder().encode(account) - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonErrorKey) - UserDefaults.standard.set(accountData, forKey: Self.pendingMastodonAccountKey) + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonError) + UserDefaults.standard.set(accountData, forKey: OAuthKeys.pendingMastodonAccount) let authURL = try MastodonClient.authorizeURL(instanceURL: url, clientID: clientID) NSWorkspace.shared.open(authURL) @@ -282,11 +279,11 @@ struct AddAccountView: View { // Poll for the account to be added by the URL callback handler for _ in 0..<120 { // wait up to 2 minutes try? await Task.sleep(for: .seconds(1)) - if let oauthError = UserDefaults.standard.string(forKey: Self.pendingMastodonErrorKey) { - UserDefaults.standard.removeObject(forKey: Self.pendingMastodonErrorKey) + if let oauthError = UserDefaults.standard.string(forKey: OAuthKeys.pendingMastodonError) { + UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonError) throw PostingError(message: oauthError) } - if UserDefaults.standard.data(forKey: Self.pendingMastodonAccountKey) == nil { + if UserDefaults.standard.data(forKey: OAuthKeys.pendingMastodonAccount) == nil { // Callback was handled, account was added waitingForOAuth = false isPresented = false