TM-SGNL-iOS/SignalServiceKit/Network/API/SignalServiceProfile.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

169 lines
7.4 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
/// Represents a user's profile as fetched from the service.
///
/// All non-capability fields are encrypted, and if present should be decrypted
/// using this user's profile key.
public class SignalServiceProfile {
private struct ValidationError: Error, CustomStringConvertible {
let description: String
}
public struct Capabilities {
public let deleteSync: Bool
public let storageServiceRecordIkm: Bool
}
public let serviceId: ServiceId
public let identityKey: IdentityKey
public let profileNameEncrypted: Data?
public let bioEncrypted: Data?
public let bioEmojiEncrypted: Data?
public let avatarUrlPath: String?
public let paymentAddressEncrypted: Data?
public let unidentifiedAccessVerifier: Data?
public let hasUnrestrictedUnidentifiedAccess: Bool
public let credential: Data?
public let badges: [(OWSUserProfileBadgeInfo, ProfileBadge)]
public let phoneNumberSharingEncrypted: Data?
public let capabilities: Capabilities
private init(
serviceId: ServiceId,
identityKey: IdentityKey,
profileNameEncrypted: Data?,
bioEncrypted: Data?,
bioEmojiEncrypted: Data?,
avatarUrlPath: String?,
paymentAddressEncrypted: Data?,
unidentifiedAccessVerifier: Data?,
hasUnrestrictedUnidentifiedAccess: Bool,
credential: Data?,
badges: [(OWSUserProfileBadgeInfo, ProfileBadge)],
phoneNumberSharingEncrypted: Data?,
capabilities: Capabilities
) {
self.serviceId = serviceId
self.identityKey = identityKey
self.profileNameEncrypted = profileNameEncrypted
self.bioEncrypted = bioEncrypted
self.bioEmojiEncrypted = bioEmojiEncrypted
self.avatarUrlPath = avatarUrlPath
self.paymentAddressEncrypted = paymentAddressEncrypted
self.unidentifiedAccessVerifier = unidentifiedAccessVerifier
self.hasUnrestrictedUnidentifiedAccess = hasUnrestrictedUnidentifiedAccess
self.credential = credential
self.badges = badges
self.phoneNumberSharingEncrypted = phoneNumberSharingEncrypted
self.capabilities = capabilities
}
public static func fromResponse(serviceId: ServiceId, responseObject: Any?) throws -> SignalServiceProfile {
guard let params = ParamParser(responseObject: responseObject) else {
throw ValidationError(description: "Invalid response JSON!")
}
do {
let identityKey = try IdentityKey(bytes: try params.requiredBase64EncodedData(key: "identityKey"))
let profileNameEncrypted = try params.optionalBase64EncodedData(key: "name")
let bioEncrypted = try params.optionalBase64EncodedData(key: "about")
let bioEmojiEncrypted = try params.optionalBase64EncodedData(key: "aboutEmoji")
let avatarUrlPath: String? = try params.optional(key: "avatar")
let paymentAddressEncrypted = try params.optionalBase64EncodedData(key: "paymentAddress")
let unidentifiedAccessVerifier = try params.optionalBase64EncodedData(key: "unidentifiedAccess")
let hasUnrestrictedUnidentifiedAccess: Bool = try params.optional(key: "unrestrictedUnidentifiedAccess") ?? false
let credential = try params.optionalBase64EncodedData(key: "credential")
let badges: [(OWSUserProfileBadgeInfo, ProfileBadge)] = try parseBadges(params: params)
let phoneNumberSharingEncrypted = try params.optionalBase64EncodedData(key: "phoneNumberSharing")
let capabilities: Capabilities = try parseCapabilities(params: params)
return SignalServiceProfile(
serviceId: serviceId,
identityKey: identityKey,
profileNameEncrypted: profileNameEncrypted,
bioEncrypted: bioEncrypted,
bioEmojiEncrypted: bioEmojiEncrypted,
avatarUrlPath: avatarUrlPath,
paymentAddressEncrypted: paymentAddressEncrypted,
unidentifiedAccessVerifier: unidentifiedAccessVerifier,
hasUnrestrictedUnidentifiedAccess: hasUnrestrictedUnidentifiedAccess,
credential: credential,
badges: badges,
phoneNumberSharingEncrypted: phoneNumberSharingEncrypted,
capabilities: capabilities
)
} catch let error {
throw ValidationError(description: "Failed to parse profile JSON: \(error)")
}
}
private static func parseBadges(params: ParamParser) throws -> [(OWSUserProfileBadgeInfo, ProfileBadge)] {
if let badgeArray: [[String: Any]] = try params.optional(key: "badges") {
return try badgeArray.compactMap { badgeDict in
let badgeParams = ParamParser(dictionary: badgeDict)
let isVisible: Bool? = try badgeParams.optional(key: "visible")
let expiration: TimeInterval? = try badgeParams.optional(key: "expiration")
let expirationMills = expiration.flatMap { UInt64($0 * 1000) }
let badge = try ProfileBadge(jsonDictionary: badgeDict)
let badgeMetadata: OWSUserProfileBadgeInfo
if let expirationMills = expirationMills, let isVisible = isVisible {
badgeMetadata = OWSUserProfileBadgeInfo(badgeId: badge.id, expiration: expirationMills, isVisible: isVisible)
} else {
badgeMetadata = OWSUserProfileBadgeInfo(badgeId: badge.id)
}
return (badgeMetadata, badge)
}
} else {
return []
}
}
private static func parseCapabilities(params: ParamParser) throws -> Capabilities {
guard
let capabilitiesJson: Any? = try params.required(key: "capabilities"),
let capabilitiesParser = ParamParser(responseObject: capabilitiesJson)
else {
throw ValidationError(description: "Missing or invalid capabilities JSON!")
}
return Capabilities(
deleteSync: parseCapabilityFlag(
capabilitiesParser: capabilitiesParser,
capabilityKey: AccountAttributes.Capabilities.CodingKeys.deleteSyncSendSupport.rawValue
),
storageServiceRecordIkm: parseCapabilityFlag(
capabilitiesParser: capabilitiesParser,
capabilityKey: AccountAttributes.Capabilities.CodingKeys.storageServiceRecordIkm.rawValue
)
)
}
/// Parse a boolean capability with the given key from the given parser.
/// - Important
/// If the capability is missing (or weirdly fails to parse), we assume it
/// was removed from the service and is therefore default-true.
private static func parseCapabilityFlag(
capabilitiesParser: ParamParser,
capabilityKey: String
) -> Bool {
do {
guard let capabilityFlag: Bool = try capabilitiesParser.optional(key: capabilityKey) else {
owsFailDebug("Missing capability \(capabilityKey)! Assuming retired from service, and therefore hardcoded-on.")
return true
}
return capabilityFlag
} catch {
owsFailDebug("Failed to parse capability \(capabilityKey)! Hardcoding to true.")
return true
}
}
}