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.
432 lines
14 KiB
432 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 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)
|
|
}
|
|
}
|
|
}
|
|
}
|