[ABANDONED] Mastodon iOS client.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
10 KiB

//
// MastodonAPI.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/17/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import Alamofire
import Foundation
typealias JSONObject = [String: Any]
typealias JSONArray = [Any]
typealias JSONObjectArray = [JSONObject]
enum MastodonRequestError: Error {
case noResponse
case unauthenticated
}
enum TimelineCategory: String {
case home, local, federated, tag, favorites
}
enum PaginationDirection: String {
case prev, next
}
public class PaginationItem: NSObject, NSCoding {
let direction: PaginationDirection
let statusID: String
init(direction: PaginationDirection, statusID: String) {
self.direction = direction
self.statusID = statusID
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(direction.rawValue, forKey: "direction")
aCoder.encode(statusID, forKey: "statusID")
}
convenience required public init?(coder aDecoder: NSCoder) {
guard
let direction = PaginationDirection(rawValue: aDecoder.decodeObject(forKey: "direction") as! String),
let statusID = aDecoder.decodeObject(forKey: "statusID") as? String
else {
return nil
}
self.init(direction: direction, statusID: statusID)
}
}
class MastodonAPI {
static var serverURL: URL? {
guard let host = AuthenticationManager.session?.client?.host else {
return nil
}
return URL(string: "https://\(host)")
}
static func parseLinkHeader(_ link: String?) -> [PaginationItem] {
guard let link = link else {
return []
}
let regex = try! NSRegularExpression(pattern: "<.*\\?(?:max_id|since_id)=([0-9]+)>;rel=\"(next|prev)\"", options: .caseInsensitive)
let matches = regex.matches(in: link, options: [], range: NSRange(location: 0, length: link.count))
return matches.map { match in
let statusRange = match.range(at: 0)
let directionRange = match.range(at: 1)
let statusID = link[Range(statusRange, in: link)!]
let direction = link[Range(directionRange, in: link)!]
return PaginationItem(direction: PaginationDirection(rawValue: String(direction))!, statusID: String(statusID))
}
}
private static func request(
token: String,
serverURL: URL,
path: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
pagination: PaginationItem? = nil,
completion: @escaping (Any?, [PaginationItem]?, Error?) -> Void
) {
let requestURL = serverURL.appendingPathComponent(path)
let headers: HTTPHeaders = [
"Authorization": "Bearer \(token)",
"Accept": "application/json",
]
if Config.logRequests {
print("Request: \(requestURL.absoluteString)")
}
var parameters = parameters ?? [:]
if let pagination = pagination {
switch pagination.direction {
case .prev:
parameters["since_id"] = pagination.statusID
case .next:
parameters["max_id"] = pagination.statusID
}
}
Alamofire.request(
requestURL,
method: method,
parameters: parameters,
encoding: URLEncoding.default,
headers: headers
).validate().responseJSON { response in
switch response.result {
case .success(let data):
let pagination = self.parseLinkHeader(response.response?.allHeaderFields["Link"] as? String)
completion(data, pagination, nil)
case .failure(let error):
completion(nil, nil, error)
}
}
}
private static func request(
path: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
pagination: PaginationItem? = nil,
completion: @escaping (Any?, [PaginationItem]?, Error?) -> Void
) {
guard let token = AuthenticationManager.token, let serverURL = self.serverURL else {
completion(nil, nil, MastodonRequestError.unauthenticated)
return
}
request(
token: token,
serverURL: serverURL,
path: path,
method: method,
parameters: parameters,
pagination: pagination,
completion: completion
)
}
static func currentUser(token: String, serverURL: URL, completion: @escaping (JSONObject?, Error?) -> Void) {
self.request(token: token, serverURL: serverURL, path: "api/v1/accounts/verify_credentials") { data, _, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? JSONObject, nil)
}
}
static func account(id: String, completion: @escaping (JSONObject?, Error?) -> Void) {
self.request(path: "api/v1/accounts/\(id)") { data, _, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? JSONObject, nil)
}
}
static func status(id: String, completion: @escaping (JSONObject?, Error?) -> Void) {
self.request(path: "api/v1/statuses/\(id)") { data, _, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? JSONObject, nil)
}
}
static func context(id: String, completion: @escaping ([String: Any]?, Error?) -> Void) {
self.request(path: "api/v1/statuses/\(id)/context") { data, _, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? [String: Any], nil)
}
}
static func homeTimeline(
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = ["limit": limit ?? 20]
self.request(path: "api/v1/timelines/home", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
guard error == nil else {
completion(nil, nil, error)
return
}
completion(data as? JSONObjectArray, pagination, nil)
}
}
static func publicTimeline(
local: Bool,
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = [
"local": local,
"limit": limit ?? 20,
]
self.request(path: "api/v1/timelines/home", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
guard error == nil else {
completion(nil, nil, error)
return
}
completion(data as? JSONObjectArray, pagination, nil)
}
}
static func tagTimeline(
local: Bool,
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = [
"local": local,
"limit": limit ?? 20,
]
self.request(path: "api/v1/timelines/home", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
guard error == nil else {
completion(nil, nil, error)
return
}
completion(data as? JSONObjectArray, pagination, nil)
}
}
static func statuses(
accountID: String,
onlyMedia: Bool,
excludeReplies: Bool,
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = [
"only_media": onlyMedia,
"exclude_replies": excludeReplies,
"limit": limit ?? 20,
]
self.request(path: "api/v1/accounts/\(accountID)/statuses", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
guard error == nil else {
completion(nil, nil, error)
return
}
completion(data as? JSONObjectArray, pagination, nil)
}
}
static func favorites(
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = ["limit": limit ?? 20]
self.request(path: "api/v1/favourites", method: .get, parameters: parameters, pagination: pagination) { data, pagination, error in
guard error == nil else {
completion(nil, nil, error)
return
}
completion(data as? JSONObjectArray, pagination, nil)
}
}
static func registerApp(serverURL: URL, completion: @escaping (JSONObject?, Error?) -> Void) {
let requestURL = serverURL.appendingPathComponent("api/v1/apps")
let parameters: Parameters = [
"client_name": Config.clientDisplayName,
"redirect_uris": "elpha://oauth",
"scopes": "read write follow",
"website": Config.clientWebsite,
]
Alamofire.request(
requestURL,
method: .post,
parameters: parameters,
encoding: URLEncoding(destination: .httpBody)
).validate().responseJSON { response in
switch response.result {
case .success(let data):
completion(data as? JSONObject, nil)
case .failure(let error):
completion(nil, error)
}
}
}
}