Dwayne Harris
6 years ago
16 changed files with 962 additions and 597 deletions
-
110elpha-ios.xcodeproj/project.pbxproj
-
160elpha-ios/AccountTableViewController.swift
-
83elpha-ios/AuthenticateViewController.swift
-
27elpha-ios/AuthenticationManager.swift
-
1elpha-ios/Base.lproj/Main.storyboard
-
2elpha-ios/Configuration.swift
-
56elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents
-
9elpha-ios/InstanceRequest.swift
-
47elpha-ios/InstancesDataManager.swift
-
2elpha-ios/MainTabBarController.swift
-
319elpha-ios/MastodonAPI.swift
-
421elpha-ios/MastodonDataManager.swift
-
85elpha-ios/StatusTableViewController.swift
-
8elpha-ios/StatusView.swift
-
227elpha-ios/TimelineTableViewController.swift
-
2elpha-ios/TimelinesTableViewController.swift
@ -0,0 +1,9 @@ |
|||
// |
|||
// InstanceRequest.swift |
|||
// elpha-ios |
|||
// |
|||
// Created by Dwayne Harris on 10/17/18. |
|||
// Copyright © 2018 Elpha. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
@ -0,0 +1,319 @@ |
|||
// |
|||
// 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) |
|||
} |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue