166 lines
6.0 KiB
Swift
166 lines
6.0 KiB
Swift
//
|
|
// FervorClient.swift
|
|
// Fervor
|
|
//
|
|
// Created by Shadowfacts on 11/25/21.
|
|
//
|
|
|
|
@preconcurrency import Foundation
|
|
|
|
public actor FervorClient: Sendable {
|
|
|
|
private let instanceURL: URL
|
|
private let session: URLSession
|
|
public private(set) var accessToken: String?
|
|
|
|
private 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 performRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
|
|
var request = request
|
|
if let accessToken = accessToken {
|
|
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
}
|
|
let (data, response) = try await session.data(for: request, delegate: nil)
|
|
if (response as! HTTPURLResponse).statusCode == 404 {
|
|
throw Error.notFound
|
|
}
|
|
let decoded = try 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 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 {
|
|
|
|
}
|