329 lines
11 KiB
Swift
329 lines
11 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import LibSignalClient
|
|
|
|
public class UsernameApiClientImpl: UsernameApiClient {
|
|
private let networkManager: Shims.NetworkManager
|
|
private let schedulers: Schedulers
|
|
|
|
init(networkManager: Shims.NetworkManager, schedulers: Schedulers) {
|
|
self.networkManager = networkManager
|
|
self.schedulers = schedulers
|
|
}
|
|
|
|
private func performRequest<T>(
|
|
request: TSRequest,
|
|
onSuccess: @escaping (HTTPResponse) throws -> T,
|
|
onFailure: @escaping (Error) throws -> T
|
|
) -> Promise<T> {
|
|
firstly {
|
|
networkManager.makePromise(request: request)
|
|
}.map(on: schedulers.sharedUserInitiated) { response throws in
|
|
try onSuccess(response)
|
|
}.recover(on: schedulers.sharedUserInitiated) { error throws -> Promise<T> in
|
|
.value(try onFailure(error))
|
|
}
|
|
}
|
|
|
|
// MARK: Selection
|
|
|
|
public func reserveUsernameCandidates(
|
|
usernameCandidates: Usernames.HashedUsername.GeneratedCandidates
|
|
) -> Promise<Usernames.ApiClientReservationResult> {
|
|
let request = OWSRequestFactory.reserveUsernameRequest(
|
|
usernameHashes: usernameCandidates.candidateHashes
|
|
)
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws -> Usernames.ApiClientReservationResult {
|
|
guard response.responseStatusCode == 200 else {
|
|
throw OWSAssertionError(
|
|
"Unexpected status code from successful request: \(response.responseStatusCode)"
|
|
)
|
|
}
|
|
|
|
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
|
|
throw OWSAssertionError(
|
|
"Unexpectedly missing JSON response body!"
|
|
)
|
|
}
|
|
|
|
let usernameHash: String = try parser.required(key: "usernameHash")
|
|
|
|
guard let acceptedCandidate = usernameCandidates.candidate(matchingHash: usernameHash) else {
|
|
throw OWSAssertionError(
|
|
"Accepted username hash did not match any candidates!"
|
|
)
|
|
}
|
|
|
|
guard let parsedUsername = Usernames.ParsedUsername(rawUsername: acceptedCandidate.usernameString) else {
|
|
throw OWSAssertionError(
|
|
"Accepted username was not parseable!"
|
|
)
|
|
}
|
|
|
|
return .successful(
|
|
username: parsedUsername,
|
|
hashedUsername: acceptedCandidate
|
|
)
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws -> Usernames.ApiClientReservationResult {
|
|
guard let statusCode = error.httpStatusCode else {
|
|
throw error
|
|
}
|
|
|
|
switch statusCode {
|
|
case 422, 409:
|
|
// 422 indicates that the given hashes failed to validate.
|
|
//
|
|
// 409 indicates that none of the given hashes are available.
|
|
//
|
|
// Either way, the reservation has been rejected.
|
|
return .rejected
|
|
case 429:
|
|
return .rateLimited
|
|
default:
|
|
throw OWSAssertionError("Unexpected status code: \(statusCode)!")
|
|
}
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
|
|
public func confirmReservedUsername(
|
|
reservedUsername: Usernames.HashedUsername,
|
|
encryptedUsernameForLink: Data,
|
|
chatServiceAuth: ChatServiceAuth
|
|
) -> Promise<Usernames.ApiClientConfirmationResult> {
|
|
let request = OWSRequestFactory.confirmReservedUsernameRequest(
|
|
reservedUsernameHash: reservedUsername.hashString,
|
|
reservedUsernameZKProof: reservedUsername.proofString,
|
|
encryptedUsernameForLink: encryptedUsernameForLink
|
|
)
|
|
request.setAuth(chatServiceAuth)
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws -> Usernames.ApiClientConfirmationResult {
|
|
guard response.responseStatusCode == 200 else {
|
|
throw OWSAssertionError("Unexpected status code from successful request: \(response.responseStatusCode)")
|
|
}
|
|
|
|
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
|
|
throw OWSAssertionError("Unexpectedly missing JSON response body!")
|
|
}
|
|
|
|
let usernameLinkHandle: UUID = try parser.required(key: "usernameLinkHandle")
|
|
|
|
return .success(usernameLinkHandle: usernameLinkHandle)
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws -> Usernames.ApiClientConfirmationResult {
|
|
guard let statusCode = error.httpStatusCode else {
|
|
owsFailDebug("Unexpectedly missing HTTP status code!")
|
|
throw error
|
|
}
|
|
|
|
switch statusCode {
|
|
case 409, 410:
|
|
// 409 indicates that we do not actually hold the reservation
|
|
// we thought we did, either because we never did or because we
|
|
// have made a different reservation since.
|
|
//
|
|
// 410 indicates that our reservation has lapsed, and another
|
|
// account has snagged the username - or that the reservation
|
|
// token is invalid.
|
|
//
|
|
// Either way, we've been rejected.
|
|
return .rejected
|
|
case 429:
|
|
return .rateLimited
|
|
default:
|
|
throw OWSAssertionError("Unexpected status code: \(statusCode)")
|
|
}
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
|
|
// MARK: Deletion
|
|
|
|
public func deleteCurrentUsername() -> Promise<Void> {
|
|
let request = OWSRequestFactory.deleteExistingUsernameRequest()
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws {
|
|
guard response.responseStatusCode == 204 else {
|
|
throw OWSAssertionError("Unexpected status code from successful request: \(response.responseStatusCode)")
|
|
}
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws {
|
|
throw error
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
|
|
// MARK: Lookup
|
|
|
|
public func lookupAci(
|
|
forHashedUsername hashedUsername: Usernames.HashedUsername
|
|
) -> Promise<Aci?> {
|
|
let request = OWSRequestFactory.lookupAciUsernameRequest(
|
|
usernameHashToLookup: hashedUsername.hashString
|
|
)
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws -> Aci {
|
|
guard response.responseStatusCode == 200 else {
|
|
throw OWSAssertionError("Unexpected response code: \(response.responseStatusCode)")
|
|
}
|
|
|
|
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
|
|
throw OWSAssertionError("Unexpectedly missing JSON response body!")
|
|
}
|
|
|
|
let aciUuid: UUID = try parser.required(key: "uuid")
|
|
|
|
return Aci(fromUUID: aciUuid)
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws -> Aci? {
|
|
guard let statusCode = error.httpStatusCode else {
|
|
owsFailDebug("Unexpectedly missing HTTP status code!")
|
|
throw error
|
|
}
|
|
|
|
switch statusCode {
|
|
case 404:
|
|
// If the requested username does not belong to any accounts,
|
|
// we get a 404.
|
|
return nil
|
|
default:
|
|
throw error
|
|
}
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
|
|
// MARK: Links
|
|
|
|
public func setUsernameLink(
|
|
encryptedUsername: Data,
|
|
keepLinkHandle: Bool
|
|
) -> Promise<UUID> {
|
|
let request = OWSRequestFactory.setUsernameLinkRequest(
|
|
encryptedUsername: encryptedUsername,
|
|
keepLinkHandle: keepLinkHandle
|
|
)
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws -> UUID {
|
|
guard response.responseStatusCode == 200 else {
|
|
throw OWSAssertionError("Unexpected response code: \(response.responseStatusCode)")
|
|
}
|
|
|
|
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
|
|
throw OWSAssertionError("Unexpectedly missing JSON response body!")
|
|
}
|
|
|
|
return try parser.required(key: "usernameLinkHandle")
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws -> UUID {
|
|
throw error
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
|
|
public func getUsernameLink(handle: UUID) -> Promise<Data?> {
|
|
let request = OWSRequestFactory.lookupUsernameLinkRequest(handle: handle)
|
|
|
|
func onRequestSuccess(response: HTTPResponse) throws -> Data {
|
|
guard response.responseStatusCode == 200 else {
|
|
throw OWSAssertionError("Unexpected response code: \(response.responseStatusCode)")
|
|
}
|
|
|
|
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
|
|
throw OWSAssertionError("Unexpectedly missing JSON response body!")
|
|
}
|
|
|
|
let encryptedUsernameString: String = try parser.required(
|
|
key: "usernameLinkEncryptedValue"
|
|
)
|
|
|
|
return try Data.data(fromBase64Url: encryptedUsernameString)
|
|
}
|
|
|
|
func onRequestFailure(error: Error) throws -> Data? {
|
|
guard let statusCode = error.httpStatusCode else {
|
|
owsFailDebug("Unexpectedly missing HTTP status code!")
|
|
throw error
|
|
}
|
|
|
|
switch statusCode {
|
|
case 404:
|
|
// If the requested handle does not belong to any username link,
|
|
// we get a 404.
|
|
return nil
|
|
default:
|
|
throw error
|
|
}
|
|
}
|
|
|
|
return performRequest(
|
|
request: request,
|
|
onSuccess: onRequestSuccess,
|
|
onFailure: onRequestFailure
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Shims
|
|
|
|
extension UsernameApiClientImpl {
|
|
enum Shims {
|
|
typealias NetworkManager = _UsernameApiClientImpl_NetworkManager_Shim
|
|
}
|
|
|
|
enum Wrappers {
|
|
typealias NetworkManager = _UsernameApiClientImpl_NetworkManager_Wrapper
|
|
}
|
|
}
|
|
|
|
protocol _UsernameApiClientImpl_NetworkManager_Shim {
|
|
func makePromise(request: TSRequest) -> Promise<HTTPResponse>
|
|
}
|
|
|
|
class _UsernameApiClientImpl_NetworkManager_Wrapper: _UsernameApiClientImpl_NetworkManager_Shim {
|
|
private let networkManager: NetworkManager
|
|
|
|
init(networkManager: NetworkManager) {
|
|
self.networkManager = networkManager
|
|
}
|
|
|
|
func makePromise(request: TSRequest) -> Promise<HTTPResponse> {
|
|
return networkManager.makePromise(request: request)
|
|
}
|
|
}
|