import Foundation /// Uploads the social.org file to a Codeberg (Gitea) repository via the Contents API. /// /// Compatible with any Gitea instance. Defaults to `https://codeberg.org`. /// Uses `GET /api/v1/repos/{owner}/{repo}/contents/{path}` for the SHA, /// then `PUT` to create or update the file. public struct CodebergUploader: FeedUploader { public let token: String public let owner: String public let repo: String public let path: String public let branch: String public let instance: String private let session: URLSession public init( token: String, owner: String, repo: String, path: String = "social.org", branch: String = "main", instance: String = "https://codeberg.org", session: URLSession = .shared ) { self.token = token self.owner = owner self.repo = repo self.path = path self.branch = branch self.instance = instance.trimmingCharacters(in: CharacterSet(charactersIn: "/")) self.session = session } public func upload(content: String) async throws { let sha = try? await fetchCurrentSHA() let encoded = Data(content.utf8).base64EncodedString() var body: [String: Any] = [ "message": "Update social.org via iOS", "content": encoded, "branch": branch ] if let sha { body["sha"] = sha } let url = try contentsURL() var request = URLRequest(url: url) request.httpMethod = sha != nil ? "PUT" : "POST" request.setValue("token \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: body) let (_, response) = try await perform(request) if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) { throw UploadError.uploadFailed(statusCode: http.statusCode) } } private func fetchCurrentSHA() async throws -> String { let url = try contentsURL() var request = URLRequest(url: url) request.setValue("token \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await perform(request) if let http = response as? HTTPURLResponse, http.statusCode == 404 { throw UploadError.fileNotFound } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let sha = json["sha"] as? String else { throw UploadError.fileNotFound } return sha } private func contentsURL() throws -> URL { let urlString = "\(instance)/api/v1/repos/\(owner)/\(repo)/contents/\(path)" guard let url = URL(string: urlString) else { throw UploadError.invalidConfiguration("Invalid Codeberg API URL: \(urlString)") } return url } private func perform(_ request: URLRequest) async throws -> (Data, URLResponse) { do { return try await session.data(for: request) } catch { throw UploadError.networkError(underlying: error.localizedDescription) } } /// Derives the public raw URL for the file (usable as `publicFeedURL`). public var derivedPublicURL: String { "\(instance)/\(owner)/\(repo)/raw/branch/\(branch)/\(path)" } }