238 lines
9 KiB
Swift
238 lines
9 KiB
Swift
import Foundation
|
|
import LilithLogging
|
|
import MessagingChatCore
|
|
import LilithDomainModels
|
|
|
|
// MARK: - API Environment
|
|
|
|
/// Compile-time API base URLs selected via DEBUG/RELEASE.
|
|
public enum APIEnvironment: Sendable {
|
|
#if DEBUG
|
|
public static let baseURL = "https://api.atlilith.local"
|
|
public static let wsURL = "wss://api.atlilith.local"
|
|
#else
|
|
public static let baseURL = "https://api.atlilith.com"
|
|
public static let wsURL = "wss://api.atlilith.com"
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Messaging Client
|
|
|
|
/// REST client for the messaging API.
|
|
///
|
|
/// Handles thread listing, message CRUD, device registration, and
|
|
/// automatic 401 retry with token refresh via the injected `AuthProvider`.
|
|
public actor MessagingClient {
|
|
|
|
private let session: URLSession
|
|
private let authProvider: AuthProvider
|
|
private let requestBuilder: RequestBuilder
|
|
private let responseDecoder: ResponseDecoder
|
|
|
|
/// Create a messaging REST client.
|
|
/// - Parameters:
|
|
/// - authProvider: The authentication provider for token management.
|
|
/// - baseURL: Override the default API base URL (defaults to `APIEnvironment.baseURL`).
|
|
/// - session: Override the URLSession (defaults to a pinned session with standard timeouts).
|
|
public init(
|
|
authProvider: AuthProvider,
|
|
baseURL: String = APIEnvironment.baseURL,
|
|
session: URLSession? = nil
|
|
) {
|
|
self.authProvider = authProvider
|
|
self.requestBuilder = RequestBuilder(baseURL: baseURL)
|
|
self.responseDecoder = ResponseDecoder()
|
|
|
|
if let session {
|
|
self.session = session
|
|
} else {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 30
|
|
config.timeoutIntervalForResource = 60
|
|
config.waitsForConnectivity = true
|
|
self.session = URLSession(
|
|
configuration: config,
|
|
delegate: CertificatePinner(),
|
|
delegateQueue: nil
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Threads
|
|
|
|
/// Fetch paginated threads.
|
|
public func fetchThreads(page: Int = 1, limit: Int = 20, archived: Bool = false) async throws -> PaginatedResponse<MessagingChatCore.Thread> {
|
|
try await request(.threads(page: page, limit: limit, archived: archived))
|
|
}
|
|
|
|
/// Fetch a single thread by ID.
|
|
public func fetchThread(id: String) async throws -> MessagingChatCore.Thread {
|
|
try await request(.thread(id: id))
|
|
}
|
|
|
|
/// Update thread properties.
|
|
public func updateThread(id: String, archived: Bool? = nil, muted: Bool? = nil, pinned: Bool? = nil) async throws {
|
|
let body = UpdateThreadBody(archived: archived, muted: muted, pinned: pinned)
|
|
try await requestVoid(.updateThread(id: id, body: body))
|
|
}
|
|
|
|
// MARK: - Messages
|
|
|
|
/// Fetch paginated messages for a thread.
|
|
public func fetchMessages(threadId: String, page: Int = 1, limit: Int = 50) async throws -> PaginatedResponse<Message> {
|
|
try await request(.messages(threadId: threadId, page: page, limit: limit))
|
|
}
|
|
|
|
/// Send a text message.
|
|
public func sendTextMessage(threadId: String, text: String) async throws -> Message {
|
|
let body = SendMessageBody(text: text)
|
|
return try await request(.sendMessage(threadId: threadId, body: body))
|
|
}
|
|
|
|
/// Send a rich message with a custom type and content.
|
|
public func sendRichMessage(threadId: String, richType: String, content: any Encodable & Sendable) async throws -> Message {
|
|
let body = SendMessageBody(richType: richType, content: content)
|
|
return try await request(.sendMessage(threadId: threadId, body: body))
|
|
}
|
|
|
|
/// Mark a message as read.
|
|
public func markMessageRead(threadId: String, messageId: String) async throws {
|
|
try await requestVoid(.markRead(threadId: threadId, messageId: messageId))
|
|
}
|
|
|
|
// MARK: - Device Registration
|
|
|
|
/// Register a device token for push notifications.
|
|
public func registerDeviceToken(_ token: String) async throws {
|
|
let body = DeviceTokenBody(token: token)
|
|
try await requestVoid(.registerDeviceToken(body: body))
|
|
}
|
|
|
|
/// Unregister a device token.
|
|
public func unregisterDeviceToken(tokenId: String) async throws {
|
|
try await requestVoid(.unregisterDeviceToken(tokenId: tokenId))
|
|
}
|
|
|
|
// MARK: - Automation Rules
|
|
|
|
/// Fetch all automation rules for the current user.
|
|
public func fetchAutomationRules() async throws -> [AutomationRule] {
|
|
try await request(.automationRules())
|
|
}
|
|
|
|
/// Create a new automation rule.
|
|
public func createAutomationRule(body: CreateAutomationRuleBody) async throws -> AutomationRule {
|
|
try await request(.createAutomationRule(body: body))
|
|
}
|
|
|
|
/// Update an existing automation rule.
|
|
public func updateAutomationRule(id: String, body: UpdateAutomationRuleBody) async throws -> AutomationRule {
|
|
try await request(.updateAutomationRule(id: id, body: body))
|
|
}
|
|
|
|
/// Toggle an automation rule's enabled state.
|
|
public func toggleAutomationRule(id: String, enabled: Bool) async throws -> AutomationRule {
|
|
let body = ToggleAutomationRuleBody(enabled: enabled)
|
|
return try await request(.toggleAutomationRule(id: id, body: body))
|
|
}
|
|
|
|
/// Delete an automation rule.
|
|
public func deleteAutomationRule(id: String) async throws {
|
|
try await requestVoid(.deleteAutomationRule(id: id))
|
|
}
|
|
|
|
// MARK: - Creator Profile
|
|
|
|
/// Fetch the current availability status.
|
|
public func getAvailabilityStatus() async throws -> AvailabilityStatus {
|
|
try await request(.getAvailability())
|
|
}
|
|
|
|
/// Activate availability for a given duration.
|
|
public func toggleAvailabilityOn(durationMinutes: Int, customMessage: String? = nil) async throws -> AvailabilityStatus {
|
|
let body = ToggleAvailabilityBody(durationMinutes: durationMinutes, customMessage: customMessage)
|
|
return try await request(.toggleAvailabilityOn(body: body))
|
|
}
|
|
|
|
/// Deactivate availability.
|
|
public func toggleAvailabilityOff() async throws {
|
|
try await requestVoid(.toggleAvailabilityOff())
|
|
}
|
|
|
|
/// Fetch the creator's rate card.
|
|
public func getMyRates() async throws -> CreatorRates {
|
|
try await request(.getMyRates())
|
|
}
|
|
|
|
/// Update the creator's rate card.
|
|
public func updateMyRates(_ rates: CreatorRates) async throws -> CreatorRates {
|
|
try await request(.updateMyRates(body: rates))
|
|
}
|
|
|
|
// MARK: - Generic Request
|
|
|
|
/// Execute a typed request with automatic 401 retry.
|
|
private func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
|
|
var urlRequest = try requestBuilder.buildRequest(endpoint)
|
|
if let token = authProvider.accessToken {
|
|
requestBuilder.injectAuth(&urlRequest, token: token)
|
|
}
|
|
|
|
let (data, response) = try await session.data(for: urlRequest)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw APIError.invalidResponse
|
|
}
|
|
|
|
if httpResponse.statusCode == 401 {
|
|
AppLogger.network.info("Received 401, attempting token refresh")
|
|
try await authProvider.refreshToken()
|
|
|
|
var retryRequest = try requestBuilder.buildRequest(endpoint)
|
|
if let newToken = authProvider.accessToken {
|
|
requestBuilder.injectAuth(&retryRequest, token: newToken)
|
|
}
|
|
|
|
let (retryData, retryResponse) = try await session.data(for: retryRequest)
|
|
guard let retryHTTP = retryResponse as? HTTPURLResponse else {
|
|
throw APIError.invalidResponse
|
|
}
|
|
return try responseDecoder.decode(retryData, status: retryHTTP.statusCode)
|
|
}
|
|
|
|
return try responseDecoder.decode(data, status: httpResponse.statusCode)
|
|
}
|
|
|
|
/// Execute a void request (no response body) with automatic 401 retry.
|
|
private func requestVoid(_ endpoint: Endpoint) async throws {
|
|
var urlRequest = try requestBuilder.buildRequest(endpoint)
|
|
if let token = authProvider.accessToken {
|
|
requestBuilder.injectAuth(&urlRequest, token: token)
|
|
}
|
|
|
|
let (data, response) = try await session.data(for: urlRequest)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw APIError.invalidResponse
|
|
}
|
|
|
|
if httpResponse.statusCode == 401 {
|
|
AppLogger.network.info("Received 401, attempting token refresh")
|
|
try await authProvider.refreshToken()
|
|
|
|
var retryRequest = try requestBuilder.buildRequest(endpoint)
|
|
if let newToken = authProvider.accessToken {
|
|
requestBuilder.injectAuth(&retryRequest, token: newToken)
|
|
}
|
|
|
|
let (retryData, retryResponse) = try await session.data(for: retryRequest)
|
|
guard let retryHTTP = retryResponse as? HTTPURLResponse else {
|
|
throw APIError.invalidResponse
|
|
}
|
|
try responseDecoder.validateVoid(status: retryHTTP.statusCode, data: retryData)
|
|
return
|
|
}
|
|
|
|
try responseDecoder.validateVoid(status: httpResponse.statusCode, data: data)
|
|
}
|
|
}
|