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
2 changes: 2 additions & 0 deletions BeeKit/Config.swift.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ public struct Config {
public var sentryClientDSN = ""
public var requestSigningKey = ""
public var baseURLString = "https://www.beeminder.com"
public var apiHost: String = "www.beeminder.com"
Comment thread
krugerk marked this conversation as resolved.

public init() {
self.baseURLString = "https://www.beeminder.com"
self.requestSigningKey = ""
self.sentryClientDSN = ""
self.apiHost = "www.beeminder.com"
}
}
2 changes: 1 addition & 1 deletion BeeKit/GoalExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension Goal {

public var isDataProvidedAutomatically: Bool { return !(self.autodata ?? "").isEmpty }

/// The daystamp corresponding to the day of the goal's creation, thus the first day we should add data points for.
/// The daystamp corresponding to the day of the goal's creation, thus the first day we should add datapoints for.
var initDaystamp: Daystamp {
let initDate = Date(timeIntervalSince1970: Double(self.initDay))

Expand Down
10 changes: 4 additions & 6 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 Expand Up @@ -123,10 +123,8 @@ import SwiftyJSON
}
public func signInWithEmail(_ email: String, password: String) async {
do {
let response = try await requestManager.post(
url: "api/private/sign_in",
parameters: ["user": ["login": email, "password": password], "beemios_secret": self.beemiosSecret]
as [String: Any]
let response = try await requestManager.request(
endpoint: .signIn(username: email, password: password, beemiosSecret: beemiosSecret)
)
try! await self.handleSuccessfulSignin(JSON(response!))
} catch { try! await self.handleFailedSignin(error, errorMessage: error.localizedDescription) }
Expand Down
36 changes: 23 additions & 13 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 All @@ -30,28 +30,38 @@ import SwiftyJSON
{
let val = datapoint.value
if datapointValue == val && comment == datapoint.comment { return }
let params = ["value": "\(datapointValue)", "comment": comment]
let _ = try await requestManager.put(
url: "api/v1/users/{username}/goals/\(goal.slug)/datapoints/\(datapoint.id).json",
parameters: params

let _ = try await requestManager.request(
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.

_ = ...

Can also be used throughout, without the let.

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.

would include SwiftLint for checking this

endpoint: .updateDatapoint(
username: goal.owner.username,
goalname: goal.slug,
datapointID: datapoint.id,
value: datapointValue,
comment: comment
)
)
}

private func deleteDatapoint(goal: Goal, datapoint: DataPoint) async throws {
let _ = try await requestManager.delete(
url: "api/v1/users/{username}/goals/\(goal.slug)/datapoints/\(datapoint.id)"
let _ = try await requestManager.request(
endpoint: .deletedDatapoint(username: goal.owner.username, goalname: goal.slug, datapointID: datapoint.id)
)
}

private func postDatapoint(goal: Goal, urText: String, requestId: String) async throws {
let _ = try await requestManager.addDatapoint(urtext: urText, slug: goal.slug, requestId: requestId)
let _ = try await requestManager.request(
endpoint: .createDatapoint(
username: goal.owner.username,
goalname: goal.slug,
urtext: urText,
requestID: requestId
)
)
}

private func fetchDatapoints(goal: Goal, sort: String, per: Int, page: Int) async throws -> [DataPoint] {
let params = ["sort": sort, "per": per, "page": page] as [String: Any]
let response = try await requestManager.get(
url: "api/v1/users/{username}/goals/\(goal.slug)/datapoints.json",
parameters: params
let response = try await requestManager.request(
endpoint: .getDatapoints(username: goal.owner.username, goalname: goal.slug, sort: sort, page: page, per: per)
)
let responseJSON = JSON(response!)

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

import Alamofire
import Foundation
import OSLog
import SwiftyJSON

public enum Endpoint {
// signing in
case signIn(username: String, password: String, beemiosSecret: String)
// about the app versions the server expects to see
case appVersions
// Retrieves information and a list of goalnames for the user with username.
case getUser(username: String, diff_since: TimeInterval? = nil, emaciated: Bool? = nil)
// Gets goal details for user u's goal g
case getGoalDetails(username: String, goalname: String, datapoints_count: Int? = nil, emaciated: Bool? = nil)
// Get the list of datapoints for user u's goal g
case getDatapoints(
username: String,
goalname: String,
sort: String? = nil,
count: Int? = nil,
page: Int? = nil,
per: Int? = nil
)

// Get all goals for a user
case getGoals(username: String, emaciated: Bool? = nil)
// Force a fetch of autodata and graph refresh
case requestAutodataFetch(username: String, goalname: String)
// Update the datapoint with ID id for user u's goal g (beeminder.com/u/g).
case updateDatapoint(
username: String,
goalname: String,
datapointID: String,
timestamp: Double? = nil,
value: NSNumber? = nil,
comment: String? = nil,
urtext: String? = nil
)
// Update a goal for a user
case updateGoal(
username: String,
goalname: String,
title: String? = nil,
tmin: String? = nil,
tmax: String? = nil,
isSecret: Bool? = nil,
isDataPublic: Bool? = nil,
tags: [String]? = nil,
iiParams: [String: Any?]? = nil,
leadtime: Int? = nil,
alertstart: Int? = nil,
deadline: Int? = nil,
usesDefaultNotifications: Bool? = nil
)
// Update the user
case updateUser(
username: String,
default_alertstart: Int? = nil,
default_deadline: Int? = nil,
default_leadtime: Int? = nil
)
// Add a new datapoint to user u's goal g — beeminder.com/u/g.
case createDatapoint(
username: String,
goalname: String,
value: NSNumber? = nil,
timestamp: Double? = nil,
daystamp: String? = nil,
comment: String? = nil,
urtext: String? = nil,
requestID: String? = nil
)
case deletedDatapoint(username: String, goalname: String, datapointID: String)
case registerDeviceToken(token: String, environment: String? = nil)

var url: URL {
var urlComponents: URLComponents {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = Config().apiHost
urlComponents.path = self.path
return urlComponents
}
return urlComponents.url!
}
var path: String {
return switch self {
case .signIn: "/api/private/sign_in"

case .appVersions: "/api/private/app_versions.json"
case .registerDeviceToken: "/api/private/device_tokens"

case .getUser(let username, _, _), .updateUser(let username, _, _, _): "/api/v1/users/\(username).json"

case .getGoalDetails(let username, let goalname, _, _): "/api/v1/users/\(username)/goals/\(goalname)"
case .getDatapoints(let username, let goalname, _, _, _, _):
"/api/v1/users/\(username)/goals/\(goalname)/datapoints.json"
case .getGoals(let username, _): "/api/v1/users/\(username)/goals.json"
case .requestAutodataFetch(let username, let goalname):
"/api/v1/users/\(username)/goals/\(goalname)/refresh_graph.json"
case .updateDatapoint(let username, let goalname, let datapointID, _, _, _, _):
"/api/v1/users/\(username)/goals/\(goalname)/datapoints/\(datapointID).json"
case .updateGoal(let username, let goalname, _, _, _, _, _, _, _, _, _, _, _):
"/api/v1/users/\(username)/goals/\(goalname).json"
case .createDatapoint(let username, let goalname, _, _, _, _, _, _):
"/api/v1/users/\(username)/goals/\(goalname)/datapoints.json"
case .deletedDatapoint(let username, let goalname, let datapointID):
"/api/v1/users/\(username)/goals/\(goalname)/datapoints/\(datapointID).json"
}
}
var method: HTTPMethod {
return switch self {
case .signIn, .createDatapoint, .registerDeviceToken: .post
case .appVersions, .getUser, .getGoalDetails, .getDatapoints, .getGoals, .requestAutodataFetch: .get
case .updateDatapoint, .updateGoal, .updateUser: .put
case .deletedDatapoint: .delete
}
}
var parameters: [String: Any]? {
switch self {
case .signIn(let username, let password, let beemiosSecret):
return ["user": ["login": username, "password": password], "beemios_secret": beemiosSecret] as [String: Any]
case .getUser(_, let diff_since, let emaciated):
var parameters: [String: Any] = [:]
if let diff_since { parameters["diff_since"] = diff_since }
if let emaciated { parameters["emaciated"] = emaciated }
return parameters.isEmpty ? nil : parameters
case .getGoalDetails(_, _, let datapoints_count, let emaciated):
var parameters: [String: Any] = [:]
if let datapoints_count { parameters["datapoints_count"] = datapoints_count }
if let emaciated { parameters["emaciated"] = emaciated }
return parameters.isEmpty ? nil : parameters
case .getDatapoints(_, _, let sort, let count, let page, let per):
var parameters: [String: Any] = [:]
if let sort { parameters["sort"] = sort }
if let count { parameters["count"] = count }
if let page { parameters["page"] = page }
if let per { parameters["per"] = per }
return parameters.isEmpty ? nil : parameters
case .getGoals(_, let emaciated):
if let emaciated { return ["emaciated": emaciated] }
return nil
case .updateDatapoint(_, _, _, let timestamp, let value, let comment, let urtext):
var parameters: [String: Any] = [:]
if let timestamp { parameters["timestamp"] = timestamp }
if let value { parameters["value"] = value }
if let comment { parameters["comment"] = comment }
if let urtext { parameters["urtext"] = urtext }
return parameters.isEmpty ? nil : parameters
case .updateGoal(
_,
_,
let title,
let tmin,
let tmax,
let isSecret,
let isDataPublic,
let tags,
let iiParams,
let leadtime,
let alertstart,
let deadline,
let usesDefaultNotifications
):
var parameters: [String: Any] = [:]
if let title { parameters["title"] = title }
if let tmin { parameters["tmin"] = tmin }
if let tmax { parameters["tmax"] = tmax }
if let isSecret { parameters["is_secret"] = isSecret }
if let isDataPublic { parameters["is_data_public"] = isDataPublic }
if let tags { parameters["tags"] = tags }
if let iiParams { parameters["ii_params"] = iiParams }
if let leadtime { parameters["leadtime"] = leadtime }
if let alertstart { parameters["alertstart"] = alertstart }
if let deadline { parameters["deadline"] = deadline }
if let usesDefaultNotifications { parameters["use_defaults"] = usesDefaultNotifications }
return parameters.isEmpty ? nil : parameters
case .updateUser(_, let default_alertstart, let default_deadline, let default_leadtime):
var parameters: [String: Any] = [:]
if let default_alertstart { parameters["default_alertstart"] = default_alertstart }
if let default_deadline { parameters["default_deadline"] = default_deadline }
if let default_leadtime { parameters["default_leadtime"] = default_leadtime }
return parameters.isEmpty ? nil : parameters
case .createDatapoint(_, _, let value, let timestamp, let daystamp, let comment, let urtext, let requestID):
var parameters: [String: Any] = [:]
if let value { parameters["value"] = value }
if let timestamp { parameters["timestamp"] = timestamp }
if let daystamp { parameters["daystamp"] = daystamp }
if let comment { parameters["comment"] = comment }
if let urtext { parameters["urtext"] = urtext }
if let requestID { parameters["request_id"] = requestID }
return parameters.isEmpty ? nil : parameters
case .registerDeviceToken(let token, let environment):
var parameters: [String: Any] = [:]
parameters["device_token"] = token
if let environment { parameters["server"] = environment }
return parameters.isEmpty ? nil : parameters
default: return nil
}
}
var shouldSign: Bool {
return switch self {
case .registerDeviceToken: true
default: false
}
}
}
31 changes: 19 additions & 12 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 Expand Up @@ -82,9 +82,9 @@ import SwiftyJSON
logger.notice("Goals unknown, doing full fetch")
// We must fetch the user object first, and then fetch goals afterwards, to guarantee User.updated_at is
// a safe timestamp for future fetches without losing data
let userResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}.json")!)
let userResponse = JSON(try await requestManager.request(endpoint: .getUser(username: user.username))!)
let goalResponse = JSON(
try await requestManager.get(url: "api/v1/users/{username}/goals.json", parameters: ["emaciated": "true"])!
try await requestManager.request(endpoint: .getGoals(username: user.username, emaciated: true))!
)

// The user may have logged out during the network operation. If so we have nothing to do
Expand All @@ -103,9 +103,12 @@ import SwiftyJSON
private func refreshGoalsIncremental(user: User) async throws {
logger.notice("Doing incremental update since \(user.updatedAt, privacy: .public)")
let userResponse = JSON(
try await requestManager.get(
url: "api/v1/users/{username}.json",
parameters: ["diff_since": user.updatedAt.timeIntervalSince1970 + 1, "emaciated": "true"]
try await requestManager.request(
endpoint: .getUser(
username: user.username,
diff_since: user.updatedAt.timeIntervalSince1970 + 1,
emaciated: true
)
)!
)
let goalResponse = userResponse["goals"]
Expand All @@ -127,9 +130,13 @@ import SwiftyJSON
public func refreshGoal(_ goalID: NSManagedObjectID) async throws {
let goal = try modelContext.existingObject(with: goalID) as! Goal

let responseObject = try await requestManager.get(
url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)",
parameters: ["datapoints_count": "5", "emaciated": "true"]
let responseObject = try await requestManager.request(
endpoint: .getGoalDetails(
username: goal.owner.username,
goalname: goal.slug,
datapoints_count: 5,
emaciated: true
)
)
let goalJSON = JSON(responseObject!)

Expand All @@ -143,8 +150,8 @@ import SwiftyJSON
}

public func forceAutodataRefresh(_ goal: Goal) async throws {
let _ = try await requestManager.get(
url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)/refresh_graph.json"
let _ = try await requestManager.request(
endpoint: .requestAutodataFetch(username: goal.owner.username, goalname: goal.slug)
)
}

Expand Down
Loading
Loading