Merge branch 'feature/authentication-api' into develop
This commit is contained in:
commit
dc6a0c8c33
|
@ -150,6 +150,8 @@
|
||||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
||||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
||||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||||
|
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||||
|
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
||||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -254,6 +256,8 @@
|
||||||
DB427DC925BAA00100D1B89D = {
|
DB427DC925BAA00100D1B89D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
|
||||||
|
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
|
||||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
||||||
DB427DD425BAA00100D1B89D /* Mastodon */,
|
DB427DD425BAA00100D1B89D /* Mastodon */,
|
||||||
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
||||||
|
|
|
@ -7,12 +7,35 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>8</integer>
|
<integer>6</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>9</integer>
|
<integer>5</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
<dict>
|
||||||
|
<key>DB427DD125BAA00100D1B89D</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>DB427DE725BAA00100D1B89D</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>DB427DF225BAA00100D1B89D</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>DB89B9F525C10FD0008580ED</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -64,7 +64,11 @@ internal extension ColorAsset.Color {
|
||||||
// swiftlint:disable convenience_type
|
// swiftlint:disable convenience_type
|
||||||
private final class BundleToken {
|
private final class BundleToken {
|
||||||
static let bundle: Bundle = {
|
static let bundle: Bundle = {
|
||||||
Bundle(for: BundleToken.self)
|
#if SWIFT_PACKAGE
|
||||||
|
return Bundle.module
|
||||||
|
#else
|
||||||
|
return Bundle(for: BundleToken.self)
|
||||||
|
#endif
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
// swiftlint:enable convenience_type
|
// swiftlint:enable convenience_type
|
||||||
|
|
|
@ -25,6 +25,12 @@ extension L10n {
|
||||||
|
|
||||||
// swiftlint:disable convenience_type
|
// swiftlint:disable convenience_type
|
||||||
private final class BundleToken {
|
private final class BundleToken {
|
||||||
static let bundle = Bundle(for: BundleToken.self)
|
static let bundle: Bundle = {
|
||||||
|
#if SWIFT_PACKAGE
|
||||||
|
return Bundle.module
|
||||||
|
#else
|
||||||
|
return Bundle(for: BundleToken.self)
|
||||||
|
#endif
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
// swiftlint:enable convenience_type
|
// swiftlint:enable convenience_type
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"id" : "63D68260-F74D-4CA6-ADBC-B1263DD6BE55",
|
||||||
|
"name" : "Configuration 1",
|
||||||
|
"options" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultOptions" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"testTargets" : [
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:Mastodon.xcodeproj",
|
||||||
|
"identifier" : "DB427DE725BAA00100D1B89D",
|
||||||
|
"name" : "MastodonTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:Mastodon.xcodeproj",
|
||||||
|
"identifier" : "DB427DF225BAA00100D1B89D",
|
||||||
|
"name" : "MastodonUITests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 1
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"id" : "5119353D-C795-4264-89FD-8376D9B144F8",
|
||||||
|
"name" : "Configuration 1",
|
||||||
|
"options" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultOptions" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"testTargets" : [
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:MastodonSDK",
|
||||||
|
"identifier" : "MastodonSDKTests",
|
||||||
|
"name" : "MastodonSDKTests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 1
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Error+MastodonAPIError.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.API.Error {
|
||||||
|
public enum MastodonAPIError: Swift.Error {
|
||||||
|
case generic(errorResponse: Mastodon.Response.ErrorResponse)
|
||||||
|
|
||||||
|
init(errorResponse: Mastodon.Response.ErrorResponse) {
|
||||||
|
self = .generic(errorResponse: errorResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Error.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import enum NIOHTTP1.HTTPResponseStatus
|
||||||
|
|
||||||
|
extension Mastodon.API {
|
||||||
|
public struct Error: Swift.Error {
|
||||||
|
|
||||||
|
public var httpResponseStatus: HTTPResponseStatus
|
||||||
|
public var mastodonAPIError: MastodonAPIError?
|
||||||
|
|
||||||
|
init(
|
||||||
|
httpResponseStatus: HTTPResponseStatus,
|
||||||
|
mastodonAPIError: Mastodon.API.Error.MastodonAPIError?
|
||||||
|
) {
|
||||||
|
self.httpResponseStatus = httpResponseStatus
|
||||||
|
self.mastodonAPIError = mastodonAPIError
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
httpResponseStatus: HTTPResponseStatus,
|
||||||
|
errorResponse: Mastodon.Response.ErrorResponse
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
httpResponseStatus: httpResponseStatus,
|
||||||
|
mastodonAPIError: MastodonAPIError(errorResponse: errorResponse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,20 +5,38 @@
|
||||||
// Created by xiaojian sun on 2021/1/25.
|
// Created by xiaojian sun on 2021/1/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
public extension Mastodon.API.App {
|
extension Mastodon.API.App {
|
||||||
|
|
||||||
static func appEndpointURL(domain: String) -> URL {
|
static func appEndpointURL(domain: String) -> URL {
|
||||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps")
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func create(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
query: CreateQuery
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.API.App.Application>, Error> {
|
||||||
|
let request = Mastodon.API.request(
|
||||||
|
url: appEndpointURL(domain: domain),
|
||||||
|
query: query,
|
||||||
|
authorization: nil
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Application.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API.App {
|
extension Mastodon.API.App {
|
||||||
|
|
||||||
struct Application: Codable {
|
public struct Application: Codable {
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
|
|
||||||
|
@ -41,32 +59,33 @@ extension Mastodon.API.App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateAnAppQuery {
|
public struct CreateQuery: Codable, PostQuery {
|
||||||
public let clientName: String
|
public let clientName: String
|
||||||
public let redirectURIs: String
|
public let redirectURIs: String
|
||||||
public let scopes: String?
|
public let scopes: String?
|
||||||
public let website: String?
|
public let website: String?
|
||||||
|
|
||||||
public init(clientName: String, redirectURIs: String, scopes: String?, website: String?) {
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case clientName = "client_name"
|
||||||
|
case redirectURIs = "redirect_uris"
|
||||||
|
case scopes
|
||||||
|
case website
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
clientName: String,
|
||||||
|
redirectURIs: String = "urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
scopes: String? = "read write follow push",
|
||||||
|
website: String?
|
||||||
|
) {
|
||||||
self.clientName = clientName
|
self.clientName = clientName
|
||||||
self.redirectURIs = redirectURIs
|
self.redirectURIs = redirectURIs
|
||||||
self.scopes = scopes
|
self.scopes = scopes
|
||||||
self.website = website
|
self.website = website
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryItems: [URLQueryItem]? {
|
var body: Data? {
|
||||||
var items: [URLQueryItem] = []
|
return try? Mastodon.API.encoder.encode(self)
|
||||||
items.append(URLQueryItem(name: "client_name", value: clientName))
|
|
||||||
items.append(URLQueryItem(name: "redirect_uris", value: redirectURIs))
|
|
||||||
scopes.flatMap {
|
|
||||||
items.append(URLQueryItem(name: "scopes", value: $0))
|
|
||||||
}
|
|
||||||
website.flatMap {
|
|
||||||
items.append(URLQueryItem(name: "website", value: $0))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !items.isEmpty else { return nil }
|
|
||||||
return items
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
//
|
|
||||||
// Mastodon+API+Error.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021/1/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Mastodon.API.Error {
|
|
||||||
|
|
||||||
struct ErrorResponse: Codable {
|
|
||||||
let error: String
|
|
||||||
let errorDescription: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case error
|
|
||||||
case errorDescription = "error_description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+OAuth.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.API.OAuth {
|
||||||
|
|
||||||
|
public static let authorizationField = "Authorization"
|
||||||
|
|
||||||
|
public struct Authorization {
|
||||||
|
public let accessToken: String
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Timeline.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.Timeline {
|
||||||
|
|
||||||
|
static func publicTimelineEndpointURL(domain: String) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/public")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func `public`(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
query: PublicTimelineQuery
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||||
|
let request = Mastodon.API.request(
|
||||||
|
url: publicTimelineEndpointURL(domain: domain),
|
||||||
|
query: query,
|
||||||
|
authorization: nil
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Timeline {
|
||||||
|
public struct PublicTimelineQuery: Codable, GetQuery {
|
||||||
|
|
||||||
|
public let local: Bool?
|
||||||
|
public let remote: Bool?
|
||||||
|
public let onlyMedia: Bool?
|
||||||
|
public let maxID: Mastodon.Entity.Toot.ID?
|
||||||
|
public let sinceID: Mastodon.Entity.Toot.ID?
|
||||||
|
public let minID: Mastodon.Entity.Toot.ID?
|
||||||
|
public let limit: Int?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
local: Bool? = nil,
|
||||||
|
remote: Bool? = nil,
|
||||||
|
onlyMedia: Bool? = nil,
|
||||||
|
maxID: Mastodon.Entity.Toot.ID? = nil,
|
||||||
|
sinceID: Mastodon.Entity.Toot.ID? = nil,
|
||||||
|
minID: Mastodon.Entity.Toot.ID? = nil,
|
||||||
|
limit: Int? = nil
|
||||||
|
) {
|
||||||
|
self.local = local
|
||||||
|
self.remote = remote
|
||||||
|
self.onlyMedia = onlyMedia
|
||||||
|
self.maxID = maxID
|
||||||
|
self.sinceID = sinceID
|
||||||
|
self.minID = minID
|
||||||
|
self.limit = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem]? {
|
||||||
|
var items: [URLQueryItem] = []
|
||||||
|
local.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "local", value: $0.queryItemValue))
|
||||||
|
}
|
||||||
|
remote.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "remote", value: $0.queryItemValue))
|
||||||
|
}
|
||||||
|
onlyMedia.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue))
|
||||||
|
}
|
||||||
|
maxID.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "max_id", value: $0))
|
||||||
|
}
|
||||||
|
sinceID.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "since_id", value: $0))
|
||||||
|
}
|
||||||
|
minID.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "min_id", value: $0))
|
||||||
|
}
|
||||||
|
limit.flatMap {
|
||||||
|
items.append(URLQueryItem(name: "limit", value: String($0)))
|
||||||
|
}
|
||||||
|
guard !items.isEmpty else { return nil }
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Bool {
|
||||||
|
var queryItemValue: String {
|
||||||
|
return self ? "true" : "false"
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,25 +6,118 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import NIOHTTP1
|
import enum NIOHTTP1.HTTPResponseStatus
|
||||||
|
|
||||||
public extension Mastodon.API {
|
extension Mastodon.API {
|
||||||
|
|
||||||
|
static let timeoutInterval: TimeInterval = 10
|
||||||
|
static let httpHeaderDateFormatter = ISO8601DateFormatter()
|
||||||
|
static let encoder: JSONEncoder = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.dateEncodingStrategy = .iso8601
|
||||||
|
return encoder
|
||||||
|
}()
|
||||||
|
static let decoder: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom { decoder throws -> Date in
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let string = try container.decode(String.self)
|
||||||
|
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions.insert(.withFractionalSeconds)
|
||||||
|
if let date = formatter.date(from: string) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoder
|
||||||
|
}()
|
||||||
|
|
||||||
static func endpointURL(domain: String) -> URL {
|
static func endpointURL(domain: String) -> URL {
|
||||||
return URL(string: "https://" + domain + "/api/v1/")!
|
return URL(string: "https://" + domain + "/api/v1/")!
|
||||||
}
|
}
|
||||||
|
|
||||||
static let timeoutInterval: TimeInterval = 10
|
}
|
||||||
static let decoder: JSONDecoder = {
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
|
||||||
|
|
||||||
return decoder
|
|
||||||
}()
|
|
||||||
|
|
||||||
static let httpHeaderDateFormatter = ISO8601DateFormatter()
|
extension Mastodon.API {
|
||||||
|
public enum App { }
|
||||||
|
public enum OAuth { }
|
||||||
|
public enum Timeline { }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API {
|
||||||
|
|
||||||
enum Error { }
|
static func request(
|
||||||
enum App { }
|
url: URL,
|
||||||
|
query: GetQuery,
|
||||||
|
authorization: OAuth.Authorization?
|
||||||
|
) -> URLRequest {
|
||||||
|
var components = URLComponents(string: url.absoluteString)!
|
||||||
|
components.queryItems = query.queryItems
|
||||||
|
|
||||||
|
let requestURL = components.url!
|
||||||
|
var request = URLRequest(
|
||||||
|
url: requestURL,
|
||||||
|
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
|
||||||
|
timeoutInterval: Mastodon.API.timeoutInterval
|
||||||
|
)
|
||||||
|
if let authorization = authorization {
|
||||||
|
request.setValue(
|
||||||
|
"Bearer \(authorization.accessToken)",
|
||||||
|
forHTTPHeaderField: Mastodon.API.OAuth.authorizationField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
static func request(
|
||||||
|
url: URL,
|
||||||
|
query: PostQuery,
|
||||||
|
authorization: OAuth.Authorization?
|
||||||
|
) -> URLRequest {
|
||||||
|
let components = URLComponents(string: url.absoluteString)!
|
||||||
|
let requestURL = components.url!
|
||||||
|
var request = URLRequest(
|
||||||
|
url: requestURL,
|
||||||
|
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
|
||||||
|
timeoutInterval: Mastodon.API.timeoutInterval
|
||||||
|
)
|
||||||
|
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.httpBody = query.body
|
||||||
|
if let authorization = authorization {
|
||||||
|
request.setValue(
|
||||||
|
"Bearer \(authorization.accessToken)",
|
||||||
|
forHTTPHeaderField: Mastodon.API.OAuth.authorizationField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode<T>(type: T.Type, from data: Data, response: URLResponse) throws -> T where T : Decodable {
|
||||||
|
// decode data then decode error if could
|
||||||
|
do {
|
||||||
|
return try Mastodon.API.decoder.decode(type, from: data)
|
||||||
|
} catch let decodeError {
|
||||||
|
#if DEBUG
|
||||||
|
debugPrint(decodeError)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
guard let httpURLResponse = response as? HTTPURLResponse else {
|
||||||
|
assertionFailure()
|
||||||
|
throw decodeError
|
||||||
|
}
|
||||||
|
|
||||||
|
let httpResponseStatus = HTTPResponseStatus(statusCode: httpURLResponse.statusCode)
|
||||||
|
if let errorResponse = try? Mastodon.API.decoder.decode(Mastodon.Response.ErrorResponse.self, from: data) {
|
||||||
|
throw Mastodon.API.Error(httpResponseStatus: httpResponseStatus, errorResponse: errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Mastodon.API.Error(httpResponseStatus: httpResponseStatus, mastodonAPIError: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// Mastodon+Entity+Toot.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Entity {
|
||||||
|
public struct Toot: Codable {
|
||||||
|
|
||||||
|
public typealias ID = String
|
||||||
|
|
||||||
|
public let id: ID
|
||||||
|
|
||||||
|
public let createdAt: Date
|
||||||
|
public let content: String
|
||||||
|
public let account: User
|
||||||
|
|
||||||
|
public let language: String
|
||||||
|
public let visibility: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case createdAt = "created_at"
|
||||||
|
case content
|
||||||
|
case account
|
||||||
|
case language
|
||||||
|
case visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// Mastodon+Entity+User.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Entity {
|
||||||
|
public struct User: Codable {
|
||||||
|
|
||||||
|
public typealias ID = String
|
||||||
|
|
||||||
|
public let id: ID
|
||||||
|
|
||||||
|
public let username: String
|
||||||
|
public let acct: String
|
||||||
|
public let displayName: String?
|
||||||
|
public let avatar: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case username
|
||||||
|
case acct
|
||||||
|
case displayName = "display_name"
|
||||||
|
case avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// Query.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol GetQuery {
|
||||||
|
var queryItems: [URLQueryItem]? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol PostQuery {
|
||||||
|
var body: Data? { get }
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// Mastodon+Response+Content.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Response {
|
||||||
|
public struct Content<T> {
|
||||||
|
|
||||||
|
// entity
|
||||||
|
public let value: T
|
||||||
|
|
||||||
|
// standard fields
|
||||||
|
public let date: Date?
|
||||||
|
|
||||||
|
// application fields
|
||||||
|
public let rateLimit: RateLimit?
|
||||||
|
public let responseTime: Int?
|
||||||
|
|
||||||
|
public var networkDate: Date {
|
||||||
|
return date ?? Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(value: T, response: URLResponse) {
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
self.date = {
|
||||||
|
guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "date") else { return nil }
|
||||||
|
return Mastodon.API.httpHeaderDateFormatter.date(from: string)
|
||||||
|
}()
|
||||||
|
|
||||||
|
self.rateLimit = RateLimit(response: response)
|
||||||
|
self.responseTime = {
|
||||||
|
guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "x-response-time") else { return nil }
|
||||||
|
return Int(string)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.Response.Content {
|
||||||
|
public struct RateLimit {
|
||||||
|
|
||||||
|
public let limit: Int
|
||||||
|
public let remaining: Int
|
||||||
|
public let reset: Date
|
||||||
|
|
||||||
|
public init(limit: Int, remaining: Int, reset: Date) {
|
||||||
|
self.limit = limit
|
||||||
|
self.remaining = remaining
|
||||||
|
self.reset = reset
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(response: URLResponse) {
|
||||||
|
guard let response = response as? HTTPURLResponse else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let limitString = response.value(forHTTPHeaderField: "X-RateLimit-Limit"),
|
||||||
|
let limit = Int(limitString),
|
||||||
|
let remainingString = response.value(forHTTPHeaderField: "X-RateLimit-Remaining"),
|
||||||
|
let remaining = Int(remainingString) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let resetTimestampString = response.value(forHTTPHeaderField: "X-RateLimit-Reset"),
|
||||||
|
let reset = Mastodon.API.httpHeaderDateFormatter.date(from: resetTimestampString) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(limit: limit, remaining: remaining, reset: reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// Mastodon+Response+ErrorResponse.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/1/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Response {
|
||||||
|
public struct ErrorResponse: Codable {
|
||||||
|
public let error: String
|
||||||
|
public let errorDescription: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case error
|
||||||
|
case errorDescription = "error_description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
import XCTest
|
|
||||||
|
|
||||||
import MastodonSDKTests
|
|
||||||
|
|
||||||
var tests = [XCTestCaseEntry]()
|
|
||||||
tests += MastodonSDKTests.allTests()
|
|
||||||
XCTMain(tests)
|
|
|
@ -1,14 +1,62 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
|
import Combine
|
||||||
@testable import MastodonSDK
|
@testable import MastodonSDK
|
||||||
|
|
||||||
final class MastodonSDKTests: XCTestCase {
|
final class MastodonSDKTests: XCTestCase {
|
||||||
func testExample() {
|
|
||||||
// This is an example of a functional test case.
|
var disposeBag = Set<AnyCancellable>()
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
|
||||||
// results.
|
let domain = "mstdn.jp"
|
||||||
}
|
let session = URLSession(configuration: .ephemeral)
|
||||||
|
|
||||||
|
func testCreateAnAnpplication() throws {
|
||||||
|
let theExpectation = expectation(description: "Create An Application")
|
||||||
|
|
||||||
|
let query = Mastodon.API.App.CreateQuery(
|
||||||
|
clientName: "XCTest",
|
||||||
|
website: nil
|
||||||
|
)
|
||||||
|
Mastodon.API.App.create(session: session, domain: domain, query: query)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail(error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
XCTAssertEqual(response.value.name, "XCTest")
|
||||||
|
XCTAssertEqual(response.value.website, nil)
|
||||||
|
XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob")
|
||||||
|
theExpectation.fulfill()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
static var allTests = [
|
wait(for: [theExpectation], timeout: 10.0)
|
||||||
("testExample", testExample),
|
}
|
||||||
]
|
|
||||||
|
func testPublicTimeline() throws {
|
||||||
|
let theExpectation = expectation(description: "Create An Application")
|
||||||
|
|
||||||
|
let query = Mastodon.API.Timeline.PublicTimelineQuery()
|
||||||
|
Mastodon.API.Timeline.public(session: session, domain: domain, query: query)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail(error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
XCTAssert(!response.value.isEmpty)
|
||||||
|
theExpectation.fulfill()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
wait(for: [theExpectation], timeout: 10.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import XCTest
|
|
||||||
|
|
||||||
#if !canImport(ObjectiveC)
|
|
||||||
public func allTests() -> [XCTestCaseEntry] {
|
|
||||||
return [
|
|
||||||
testCase(MastodonSDKTests.allTests),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
#endif
|
|
Loading…
Reference in New Issue