Put Mastodon API code in separate module from entities

This commit is contained in:
Justin Mazzocchi 2020-09-03 18:55:46 -07:00
parent f6f065e143
commit f921d154b3
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
53 changed files with 170 additions and 101 deletions

View file

@ -38,11 +38,11 @@ struct StoredStatus: Codable, Hashable {
extension StoredStatus: FetchableRecord, PersistableRecord { extension StoredStatus: FetchableRecord, PersistableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder { static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder() MastodonDecoder()
} }
static func databaseJSONEncoder(for column: String) -> JSONEncoder { static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder() MastodonEncoder()
} }
} }

View file

@ -6,10 +6,10 @@ import Mastodon
extension Account: FetchableRecord, PersistableRecord { extension Account: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder { public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder() MastodonDecoder()
} }
public static func databaseJSONEncoder(for column: String) -> JSONEncoder { public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder() MastodonEncoder()
} }
} }

View file

@ -6,10 +6,10 @@ import Mastodon
extension Filter: FetchableRecord, PersistableRecord { extension Filter: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder { public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder() MastodonDecoder()
} }
public static func databaseJSONEncoder(for column: String) -> JSONEncoder { public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder() MastodonEncoder()
} }
} }

View file

@ -11,24 +11,13 @@ let package = Package(
products: [ products: [
.library( .library(
name: "Mastodon", name: "Mastodon",
targets: ["Mastodon"]), targets: ["Mastodon"])
.library(
name: "MastodonStubs",
targets: ["MastodonStubs"])
],
dependencies: [
.package(path: "HTTP")
], ],
dependencies: [],
targets: [ targets: [
.target( .target(name: "Mastodon"),
name: "Mastodon",
dependencies: ["HTTP"]),
.target(
name: "MastodonStubs",
dependencies: ["Mastodon", .product(name: "Stubbing", package: "HTTP")],
resources: [.process("Resources")]),
.testTarget( .testTarget(
name: "MastodonTests", name: "MastodonTests",
dependencies: ["MastodonStubs"]) dependencies: ["Mastodon"])
] ]
) )

View file

