Deduplicate shared constants, MIME helper, and cache normalized URLs
- Extract OAuth UserDefaults keys into shared OAuthKeys enum - Extract triplicated mimeTypeFor() into shared mimeTypeForImage() - Cache normalized base URL at client init instead of per-request
This commit is contained in:
parent
79f24ea1a4
commit
733334df9b
8 changed files with 54 additions and 84 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue