import Foundation import UIKit actor APIService { static let shared = APIService() // Change to your server URL private let baseURL = URL(string: "https://api.nightly.app/")! private var authToken: String? { KeychainService.shared.getToken() } // MARK: - Auth func login(username: String, password: String) async throws { let r: AuthResponse = try await post("auth/login", body: [ "username": username, "password": password ]) KeychainService.shared.saveToken(r.token) } func register(username: String, password: String, displayName: String) async throws { let r: AuthResponse = try await post("auth/register", body: [ "username": username, "password": password, "displayName": displayName ]) KeychainService.shared.saveToken(r.token) } func getCurrentUser() async throws -> User { try await get("users/me") } // MARK: - Posts func getFeed() async throws -> [Post] { try await get("posts/feed") } func createPost(content: String, mood: Mood, isAnonymous: Bool) async throws { let _: EmptyResponse = try await post("posts", body: [ "content": content, "mood": mood.rawValue, "isAnonymous": isAnonymous ]) } func resonate(postId: String) async throws { let _: EmptyResponse = try await post("posts/\(postId)/resonate", body: [:]) } func unresoante(postId: String) async throws { let _: EmptyResponse = try await delete("posts/\(postId)/resonate") } func sendWhisper(toUserId: String, content: String) async throws { let _: EmptyResponse = try await post("users/\(toUserId)/whisper", body: ["content": content]) } // MARK: - Users func getUserPosts(userId: String) async throws -> [Post] { try await get("users/\(userId)/posts") } func getUserStreak(userId: String) async throws -> Int { let r: StreakResponse = try await get("users/\(userId)/streak") return r.streak } func registerPushToken(_ token: String) async { _ = try? await post("users/me/push-token", body: ["token": token]) as EmptyResponse } // MARK: - HTTP private func get(_ path: String) async throws -> T { try await perform(makeRequest("GET", path: path)) } @discardableResult private func post(_ path: String, body: [String: Any]) async throws -> T { var req = makeRequest("POST", path: path) req.httpBody = try JSONSerialization.data(withJSONObject: body) return try await perform(req) } @discardableResult private func delete(_ path: String) async throws -> T { try await perform(makeRequest("DELETE", path: path)) } private func makeRequest(_ method: String, path: String) -> URLRequest { var req = URLRequest(url: baseURL.appendingPathComponent(path)) req.httpMethod = method req.setValue("application/json", forHTTPHeaderField: "Content-Type") if let t = authToken { req.setValue("Bearer \(t)", forHTTPHeaderField: "Authorization") } return req } private func perform(_ request: URLRequest) async throws -> T { let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse } guard (200...299).contains(http.statusCode) else { let msg = (try? JSONDecoder().decode(APIErrorBody.self, from: data))?.message ?? HTTPURLResponse.localizedString(forStatusCode: http.statusCode) throw APIError.serverError(msg) } let dec = JSONDecoder() dec.dateDecodingStrategy = .iso8601 return try dec.decode(T.self, from: data) } } private struct AuthResponse: Decodable { let token: String } private struct StreakResponse: Decodable { let streak: Int } private struct APIErrorBody: Decodable { let message: String } struct EmptyResponse: Decodable {} enum APIError: LocalizedError { case invalidResponse case serverError(String) var errorDescription: String? { switch self { case .invalidResponse: return "Ungültige Serverantwort" case .serverError(let m): return m } } }