frenzy-ios/Fervor/Sources/Fervor/FervorClient.swift

178 lines
6.4 KiB
Swift

//
// FervorClient.swift
// Fervor
//
// Created by Shadowfacts on 11/25/21.
//
import Foundation
public actor FervorClient: Sendable {
private let instanceURL: URL
private let session: URLSession
public private(set) var accessToken: String?
public static let decoder: JSONDecoder = {
let d = JSONDecoder()
let withFractionalSeconds = ISO8601DateFormatter()
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let without = ISO8601DateFormatter()
without.formatOptions = [.withInternetDateTime]
// because fucking ISO8601DateFormatter isn't a DateFormatter
d.dateDecodingStrategy = .custom({ decoder in
let s = try decoder.singleValueContainer().decode(String.self)
// try both because Elixir's DateTime.to_iso8601 omits the .0 if the date doesn't have fractional seconds
if let d = withFractionalSeconds.date(from: s) {
return d
} else if let d = without.date(from: s) {
return d
} else {
throw DateDecodingError()
}
})
return d
}()
public init(instanceURL: URL, accessToken: String?, session: URLSession = .shared) {
self.instanceURL = instanceURL
self.accessToken = accessToken
self.session = session
}
private func buildURL(path: String, queryItems: [URLQueryItem] = []) -> URL {
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
components.path = path
components.queryItems = queryItems
return components.url!
}
private func configureRequest(_ request: URLRequest) -> URLRequest {
var request = request
if let accessToken = accessToken,
request.value(forHTTPHeaderField: "Authorization") == nil {
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return request
}
private func performRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
let request = configureRequest(request)
let (data, response) = try await session.data(for: request)
if (response as! HTTPURLResponse).statusCode == 404 {
throw Error.notFound
}
let decoded = try FervorClient.decoder.decode(T.self, from: data)
return decoded
}
public func register(clientName: String, website: URL? = nil, redirectURI: URL) async throws -> ClientRegistration {
var request = URLRequest(url: buildURL(path: "/api/v1/register"))
request.httpMethod = "POST"
request.setURLEncodedBody(params: [
"client_name": clientName,
"website": website?.absoluteString,
"redirect_uri": redirectURI.absoluteString,
])
return try await performRequest(request)
}
public func token(authCode: String, redirectURI: URL, clientID: String, clientSecret: String) async throws -> Token {
var request = URLRequest(url: buildURL(path: "/oauth/token"))
request.httpMethod = "POST"
request.setURLEncodedBody(params: [
"grant_type": "authorization_code",
"authorization_code": authCode,
"redirect_uri": redirectURI.absoluteString,
"client_id": clientID,
"client_secret": clientSecret,
])
let result: Token = try await performRequest(request)
self.accessToken = result.accessToken
return result
}
public func groups() async throws -> [Group] {
let request = URLRequest(url: buildURL(path: "/api/v1/groups"))
return try await performRequest(request)
}
public func feeds() async throws -> [Feed] {
let request = URLRequest(url: buildURL(path: "/api/v1/feeds"))
return try await performRequest(request)
}
public func syncItems(lastSync: Date?) async throws -> ItemsSyncUpdate {
let request = URLRequest(url: buildURL(path: "/api/v1/items/sync", queryItems: [
URLQueryItem(name: "last_sync", value: lastSync?.formatted(.iso8601))
]))
return try await performRequest(request)
}
public func items(feed id: FervorID) async throws -> [Item] {
let request = URLRequest(url: buildURL(path: "/api/v1/feeds/\(id)/items"))
return try await performRequest(request)
}
public func itemsRequest(limit: Int) -> URLRequest {
return configureRequest(URLRequest(url: buildURL(path: "/api/v1/items", queryItems: [
URLQueryItem(name: "limit", value: limit.formatted()),
])))
}
public func item(id: FervorID) async throws -> Item? {
let request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)"))
do {
return try await performRequest(request)
} catch {
if let error = error as? Error, case .notFound = error {
return nil
} else {
throw error
}
}
}
public func read(item id: FervorID) async throws -> Item {
var request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)/read"))
request.httpMethod = "POST"
return try await performRequest(request)
}
public func unread(item id: FervorID) async throws -> Item {
var request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)/unread"))
request.httpMethod = "POST"
return try await performRequest(request)
}
public func read(ids: [FervorID]) async throws -> [FervorID] {
var request = URLRequest(url: buildURL(path: "/api/v1/items/read"))
request.httpMethod = "POST"
request.setURLEncodedBody(params: ["ids": ids.joined(separator: ",")])
return try await performRequest(request)
}
public func unread(ids: [FervorID]) async throws -> [FervorID] {
var request = URLRequest(url: buildURL(path: "/api/v1/items/unread"))
request.httpMethod = "POST"
request.setURLEncodedBody(params: ["ids": ids.joined(separator: ",")])
return try await performRequest(request)
}
public struct Auth {
public let accessToken: String
public let refreshToken: String?
}
public enum Error: Swift.Error {
case urlSession(Swift.Error)
case decode(Swift.Error)
case notFound
}
}
private struct DateDecodingError: Error {
}