Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions BeeKit/ImageDownloadService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Part of BeeSwift. Copyright Beeminder

import AlamofireImage

final public class ImageDownloadService {
static public let shared = ImageDownloadService()
public let downloader: ImageDownloader

private init() { downloader = ImageDownloader.default }
}
4 changes: 2 additions & 2 deletions BeeKit/Managers/CurrentUserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ import SwiftyJSON

internal static let keychainPrefix = "CurrentUserManager_"

private let requestManager: RequestManager
private let requestManager: RequestManaging

fileprivate static var allKeys: [String] {
[accessTokenKey, usernameKey, deadbeatKey, defaultLeadtimeKey, defaultAlertstartKey, defaultDeadlineKey, beemTZKey]
}

init(requestManager: RequestManager, container: BeeminderPersistentContainer) {
init(requestManager: RequestManaging, container: BeeminderPersistentContainer) {
self.requestManager = requestManager
self.modelContainer = container
let context = container.newBackgroundContext()
Expand Down
4 changes: 2 additions & 2 deletions BeeKit/Managers/DataPointManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import SwiftyJSON
// prevents effectively no-op updates due to float rounding
private let datapointValueEpsilon = 0.00000001

let requestManager: RequestManager
let requestManager: RequestManaging

init(requestManager: RequestManager, container: BeeminderPersistentContainer) {
init(requestManager: RequestManaging, container: BeeminderPersistentContainer) {
self.requestManager = requestManager
self.modelContainer = container
let context = container.newBackgroundContext()
Expand Down
4 changes: 2 additions & 2 deletions BeeKit/Managers/GoalManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import SwiftyJSON
@NSModelActor(disableGenerateInit: true) public actor GoalManager {
private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "GoalManager")

private let requestManager: RequestManager
private let requestManager: RequestManaging
private nonisolated let currentUserManager: CurrentUserManager

private var queuedGoalsBackgroundTaskRunning: Bool = false

init(requestManager: RequestManager, currentUserManager: CurrentUserManager, container: BeeminderPersistentContainer)
init(requestManager: RequestManaging, currentUserManager: CurrentUserManager, container: BeeminderPersistentContainer)
{
modelContainer = container
let context = container.newBackgroundContext()
Expand Down
80 changes: 40 additions & 40 deletions BeeKit/Managers/RequestManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,12 @@ import Foundation
import OSLog
import SwiftyJSON

public enum ServerError: LocalizedError {
case notFound
case unauthorized
case forbidden
case serverError(Int)
case custom(String, requestError: Error?)
public var errorDescription: String? {
switch self {
case .notFound: return "Not found"
case .unauthorized: return "Unauthorized"
case .forbidden: return "Permission denied"
case .serverError(let code): return "Server error (\(code)). Please try again later"
case .custom(let message, _): return message
}
}
var requestError: Error? {
switch self {
case .custom(_, let error): return error
default: return nil
}
}
}

public class RequestManager {
public actor RequestManager {
public let baseURLString = Config().baseURLString
private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "RequestManager")
func rawRequest(url: String, method: HTTPMethod, parameters: [String: Any]? = nil, headers: HTTPHeaders) async throws
-> Any?
{

var urlWithSubstitutions = url
if url.contains("{username}") {
guard let username = await ServiceLocator.currentUserManager.username else {
Expand All @@ -51,36 +27,29 @@ public class RequestManager {
}
urlWithSubstitutions = urlWithSubstitutions.replacingOccurrences(of: "{username}", with: username)
}

let encoding: ParameterEncoding = if method == .get { URLEncoding.default } else { JSONEncoding.default } // TODO
let encoding: ParameterEncoding = method == .get ? URLEncoding.default : JSONEncoding.default
let response = await AF.request(
"\(baseURLString)/\(urlWithSubstitutions)",
method: method,
parameters: parameters,
encoding: encoding,
headers: HTTPHeaders.default + headers
).validate().serializingData(emptyRequestMethods: [HTTPMethod.post]).response

switch response.result {
case .success(let data):
let asJSON = try? JSONSerialization.jsonObject(with: data)
return asJSON

return try await Task.detached(priority: .low) { try JSONSerialization.jsonObject(with: data) }.value
case .failure(let error):
logger.error("Error issuing request \(url): \(error, privacy: .public)")

// Log out the user on an unauthorized response
if case .responseValidationFailed(let reason) = error {
if case .unacceptableStatusCode(let code) = reason {
if code == 401 { try? await ServiceLocator.currentUserManager.signOut() }
}
}

// If we receive an error message from the server use it as our user-visible error
if let data = response.data, let errorMessage = try JSON(data: data)["error_message"].string {
throw ServerError.custom(errorMessage, requestError: error)
}

// Handle common HTTP errors with specific error types
if case .responseValidationFailed(let reason) = error {
if case .unacceptableStatusCode(let code) = reason {
Expand All @@ -93,10 +62,16 @@ public class RequestManager {
}
}
}

throw error
}
}
func authenticationHeaders() -> HTTPHeaders {
guard let accessToken = ServiceLocator.currentUserManager.accessToken else { return HTTPHeaders() }
return HTTPHeaders([HTTPHeader(name: "Authorization", value: "Bearer " + accessToken)])
}
}

extension RequestManager: RequestManaging {
public func get(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
return try await rawRequest(url: url, method: .get, parameters: parameters, headers: authenticationHeaders())
}
Expand All @@ -109,11 +84,6 @@ public class RequestManager {
public func delete(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
return try await rawRequest(url: url, method: .delete, parameters: parameters, headers: authenticationHeaders())
}
func authenticationHeaders() -> HTTPHeaders {
guard let accessToken = ServiceLocator.currentUserManager.accessToken else { return HTTPHeaders() }
return HTTPHeaders([HTTPHeader(name: "Authorization", value: "Bearer " + accessToken)])
}

public func addDatapoint(urtext: String, slug: String, requestId: String? = nil) async throws -> Any? {
let params = ["urtext": urtext, "requestid": requestId].compactMapValues { $0 }
return try await post(url: "api/v1/users/{username}/goals/\(slug)/datapoints.json", parameters: params)
Expand All @@ -128,3 +98,33 @@ extension HTTPHeaders {
return HTTPHeaders(allHeaders)
}
}

extension RequestManager: SignedRequestManaging {
public func signedGET(url: String, parameters: [String: Any]?) async throws -> Any? {
let params = signedParameters(parameters)
return try await rawRequest(url: url, method: .get, parameters: params, headers: authenticationHeaders())
}
public func signedPOST(url: String, parameters: [String: Any]?) async throws -> Any? {
let params = signedParameters(parameters)
return try await rawRequest(url: url, method: .post, parameters: params, headers: authenticationHeaders())
}
fileprivate func signedParameters(_ params: [String: Any]?) -> [String: Any]? {
if params == nil { return params }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guard

var signed = params
var base = ""
var keys = Array(params!.keys)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

force

keys.sort(by: { $0 < $1 })
for key in keys {
let value: AnyObject? = params![key] as AnyObject?
if !(value is String) { return params! }
let allowedCharacterSet = (CharacterSet(charactersIn: "@/").inverted)
let escapedKey = key.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)
let escapedValue = (params![key] as AnyObject).addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)
if base.count > 0 { base += "&" }
base += "\(escapedKey!)=\(escapedValue!)"
}
let token = base.hmac(algorithm: HMACAlgorithm.SHA1, key: Config().requestSigningKey)
signed?["beemios_token"] = token
return signed! as [String: Any]
}
}
27 changes: 27 additions & 0 deletions BeeKit/Managers/RequestManaging.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Part of BeeSwift. Copyright Beeminder

public protocol RequestManaging {
func get(url: String, parameters: [String: Any]?) async throws -> Any?
func put(url: String, parameters: [String: Any]?) async throws -> Any?
func post(url: String, parameters: [String: Any]?) async throws -> Any?
func delete(url: String, parameters: [String: Any]?) async throws -> Any?
func addDatapoint(urtext: String, slug: String, requestId: String?) async throws -> Any?
}

extension RequestManaging {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what this is for? It seems like functions just calling themselves

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol cannot have a default for the parameter and several places were calling them without specifying the parameter with a nil.

This here is a version that does (pass nil as a default) and allows then others to make calls without specifying also the parameters: nil for each and every call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made obsolete by #744

public func get(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await get(url: url, parameters: parameters)
}
public func put(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await put(url: url, parameters: parameters)
}
public func post(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await post(url: url, parameters: parameters)
}
public func delete(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await delete(url: url, parameters: parameters)
}
public func addDatapoint(urtext: String, slug: String, requestId: String? = nil) async throws -> Any? {
try await addDatapoint(urtext: urtext, slug: slug, requestId: requestId)
}
}
26 changes: 26 additions & 0 deletions BeeKit/Managers/ServerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Part of BeeSwift. Copyright Beeminder

import Foundation

public enum ServerError: LocalizedError {
case notFound
case unauthorized
case forbidden
case serverError(Int)
case custom(String, requestError: Error?)
public var errorDescription: String? {
switch self {
case .notFound: return "Not found"
case .unauthorized: return "Unauthorized"
case .forbidden: return "Permission denied"
case .serverError(let code): return "Server error (\(code)). Please try again later"
case .custom(let message, _): return message
}
}
var requestError: Error? {
switch self {
case .custom(_, let error): return error
default: return nil
}
}
}
54 changes: 0 additions & 54 deletions BeeKit/Managers/SignedRequestManager.swift

This file was deleted.

15 changes: 15 additions & 0 deletions BeeKit/Managers/SignedRequestManaging.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Part of BeeSwift. Copyright Beeminder

public protocol SignedRequestManaging {
func signedGET(url: String, parameters: [String: Any]?) async throws -> Any?
func signedPOST(url: String, parameters: [String: Any]?) async throws -> Any?
}

extension SignedRequestManaging {
public func signedGET(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await signedGET(url: url, parameters: parameters)
}
public func signedPOST(url: String, parameters: [String: Any]? = nil) async throws -> Any? {
try await signedPOST(url: url, parameters: parameters)
}
}
4 changes: 2 additions & 2 deletions BeeKit/Managers/VersionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ private let ageOfReleaseToWarn: TimeInterval = 10.0 * dayInSeconds
public class VersionManager {
private var minRequiredVersion: String = "1.0"
private var updateState = UpdateState.UpToDate
private let requestManager: RequestManager
private let requestManager: RequestManaging

init(requestManager: RequestManager) { self.requestManager = requestManager }
init(requestManager: RequestManaging) { self.requestManager = requestManager }

public func lastChckedUpdateState() -> UpdateState { return updateState }

Expand Down
10 changes: 8 additions & 2 deletions BeeKit/ServiceLocator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
// Copyright 2023 APB. All rights reserved.
//

import AlamofireImage
import Foundation
import OSLog

public class ServiceLocator {
private static let logger = Logger(subsystem: "com.beeminder.beeminder", category: "ServiceLocator")

private static let sharedRequestManager = RequestManager()

public static let requestManager: RequestManaging = sharedRequestManager
public static let signedRequestManager: SignedRequestManaging = sharedRequestManager

public static let persistentContainer = BeeminderPersistentContainer.create()

public static let requestManager = RequestManager()
public static let signedRequestManager = SignedRequestManager(requestManager: requestManager)
public static let currentUserManager = CurrentUserManager(
requestManager: requestManager,
container: persistentContainer
Expand All @@ -33,4 +37,6 @@ public class ServiceLocator {
goalManager: goalManager,
container: persistentContainer
)

public static let imageDownloader = ImageDownloadService.shared.downloader
}
Loading
Loading