Path: blob/main/SignalServiceKit/Usernames/UsernameApiClientImpl.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import LibSignalClient
public class UsernameApiClientImpl: UsernameApiClient {
private let networkManager: NetworkManager
private let chatConnectionManager: ChatConnectionManager
init(
networkManager: NetworkManager,
chatConnectionManager: ChatConnectionManager,
) {
self.networkManager = networkManager
self.chatConnectionManager = chatConnectionManager
}
private func performRequest(
request: TSRequest,
) async throws -> HTTPResponse {
try await networkManager.asyncRequest(request)
}
// MARK: Selection
public func reserveUsernameCandidates(
usernameCandidates: Usernames.HashedUsername.GeneratedCandidates,
) async throws -> Usernames.ApiClientReservationResult {
let request = OWSRequestFactory.reserveUsernameRequest(
usernameHashes: usernameCandidates.candidateHashes,
)
do {
let response = try await performRequest(request: request)
guard response.responseStatusCode == 200 else {
throw response.asError()
}
guard let parser = response.responseBodyParamParser 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,
)
} catch {
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)!")
}
}
}
public func confirmReservedUsername(
reservedUsername: Usernames.HashedUsername,
encryptedUsernameForLink: Data,
chatServiceAuth: ChatServiceAuth,
) async throws -> Usernames.ApiClientConfirmationResult {
var request = OWSRequestFactory.confirmReservedUsernameRequest(
reservedUsernameHash: reservedUsername.hashString,
reservedUsernameZKProof: reservedUsername.proofString,
encryptedUsernameForLink: encryptedUsernameForLink,
)
request.auth = .identified(chatServiceAuth)
do {
let response = try await performRequest(request: request)
guard response.responseStatusCode == 200 else {
throw response.asError()
}
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unexpectedly missing JSON response body!")
}
let usernameLinkHandle: UUID = try parser.required(key: "usernameLinkHandle")
return .success(usernameLinkHandle: usernameLinkHandle)
} catch {
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)")
}
}
}
// MARK: Deletion
public func deleteCurrentUsername() async throws {
let request = OWSRequestFactory.deleteExistingUsernameRequest()
let response = try await performRequest(request: request)
guard response.responseStatusCode == 204 else {
throw response.asError()
}
}
// MARK: Lookup
public func lookupAci(
forHashedUsername hashedUsername: Usernames.HashedUsername,
) async throws -> Aci? {
try await chatConnectionManager.withUnauthService(.usernames) {
try await $0.lookUpUsernameHash(hashedUsername.rawHash)
}
}
// MARK: Links
public func setUsernameLink(
encryptedUsername: Data,
keepLinkHandle: Bool,
) async throws -> UUID {
let request = OWSRequestFactory.setUsernameLinkRequest(
encryptedUsername: encryptedUsername,
keepLinkHandle: keepLinkHandle,
)
let response = try await performRequest(request: request)
guard response.responseStatusCode == 200 else {
throw response.asError()
}
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unexpectedly missing JSON response body!")
}
return try parser.required(key: "usernameLinkHandle")
}
public func getUsernameLink(
handle: UUID,
entropy: Data,
) async throws -> LibSignalClient.Username? {
try await chatConnectionManager.withUnauthService(.usernames) {
try await $0.lookUpUsernameLink(handle, entropy: entropy)
}
}
}