@ -2,7 +2,7 @@
import Foundation import Foundation
public final class APIDecoder: JSONDecoder { public final class MastodonDecoder: JSONDecoder {
public override init() { public override init() {
super.init() super.init()

View file

@ -2,7 +2,7 @@
import Foundation import Foundation
public final class APIEncoder: JSONEncoder { public final class MastodonEncoder: JSONEncoder {
public override init() { public override init() {
super.init() super.init()

View file

@ -12,21 +12,6 @@ public enum Timeline: Hashable {
public extension Timeline { public extension Timeline {
static let nonLists: [Timeline] = [.home, .local, .federated] static let nonLists: [Timeline] = [.home, .local, .federated]
var endpoint: TimelinesEndpoint {
switch self {
case .home:
return .home
case .local:
return .public(local: true)
case .federated:
return .public(local: false)
case let .list(list):
return .list(id: list.id)
case let .tag(tag):
return .tag(tag)
}
}
} }
extension Timeline: Identifiable { extension Timeline: Identifiable {

5
MastodonAPI/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

35
MastodonAPI/Package.swift Normal file
View file

@ -0,0 +1,35 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MastodonAPI",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "MastodonAPI",
targets: ["MastodonAPI"]),
.library(
name: "MastodonAPIStubs",
targets: ["MastodonAPIStubs"])
],
dependencies: [
.package(path: "HTTP"),
.package(path: "Mastodon")
],
targets: [
.target(
name: "MastodonAPI",
dependencies: ["HTTP", "Mastodon"]),
.target(
name: "MastodonAPIStubs",
dependencies: ["MastodonAPI", .product(name: "Stubbing", package: "HTTP")],
resources: [.process("Resources")]),
.testTarget(
name: "MastodonAPITests",
dependencies: ["MastodonAPIStubs"])
]
)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum AccessTokenEndpoint { public enum AccessTokenEndpoint {
case oauthToken( case oauthToken(

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum AccountEndpoint { public enum AccountEndpoint {
case verifyCredentials case verifyCredentials

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum AppAuthorizationEndpoint { public enum AppAuthorizationEndpoint {
case apps(clientName: String, redirectURI: String, scopes: String, website: URL?) case apps(clientName: String, redirectURI: String, scopes: String, website: URL?)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum ContextEndpoint { public enum ContextEndpoint {
case context(id: String) case context(id: String)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum DeletionEndpoint { public enum DeletionEndpoint {
case oauthRevoke(token: String, clientID: String, clientSecret: String) case oauthRevoke(token: String, clientID: String, clientSecret: String)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum FilterEndpoint { public enum FilterEndpoint {
case create( case create(

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum FiltersEndpoint { public enum FiltersEndpoint {
case filters case filters

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum InstanceEndpoint { public enum InstanceEndpoint {
case instance case instance

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum ListEndpoint { public enum ListEndpoint {
case create(title: String) case create(title: String)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum ListsEndpoint { public enum ListsEndpoint {
case lists case lists

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public struct Paged<T: Endpoint> { public struct Paged<T: Endpoint> {
public let endpoint: T public let endpoint: T

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum PreferencesEndpoint { public enum PreferencesEndpoint {
case preferences case preferences

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum PushSubscriptionEndpoint { public enum PushSubscriptionEndpoint {
case create( case create(

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum StatusEndpoint { public enum StatusEndpoint {
case status(id: String) case status(id: String)

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import HTTP import HTTP
import Mastodon
public enum TimelinesEndpoint { public enum TimelinesEndpoint {
case `public`(local: Bool) case `public`(local: Bool)

View file

@ -0,0 +1,21 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
public extension Timeline {
var endpoint: TimelinesEndpoint {
switch self {
case .home:
return .home
case .local:
return .public(local: true)
case .federated:
return .public(local: false)
case let .list(list):
return .list(id: list.id)
case let .tag(tag):
return .tag(tag)
}
}
}

View file

@ -3,13 +3,14 @@
import Foundation import Foundation
import Combine import Combine
import HTTP import HTTP
import Mastodon
public final class APIClient: Client { public final class MastodonAPIClient: Client {
public var instanceURL: URL? public var instanceURL: URL?
public var accessToken: String? public var accessToken: String?
public required init(session: Session) { public required init(session: Session) {
super.init(session: session, decoder: APIDecoder()) super.init(session: session, decoder: MastodonDecoder())
} }
public override func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> { public override func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
@ -17,14 +18,14 @@ public final class APIClient: Client {
} }
} }
extension APIClient { extension MastodonAPIClient {
public func request<E: Endpoint>(_ endpoint: E) -> AnyPublisher<E.ResultType, Error> { public func request<E: Endpoint>(_ endpoint: E) -> AnyPublisher<E.ResultType, Error> {
guard let instanceURL = instanceURL else { guard let instanceURL = instanceURL else {
return Fail(error: URLError(.badURL)).eraseToAnyPublisher() return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
} }
return super.request( return super.request(
APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: accessToken), MastodonAPITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: accessToken),
decodeErrorsAs: APIError.self) decodeErrorsAs: APIError.self)
} }
} }

View file

@ -3,7 +3,7 @@
import Foundation import Foundation
import HTTP import HTTP
public struct APITarget<E: Endpoint> { public struct MastodonAPITarget<E: Endpoint> {
public let baseURL: URL public let baseURL: URL
public let endpoint: E public let endpoint: E
public let accessToken: String? public let accessToken: String?
@ -15,7 +15,7 @@ public struct APITarget<E: Endpoint> {
} }
} }
extension APITarget: DecodableTarget { extension MastodonAPITarget: DecodableTarget {
public typealias ResultType = E.ResultType public typealias ResultType = E.ResultType
public var pathComponents: [String] { endpoint.pathComponents } public var pathComponents: [String] { endpoint.pathComponents }

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension AccessTokenEndpoint: Stubbing { extension AccessTokenEndpoint: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension AccountEndpoint: Stubbing { extension AccountEndpoint: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension AppAuthorizationEndpoint: Stubbing { extension AppAuthorizationEndpoint: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension ContextEndpoint: Stubbing { extension ContextEndpoint: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension InstanceEndpoint: Stubbing { extension InstanceEndpoint: Stubbing {

View file

@ -1,10 +1,10 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension APITarget: Stubbing { extension MastodonAPITarget: Stubbing {
public func stub(url: URL) -> HTTPStub? { public func stub(url: URL) -> HTTPStub? {
(endpoint as? Stubbing)?.stub(url: url) (endpoint as? Stubbing)?.stub(url: url)
} }

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension Paged: Stubbing where T: Stubbing { extension Paged: Stubbing where T: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension PreferencesEndpoint: Stubbing { extension PreferencesEndpoint: Stubbing {

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import MastodonAPI
import Stubbing import Stubbing
extension TimelinesEndpoint: Stubbing { extension TimelinesEndpoint: Stubbing {

View file

@ -0,0 +1,10 @@
import XCTest
@testable import MastodonAPI
final class MastodonAPITests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

View file

@ -95,6 +95,7 @@
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; }; D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
D0BECB952501B3DD002C1B13 /* Keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Keychain; sourceTree = "<group>"; }; D0BECB952501B3DD002C1B13 /* Keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Keychain; sourceTree = "<group>"; };
D0BECB962501BCE0002C1B13 /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = "<group>"; }; D0BECB962501BCE0002C1B13 /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = "<group>"; };
D0BECB9D2501CBC3002C1B13 /* MastodonAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonAPI; sourceTree = "<group>"; };
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; }; D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; }; D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -188,6 +189,7 @@
D0BECB952501B3DD002C1B13 /* Keychain */, D0BECB952501B3DD002C1B13 /* Keychain */,
D0C7D45624F76169001EBDBB /* Localizations */, D0C7D45624F76169001EBDBB /* Localizations */,
D0E0F1E424FC49FC002C04BF /* Mastodon */, D0E0F1E424FC49FC002C04BF /* Mastodon */,
D0BECB9D2501CBC3002C1B13 /* MastodonAPI */,
D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */, D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */,
D047FA8D24C3E21200AF17C5 /* Products */, D047FA8D24C3E21200AF17C5 /* Products */,
D0BECB962501BCE0002C1B13 /* Secrets */, D0BECB962501BCE0002C1B13 /* Secrets */,

View file

@ -24,7 +24,7 @@ class NotificationService: UNNotificationServiceExtension {
do { do {
let decryptedJSON = try Self.extractAndDecrypt(userInfo: request.content.userInfo) let decryptedJSON = try Self.extractAndDecrypt(userInfo: request.content.userInfo)
pushNotification = try APIDecoder().decode(PushNotification.self, from: decryptedJSON) pushNotification = try MastodonDecoder().decode(PushNotification.self, from: decryptedJSON)
} catch { } catch {
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)

View file

@ -20,18 +20,18 @@ let package = Package(
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")), .package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
.package(path: "DB"), .package(path: "DB"),
.package(path: "Keychain"), .package(path: "Keychain"),
.package(path: "Mastodon"), .package(path: "MastodonAPI"),
.package(path: "Secrets") .package(path: "Secrets")
], ],
targets: [ targets: [
.target( .target(
name: "ServiceLayer", name: "ServiceLayer",
dependencies: ["DB", "Secrets"]), dependencies: ["DB", "MastodonAPI", "Secrets"]),
.target( .target(
name: "ServiceLayerMocks", name: "ServiceLayerMocks",
dependencies: [ dependencies: [
"ServiceLayer", "ServiceLayer",
.product(name: "MastodonStubs", package: "Mastodon"), .product(name: "MastodonAPIStubs", package: "MastodonAPI"),
.product(name: "MockKeychain", package: "Keychain")]), .product(name: "MockKeychain", package: "Keychain")]),
.testTarget( .testTarget(
name: "ServiceLayerTests", name: "ServiceLayerTests",

View file

@ -4,6 +4,7 @@ import DB
import Foundation import Foundation
import Combine import Combine
import Mastodon import Mastodon
import MastodonAPI
import Secrets import Secrets
public struct AllIdentitiesService { public struct AllIdentitiesService {
@ -53,9 +54,9 @@ public extension AllIdentitiesService {
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> { func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> {
let secrets = Secrets(identityID: identity.id, keychain: environment.keychain) let secrets = Secrets(identityID: identity.id, keychain: environment.keychain)
let networkClient = APIClient(session: environment.session) let mastodonAPIClient = MastodonAPIClient(session: environment.session)
networkClient.instanceURL = identity.url mastodonAPIClient.instanceURL = identity.url
return identityDatabase.deleteIdentity(id: identity.id) return identityDatabase.deleteIdentity(id: identity.id)
.collect() .collect()
@ -65,7 +66,7 @@ public extension AllIdentitiesService {
clientID: try secrets.item(.clientID), clientID: try secrets.item(.clientID),
clientSecret: try secrets.item(.clientSecret)) clientSecret: try secrets.item(.clientSecret))
} }
.flatMap(networkClient.request) .flatMap(mastodonAPIClient.request)
.collect() .collect()
.tryMap { _ in .tryMap { _ in
try secrets.deleteAllItems() try secrets.deleteAllItems()

View file

@ -3,14 +3,15 @@
import Foundation import Foundation
import Combine import Combine
import Mastodon import Mastodon
import MastodonAPI
public struct AuthenticationService { public struct AuthenticationService {
private let networkClient: APIClient private let mastodonAPIClient: MastodonAPIClient
private let webAuthSessionType: WebAuthSession.Type private let webAuthSessionType: WebAuthSession.Type
private let webAuthSessionContextProvider = WebAuthSessionContextProvider() private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
public init(environment: AppEnvironment) { public init(environment: AppEnvironment) {
networkClient = APIClient(session: environment.session) mastodonAPIClient = MastodonAPIClient(session: environment.session)
webAuthSessionType = environment.webAuthSessionType webAuthSessionType = environment.webAuthSessionType
} }
} }
@ -22,9 +23,9 @@ public extension AuthenticationService {
redirectURI: OAuth.callbackURL.absoluteString, redirectURI: OAuth.callbackURL.absoluteString,
scopes: OAuth.scopes, scopes: OAuth.scopes,
website: OAuth.website) website: OAuth.website)
let target = APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil) let target = MastodonAPITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
return networkClient.request(target) return mastodonAPIClient.request(target)
} }
func authenticate(instanceURL: URL, appAuthorization: AppAuthorization) -> AnyPublisher<AccessToken, Error> { func authenticate(instanceURL: URL, appAuthorization: AppAuthorization) -> AnyPublisher<AccessToken, Error> {
@ -63,9 +64,9 @@ public extension AuthenticationService {
grantType: OAuth.grantType, grantType: OAuth.grantType,
scopes: OAuth.scopes, scopes: OAuth.scopes,
redirectURI: OAuth.callbackURL.absoluteString) redirectURI: OAuth.callbackURL.absoluteString)
let target = APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil) let target = MastodonAPITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
return networkClient.request(target) return mastodonAPIClient.request(target)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View file

@ -4,6 +4,7 @@ import DB
import Foundation import Foundation
import Combine import Combine
import Mastodon import Mastodon
import MastodonAPI
import Secrets import Secrets
public class IdentityService { public class IdentityService {
@ -13,7 +14,7 @@ public class IdentityService {
private let identityDatabase: IdentityDatabase private let identityDatabase: IdentityDatabase
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let environment: AppEnvironment private let environment: AppEnvironment
private let networkClient: APIClient private let mastodonAPIClient: MastodonAPIClient
private let secrets: Secrets private let secrets: Secrets
private let observationErrorsInput = PassthroughSubject<Error, Never>() private let observationErrorsInput = PassthroughSubject<Error, Never>()
@ -37,9 +38,9 @@ public class IdentityService {
secrets = Secrets( secrets = Secrets(
identityID: identityID, identityID: identityID,
keychain: environment.keychain) keychain: environment.keychain)
networkClient = APIClient(session: environment.session) mastodonAPIClient = MastodonAPIClient(session: environment.session)
networkClient.instanceURL = identity.url mastodonAPIClient.instanceURL = identity.url
networkClient.accessToken = try? secrets.item(.accessToken) mastodonAPIClient.accessToken = try? secrets.item(.accessToken)
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent) contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
@ -53,21 +54,21 @@ public class IdentityService {
} }
public extension IdentityService { public extension IdentityService {
var isAuthorized: Bool { networkClient.accessToken != nil } var isAuthorized: Bool { mastodonAPIClient.accessToken != nil }
func updateLastUse() -> AnyPublisher<Never, Error> { func updateLastUse() -> AnyPublisher<Never, Error> {
identityDatabase.updateLastUsedAt(identityID: identity.id) identityDatabase.updateLastUsedAt(identityID: identity.id)
} }
func verifyCredentials() -> AnyPublisher<Never, Error> { func verifyCredentials() -> AnyPublisher<Never, Error> {
networkClient.request(AccountEndpoint.verifyCredentials) mastodonAPIClient.request(AccountEndpoint.verifyCredentials)
.zip(Just(identity.id).first().setFailureType(to: Error.self)) .zip(Just(identity.id).first().setFailureType(to: Error.self))
.flatMap(identityDatabase.updateAccount) .flatMap(identityDatabase.updateAccount)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func refreshServerPreferences() -> AnyPublisher<Never, Error> { func refreshServerPreferences() -> AnyPublisher<Never, Error> {
networkClient.request(PreferencesEndpoint.preferences) mastodonAPIClient.request(PreferencesEndpoint.preferences)
.zip(Just(self).first().setFailureType(to: Error.self)) .zip(Just(self).first().setFailureType(to: Error.self))
.map { ($1.identity.preferences.updated(from: $0), $1.identity.id) } .map { ($1.identity.preferences.updated(from: $0), $1.identity.id) }
.flatMap(identityDatabase.updatePreferences) .flatMap(identityDatabase.updatePreferences)
@ -75,7 +76,7 @@ public extension IdentityService {
} }
func refreshInstance() -> AnyPublisher<Never, Error> { func refreshInstance() -> AnyPublisher<Never, Error> {
networkClient.request(InstanceEndpoint.instance) mastodonAPIClient.request(InstanceEndpoint.instance)
.zip(Just(identity.id).first().setFailureType(to: Error.self)) .zip(Just(identity.id).first().setFailureType(to: Error.self))
.flatMap(identityDatabase.updateInstance) .flatMap(identityDatabase.updateInstance)
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -90,19 +91,19 @@ public extension IdentityService {
} }
func refreshLists() -> AnyPublisher<Never, Error> { func refreshLists() -> AnyPublisher<Never, Error> {
networkClient.request(ListsEndpoint.lists) mastodonAPIClient.request(ListsEndpoint.lists)
.flatMap(contentDatabase.setLists(_:)) .flatMap(contentDatabase.setLists(_:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func createList(title: String) -> AnyPublisher<Never, Error> { func createList(title: String) -> AnyPublisher<Never, Error> {
networkClient.request(ListEndpoint.create(title: title)) mastodonAPIClient.request(ListEndpoint.create(title: title))
.flatMap(contentDatabase.createList(_:)) .flatMap(contentDatabase.createList(_:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func deleteList(id: String) -> AnyPublisher<Never, Error> { func deleteList(id: String) -> AnyPublisher<Never, Error> {
networkClient.request(DeletionEndpoint.list(id: id)) mastodonAPIClient.request(DeletionEndpoint.list(id: id))
.map { _ in id } .map { _ in id }
.flatMap(contentDatabase.deleteList(id:)) .flatMap(contentDatabase.deleteList(id:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -113,13 +114,13 @@ public extension IdentityService {
} }
func refreshFilters() -> AnyPublisher<Never, Error> { func refreshFilters() -> AnyPublisher<Never, Error> {
networkClient.request(FiltersEndpoint.filters) mastodonAPIClient.request(FiltersEndpoint.filters)
.flatMap(contentDatabase.setFilters(_:)) .flatMap(contentDatabase.setFilters(_:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> { func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
networkClient.request(FilterEndpoint.create(phrase: filter.phrase, mastodonAPIClient.request(FilterEndpoint.create(phrase: filter.phrase,
context: filter.context, context: filter.context,
irreversible: filter.irreversible, irreversible: filter.irreversible,
wholeWord: filter.wholeWord, wholeWord: filter.wholeWord,
@ -129,7 +130,7 @@ public extension IdentityService {
} }
func updateFilter(_ filter: Filter) -> AnyPublisher<Never, Error> { func updateFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
networkClient.request(FilterEndpoint.update(id: filter.id, mastodonAPIClient.request(FilterEndpoint.update(id: filter.id,
phrase: filter.phrase, phrase: filter.phrase,
context: filter.context, context: filter.context,
irreversible: filter.irreversible, irreversible: filter.irreversible,
@ -140,7 +141,7 @@ public extension IdentityService {
} }
func deleteFilter(id: String) -> AnyPublisher<Never, Error> { func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
networkClient.request(DeletionEndpoint.filter(id: id)) mastodonAPIClient.request(DeletionEndpoint.filter(id: id))
.map { _ in id } .map { _ in id }
.flatMap(contentDatabase.deleteFilter(id:)) .flatMap(contentDatabase.deleteFilter(id:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -180,7 +181,7 @@ public extension IdentityService {
.appendingPathComponent(deviceToken) .appendingPathComponent(deviceToken)
.appendingPathComponent(identityID.uuidString) .appendingPathComponent(identityID.uuidString)
return networkClient.request( return mastodonAPIClient.request(
PushSubscriptionEndpoint.create( PushSubscriptionEndpoint.create(
endpoint: endpoint, endpoint: endpoint,
publicKey: publicKey, publicKey: publicKey,
@ -194,14 +195,14 @@ public extension IdentityService {
func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> { func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> {
let identityID = identity.id let identityID = identity.id
return networkClient.request(PushSubscriptionEndpoint.update(alerts: alerts)) return mastodonAPIClient.request(PushSubscriptionEndpoint.update(alerts: alerts))
.map { ($0.alerts, nil, identityID) } .map { ($0.alerts, nil, identityID) }
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:)) .flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func service(timeline: Timeline) -> StatusListService { func service(timeline: Timeline) -> StatusListService {
StatusListService(timeline: timeline, networkClient: networkClient, contentDatabase: contentDatabase) StatusListService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
} }

View file

@ -4,6 +4,7 @@ import Combine
import DB import DB
import Foundation import Foundation
import Mastodon import Mastodon
import MastodonAPI
public struct StatusListService { public struct StatusListService {
public let statusSections: AnyPublisher<[[Status]], Error> public let statusSections: AnyPublisher<[[Status]], Error>
@ -11,13 +12,13 @@ public struct StatusListService {
public let contextParentID: String? public let contextParentID: String?
private let filterContext: Filter.Context private let filterContext: Filter.Context
private let networkClient: APIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let requestClosure: (_ maxID: String?, _ minID: String?) -> AnyPublisher<Never, Error> private let requestClosure: (_ maxID: String?, _ minID: String?) -> AnyPublisher<Never, Error>
} }
extension StatusListService { extension StatusListService {
init(timeline: Timeline, networkClient: APIClient, contentDatabase: ContentDatabase) { init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
let filterContext: Filter.Context let filterContext: Filter.Context
switch timeline { switch timeline {
@ -31,9 +32,9 @@ extension StatusListService {
paginates: true, paginates: true,
contextParentID: nil, contextParentID: nil,
filterContext: filterContext, filterContext: filterContext,
networkClient: networkClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) { maxID, minID in contentDatabase: contentDatabase) { maxID, minID in
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID)) mastodonAPIClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
.flatMap { contentDatabase.insert(statuses: $0, timeline: timeline) } .flatMap { contentDatabase.insert(statuses: $0, timeline: timeline) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -50,7 +51,7 @@ public extension StatusListService {
} }
func statusService(status: Status) -> StatusService { func statusService(status: Status) -> StatusService {
StatusService(status: status, networkClient: networkClient, contentDatabase: contentDatabase) StatusService(status: status, networkClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
func contextService(statusID: String) -> Self { func contextService(statusID: String) -> Self {
@ -58,13 +59,13 @@ public extension StatusListService {
paginates: false, paginates: false,
contextParentID: statusID, contextParentID: statusID,
filterContext: .thread, filterContext: .thread,
networkClient: networkClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) { _, _ in contentDatabase: contentDatabase) { _, _ in
Publishers.Merge( Publishers.Merge(
networkClient.request(StatusEndpoint.status(id: statusID)) mastodonAPIClient.request(StatusEndpoint.status(id: statusID))
.flatMap(contentDatabase.insert(status:)) .flatMap(contentDatabase.insert(status:))
.eraseToAnyPublisher(), .eraseToAnyPublisher(),
networkClient.request(ContextEndpoint.context(id: statusID)) mastodonAPIClient.request(ContextEndpoint.context(id: statusID))
.flatMap { contentDatabase.insert(context: $0, parentID: statusID) } .flatMap { contentDatabase.insert(context: $0, parentID: statusID) }
.eraseToAnyPublisher()) .eraseToAnyPublisher())
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -4,24 +4,25 @@ import Foundation
import Combine import Combine
import DB import DB
import Mastodon import Mastodon
import MastodonAPI
public struct StatusService { public struct StatusService {
public let status: Status public let status: Status
private let networkClient: APIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(status: Status, networkClient: APIClient, contentDatabase: ContentDatabase) { init(status: Status, networkClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.status = status self.status = status
self.networkClient = networkClient self.mastodonAPIClient = networkClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
} }
} }
public extension StatusService { public extension StatusService {
func toggleFavorited() -> AnyPublisher<Never, Error> { func toggleFavorited() -> AnyPublisher<Never, Error> {
networkClient.request(status.displayStatus.favourited mastodonAPIClient.request(status.displayStatus.favourited
? StatusEndpoint.unfavourite(id: status.displayStatus.id) ? StatusEndpoint.unfavourite(id: status.displayStatus.id)
: StatusEndpoint.favourite(id: status.displayStatus.id)) : StatusEndpoint.favourite(id: status.displayStatus.id))
.flatMap(contentDatabase.insert(status:)) .flatMap(contentDatabase.insert(status:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View file

@ -4,12 +4,13 @@ import Foundation
import Combine import Combine
import HTTP import HTTP
import Mastodon import Mastodon
import MastodonStubs import MastodonAPI
import MastodonAPIStubs
import ServiceLayer import ServiceLayer
import ServiceLayerMocks import ServiceLayerMocks
import ViewModels import ViewModels
private let decoder = APIDecoder() private let decoder = MastodonDecoder()
private let devInstanceURL = URL(string: "https://mastodon.social")! private let devInstanceURL = URL(string: "https://mastodon.social")!
// swiftlint:disable force_try // swiftlint:disable force_try