Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Profiles/SignalServiceProfile.swift
1 views
//
// 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 {
        fileprivate static let dummyCapabilityKey = "dummy"

        /// A dummy capability that keeps this struct non-empty. If it were
        /// empty, the compiler would complain about much of the code in here
        /// being unused...which is true! But, we want to keep it around for the
        /// future, when we may add new capabilities.
        public let dummyCapability: 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, params: ParamParser) throws -> SignalServiceProfile {
        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(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, let 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 capabilitiesDict: [String: Any] = try params.required(key: "capabilities") else {
            throw ValidationError(description: "Missing or invalid capabilities JSON!")
        }

        return Capabilities(
            dummyCapability: parseCapabilityFlag(
                capabilitiesParser: ParamParser(capabilitiesDict),
                capabilityKey: Capabilities.dummyCapabilityKey,
            ),
        )
    }

    /// 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 {
        if capabilityKey == Capabilities.dummyCapabilityKey {
            return false
        }

        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
        }
    }
}