fchaty/FchatiApp/Sources/FchatiApp/Networking/RelayAPI.swift
2026-07-26 17:36:46 +02:00

165 lines
4.9 KiB
Swift

import Foundation
actor RelayAPI {
static let shared = RelayAPI()
func createPairing(displayName: String) async throws -> PairingCreateResponse {
var request = URLRequest(url: endpoint(path: "pairing/create"))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(PairingRequest(displayName: displayName))
return try await executeJSON(request)
}
func joinPairing(code: String, displayName: String) async throws -> PairingJoinResponse {
var request = URLRequest(url: endpoint(path: "pairing/join"))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(
PairingJoinRequest(code: code, displayName: displayName)
)
return try await executeJSON(request)
}
func uploadFile(url: URL, token: String) async throws -> FileUploadResponse {
let boundary = "Boundary-\(UUID().uuidString)"
let fileData: Data
do {
fileData = try Data(contentsOf: url)
} catch {
throw RelayAPIError.networkError(error)
}
var request = URLRequest(url: endpoint(path: "files"))
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue(
"multipart/form-data; boundary=\(boundary)",
forHTTPHeaderField: "Content-Type"
)
request.httpBody = multipartBody(
fileData: fileData,
fileName: safeFileName(from: url),
boundary: boundary
)
return try await executeJSON(request)
}
func downloadFile(id: String, token: String) async throws -> Data {
var request = URLRequest(url: endpoint(path: "files/\(id)"))
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return try await execute(request)
}
private let baseURL = URL(string: "https://fchati.diyaa.de")!
private let session = URLSession.shared
private init() {}
private func endpoint(path: String) -> URL {
baseURL.appendingPathComponent(path)
}
private func executeJSON<Response: Decodable>(_ request: URLRequest) async throws -> Response {
let data = try await execute(request)
do {
return try JSONDecoder().decode(Response.self, from: data)
} catch {
throw RelayAPIError.decodingError(error)
}
}
private func execute(_ request: URLRequest) async throws -> Data {
do {
let (data, response) = try await session.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw RelayAPIError.networkError(URLError(.badServerResponse))
}
guard (200 ... 299).contains(response.statusCode) else {
throw RelayAPIError.serverError(response.statusCode)
}
return data
} catch let error as RelayAPIError {
throw error
} catch {
throw RelayAPIError.networkError(error)
}
}
private func multipartBody(fileData: Data, fileName: String, boundary: String) -> Data {
var body = Data()
let escapedFileName = fileName
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
append("--\(boundary)\r\n", to: &body)
append(
"Content-Disposition: form-data; name=\"file\"; filename=\"\(escapedFileName)\"\r\n",
to: &body
)
append("Content-Type: application/octet-stream\r\n\r\n", to: &body)
body.append(fileData)
append("\r\n--\(boundary)--\r\n", to: &body)
return body
}
private func safeFileName(from url: URL) -> String {
let name = url.lastPathComponent
return name.isEmpty ? "upload" : name
}
private func append(_ string: String, to data: inout Data) {
data.append(Data(string.utf8))
}
}
struct PairingCreateResponse: Codable, Equatable {
let code: String
let token: String
let peerID: String
let expiresAt: String
}
struct PairingJoinResponse: Codable, Equatable {
let token: String
let peerID: String
let peer: PeerInfo
}
struct PeerInfo: Codable, Equatable {
let id: String
let displayName: String
}
struct FileUploadResponse: Codable, Equatable {
let id: String
let name: String
let size: Int
}
enum RelayAPIError: Error {
case networkError(Error)
case serverError(Int)
case decodingError(Error)
}
private struct PairingRequest: Encodable {
let displayName: String
}
private struct PairingJoinRequest: Encodable {
let code: String
let displayName: String
}