[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.

447 lines
14 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, unauthenticated
}
enum TimelineCategory: String {
case home, local, federated, tag, favorites
}
enum PaginationDirection: String {
case prev, next
}
enum AttachmentType: String {
case unknown, image, gifv, video
}
enum StatusVisibility: String, CaseIterable {
case `public`, unlisted, `private`, direct
}
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: "<[\\S]+(?:max_id|since_id|min_id)=([0-9]+)[\\S]*>; 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: 1)
let directionRange = match.range(at: 2)
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["min_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/public", 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(
tag: String,
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/tag/\(tag)", 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 pinnedStatuses(
accountID: String,
limit: Int?,
completion: @escaping (JSONObjectArray?, Error?) -> Void
) {
let parameters: Parameters = [
"only_media": false,
"exclude_replies": false,
"pinned": true,
"limit": limit ?? 20,
]
self.request(path: "api/v1/accounts/\(accountID)/statuses", method: .get, parameters: parameters) { data, pagination, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? JSONObjectArray, 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 reblog(statusID: String, completion: @escaping (Error?) -> Void) {
self.request(path: "api/v1/statuses/\(statusID)/reblog", method: .post) { _, _, error in
guard error == nil else {
completion(error)
return
}
completion(nil)
}
}
static func unreblog(statusID: String, completion: @escaping (Error?) -> Void) {
self.request(path: "api/v1/statuses/\(statusID)/unreblog", method: .post) { _, _, error in
guard error == nil else {
completion(error)
return
}
completion(nil)
}
}
static func favorite(statusID: String, completion: @escaping (Error?) -> Void) {
self.request(path: "api/v1/statuses/\(statusID)/favourite", method: .post) { _, _, error in
guard error == nil else {
completion(error)
return
}
completion(nil)
}
}
static func unfavorite(statusID: String, completion: @escaping (Error?) -> Void) {
self.request(path: "api/v1/statuses/\(statusID)/unfavourite", method: .post) { _, _, error in
guard error == nil else {
completion(error)
return
}
completion(nil)
}
}
static func postStatus(content: String, replyToID: String?, spoilerText: String?, visibility: StatusVisibility = .public, completion: @escaping (Error?) -> Void) {
var parameters: Parameters = [
"status": content,
"visibility": visibility.rawValue,
]
if let replyToID = replyToID {
parameters["in_reply_to_id"] = replyToID
}
if let spoilerText = spoilerText {
parameters["spoiler_text"] = spoilerText
}
self.request(path: "api/v1/statuses", method: .post, parameters: parameters) { _, _, error in
guard error == nil else {
completion(error)
return
}
completion(nil)
}
}
static func search(content: String, completion: @escaping (JSONObject?, Error?) -> Void) {
let parameters: Parameters = ["q": content]
self.request(path: "api/v2/search", method: .get, parameters: parameters) { data, _, error in
guard error == nil else {
completion(nil, error)
return
}
completion(data as? JSONObject, nil)
}
}
static func notifications(
limit: Int?,
pagination: PaginationItem?,
completion: @escaping (JSONObjectArray?, [PaginationItem]?, Error?) -> Void
) {
let parameters: Parameters = ["limit": limit ?? 20]
self.request(path: "api/v1/notifications", 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": Config.clientRedirectURI,
"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)
}
}
}
}