Tusker/Pachyderm/Client.swift

326 lines
13 KiB
Swift
Raw Permalink Normal View History

2018-09-11 14:52:21 +00:00
//
// Client.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
/**
The base Mastodon API client.
*/
public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
let baseURL: URL
let session: URLSession
public var accessToken: String?
public var appID: String?
public var clientID: String?
public var clientSecret: String?
public var timeoutInterval: TimeInterval = 60
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
decoder.dateDecodingStrategy = .formatted(formatter)
2018-09-11 14:52:21 +00:00
return decoder
}()
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
self.baseURL = baseURL
self.accessToken = accessToken
self.session = session
}
public func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) {
2018-09-11 14:52:21 +00:00
guard let request = createURLRequest(request: request) else {
completion(.failure(Error.invalidRequest))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Error.invalidResponse))
return
}
guard response.statusCode == 200 else {
let mastodonError = try? self.decoder.decode(MastodonError.self, from: data)
let error = mastodonError.flatMap { Error.mastodonError($0.description) } ?? Error.unknownError
completion(.failure(error))
return
}
guard let result = try? self.decoder.decode(Result.self, from: data) else {
completion(.failure(Error.invalidModel))
return
}
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
completion(.success(result, pagination))
}
task.resume()
}
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
2018-09-11 14:52:21 +00:00
components.path = request.path
components.queryItems = request.queryParameters.queryItems
guard let url = components.url else { return nil }
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data
urlRequest.setValue(request.body.mimeType, forHTTPHeaderField: "Content-Type")
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - Authorization
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: .parameters([
"client_name" => name,
"redirect_uris" => redirectURI,
"scopes" => scopes.scopeString,
"website" => website?.absoluteString
]))
run(request) { result in
defer { completion(result) }
guard case let .success(application, _) = result else { return }
self.appID = application.id
self.clientID = application.clientID
self.clientSecret = application.clientSecret
}
}
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
"client_id" => clientID,
"client_secret" => clientSecret,
"grant_type" => "authorization_code",
"code" => authorizationCode,
"redirect_uri" => redirectURI
]))
run(request) { result in
defer { completion(result) }
guard case let .success(loginSettings, _) = result else { return }
self.accessToken = loginSettings.accessToken
}
}
// MARK: - Self
public static func getSelfAccount() -> Request<Account> {
2018-09-18 00:58:05 +00:00
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
2018-09-11 14:52:21 +00:00
}
public static func getFavourites() -> Request<[Status]> {
2018-09-18 00:58:05 +00:00
return Request<[Status]>(method: .get, path: "/api/v1/favourites")
2018-09-11 14:52:21 +00:00
}
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
2018-09-24 12:49:39 +00:00
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
2018-09-11 14:52:21 +00:00
}
public static func getInstance() -> Request<Instance> {
2018-09-18 00:58:05 +00:00
return Request<Instance>(method: .get, path: "/api/v1/instance")
2018-09-11 14:52:21 +00:00
}
public static func getCustomEmoji() -> Request<[Emoji]> {
2018-09-18 00:58:05 +00:00
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
2018-09-11 14:52:21 +00:00
}
// MARK: - Accounts
public static func getAccount(id: String) -> Request<Account> {
2018-09-18 00:58:05 +00:00
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
2018-09-11 14:52:21 +00:00
}
public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
2018-09-18 00:58:05 +00:00
return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
2018-09-11 14:52:21 +00:00
"q" => query,
"limit" => limit,
"following" => following
])
}
// MARK: - Blocks
public static func getBlocks() -> Request<[Account]> {
2018-09-18 00:58:05 +00:00
return Request<[Account]>(method: .get, path: "/api/v1/blocks")
2018-09-11 14:52:21 +00:00
}
public static func getDomainBlocks() -> Request<[String]> {
2018-09-18 00:58:05 +00:00
return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
2018-09-11 14:52:21 +00:00
}
public static func block(domain: String) -> Request<Empty> {
2018-09-18 00:58:05 +00:00
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
2018-09-11 14:52:21 +00:00
"domain" => domain
]))
}
public static func unblock(domain: String) -> Request<Empty> {
2018-09-18 00:58:05 +00:00
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
2018-09-11 14:52:21 +00:00
"domain" => domain
]))
}
// MARK: - Filters
public static func getFilters() -> Request<[Filter]> {
2018-09-18 00:58:05 +00:00
return Request<[Filter]>(method: .get, path: "/api/v1/filters")
2018-09-11 14:52:21 +00:00
}
public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
2018-09-18 00:58:05 +00:00
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
2018-09-11 14:52:21 +00:00
"phrase" => phrase,
"irreversible" => irreversible,
"whole_word" => wholeWord,
"expires_at" => expiresAt
] + "context" => context.contextStrings))
}
public static func getFilter(id: String) -> Request<Filter> {
2018-09-18 00:58:05 +00:00
return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
2018-09-11 14:52:21 +00:00
}
// MARK: - Follows
public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
2018-09-11 14:52:21 +00:00
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range
2018-09-18 00:58:05 +00:00
return request
2018-09-11 14:52:21 +00:00
}
public static func getFollowSuggestions() -> Request<[Account]> {
2018-09-18 00:58:05 +00:00
return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
2018-09-11 14:52:21 +00:00
}
public static func followRemote(acct: String) -> Request<Account> {
2018-09-18 00:58:05 +00:00
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
2018-09-11 14:52:21 +00:00
}
// MARK: - Lists
public static func getLists() -> Request<[List]> {
2018-09-18 00:58:05 +00:00
return Request<[List]>(method: .get, path: "/api/v1/lists")
2018-09-11 14:52:21 +00:00
}
public static func getList(id: String) -> Request<List> {
2018-09-18 00:58:05 +00:00
return Request<List>(method: .get, path: "/api/v1/lists/\(id)")
2018-09-11 14:52:21 +00:00
}
public static func createList(title: String) -> Request<List> {
2018-09-18 00:58:05 +00:00
return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
2018-09-11 14:52:21 +00:00
}
// MARK: - Media
public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
2018-09-18 00:58:05 +00:00
return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
2018-09-11 14:52:21 +00:00
"description" => description,
"focus" => focus
], attachment))
}
// MARK: - Mutes
public static func getMutes(range: RequestRange) -> Request<[Account]> {
2018-09-11 14:52:21 +00:00
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range
2018-09-18 00:58:05 +00:00
return request
2018-09-11 14:52:21 +00:00
}
// MARK: - Notifications
public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"exclude_types" => excludeTypes.map { $0.rawValue }
)
2018-09-11 14:52:21 +00:00
request.range = range
2018-09-18 00:58:05 +00:00
return request
2018-09-11 14:52:21 +00:00
}
public static func clearNotifications() -> Request<Empty> {
2018-09-18 00:58:05 +00:00
return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
2018-09-11 14:52:21 +00:00
}
// MARK: - Reports
public static func getReports() -> Request<[Report]> {
2018-09-18 00:58:05 +00:00
return Request<[Report]>(method: .get, path: "/api/v1/reports")
2018-09-11 14:52:21 +00:00
}
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
2018-09-18 00:58:05 +00:00
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
2018-09-11 14:52:21 +00:00
"account_id" => account.id,
"comment" => comment
] + "status_ids" => statuses.map { $0.id }))
}
// MARK: - Search
public static func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
2018-09-18 00:58:05 +00:00
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
2018-09-11 14:52:21 +00:00
"q" => query,
2019-09-15 00:47:08 +00:00
"resolve" => resolve,
"limit" => limit
2018-09-11 14:52:21 +00:00
])
}
// MARK: - Statuses
public static func getStatus(id: String) -> Request<Status> {
2018-09-18 00:58:05 +00:00
return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
2018-09-11 14:52:21 +00:00
}
public static func createStatus(text: String,
contentType: StatusContentType = .plain,
inReplyTo: String? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visibility: Status.Visibility? = nil,
language: String? = nil) -> Request<Status> {
2018-09-18 00:58:05 +00:00
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
2018-09-11 14:52:21 +00:00
"status" => text,
2019-01-14 00:46:51 +00:00
"content_type" => contentType.mimeType,
2018-09-18 01:57:46 +00:00
"in_reply_to_id" => inReplyTo,
2018-09-11 14:52:21 +00:00
"sensitive" => sensitive,
"spoiler_text" => spoilerText,
2018-09-24 01:10:45 +00:00
"visibility" => visibility?.rawValue,
2018-09-11 14:52:21 +00:00
"language" => language
2018-09-11 17:45:48 +00:00
] + "media_ids" => media?.map { $0.id }))
2018-09-11 14:52:21 +00:00
}
// MARK: - Timelines
public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
2018-09-18 00:58:05 +00:00
return timeline.request(range: range)
2018-09-11 14:52:21 +00:00
}
// MARK: Bookmarks
public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
request.range = range
return request
}
2018-09-11 14:52:21 +00:00
}
extension Client {
public enum Error: Swift.Error {
case unknownError
case invalidRequest
case invalidResponse
case invalidModel
case mastodonError(String)
}
}