Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Spam/SpamReportingUIUtils.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient
import SignalServiceKit
import SignalUI

enum ReportSpamUIUtils {
    /// Called only if the user reports spam.
    /// The `Bool` parameter represents if the thread was also blocked.
    typealias Completion = (Bool) -> Void

    static func showReportSpamActionSheet(
        _ thread: TSThread,
        isBlocked: Bool,
        from viewController: UIViewController,
        completion: Completion?,
    ) {
        let actionSheet = createReportSpamActionSheet(for: thread, isBlocked: isBlocked, completion: completion)
        viewController.presentActionSheet(actionSheet)
    }

    static func createReportSpamActionSheet(for thread: TSThread, isBlocked: Bool, completion: Completion? = nil) -> ActionSheetController {
        let actionSheetTitle = OWSLocalizedString(
            "MESSAGE_REQUEST_REPORT_CONVERSATION_TITLE",
            comment: "Action sheet title to confirm reporting a conversation as spam via a message request.",
        )
        let actionSheetMessage = OWSLocalizedString(
            "MESSAGE_REQUEST_REPORT_CONVERSATION_MESSAGE",
            comment: "Action sheet message to confirm reporting a conversation as spam via a message request.",
        )

        let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
        actionSheet.addAction(
            ActionSheetAction(
                title: OWSLocalizedString(
                    "MESSAGE_REQUEST_REPORT_SPAM_ACTION",
                    comment: "Action sheet action to confirm reporting a conversation as spam via a message request.",
                ),
                handler: { _ in
                    let spamReport = SSKEnvironment.shared.databaseStorageRef.write { tx in
                        return Self.buildSpamReport(in: thread, tx: tx)
                    }
                    Task {
                        try? await spamReport?.submit(using: SSKEnvironment.shared.networkManagerRef)
                    }
                    completion?(false)
                },
            ),
        )
        if !isBlocked, !thread.isTerminatedGroup {
            actionSheet.addAction(
                ActionSheetAction(
                    title: OWSLocalizedString(
                        "MESSAGE_REQUEST_BLOCK_AND_REPORT_SPAM_ACTION",
                        comment: "Action sheet action to confirm blocking and reporting spam for a thread via a message request.",
                    ),
                    handler: { _ in
                        let spamReport = SSKEnvironment.shared.databaseStorageRef.write { tx in
                            return Self.blockAndBuildSpamReport(in: thread, tx: tx)
                        }
                        Task {
                            try? await spamReport?.submit(using: SSKEnvironment.shared.networkManagerRef)
                        }
                        completion?(true)
                    },
                ),
            )
        }
        actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel))
        return actionSheet
    }

    static func successfulReportText(didBlock: Bool) -> String {
        if didBlock {
            OWSLocalizedString(
                "MESSAGE_REQUEST_SPAM_REPORTED_AND_BLOCKED",
                comment: "String indicating that spam has been reported and the chat has been blocked.",
            )
        } else {
            OWSLocalizedString(
                "MESSAGE_REQUEST_SPAM_REPORTED",
                comment: "String indicating that spam has been reported.",
            )
        }
    }

    static func blockAndBuildSpamReport(in thread: TSThread, tx: DBWriteTransaction) -> SpamReport? {
        SSKEnvironment.shared.blockingManagerRef.addBlockedThread(
            thread,
            blockMode: .local,
            shouldLeaveIfGroup: false,
            transaction: tx,
        )

        let result = Self._buildSpamReport(in: thread, tx: tx)

        SSKEnvironment.shared.syncManagerRef.sendMessageRequestResponseSyncMessage(
            thread: thread,
            responseType: .blockAndSpam,
            transaction: tx,
        )

        return result
    }

    static func buildSpamReport(in thread: TSThread, tx: DBWriteTransaction) -> SpamReport? {
        let result = Self._buildSpamReport(in: thread, tx: tx)

        SSKEnvironment.shared.syncManagerRef.sendMessageRequestResponseSyncMessage(
            thread: thread,
            responseType: .spam,
            transaction: tx,
        )

        return result
    }

    private static func _buildSpamReport(in thread: TSThread, tx: DBWriteTransaction) -> SpamReport? {
        var aci: Aci?
        var isGroup = false
        if let contactThread = thread as? TSContactThread {
            aci = contactThread.contactAddress.serviceId as? Aci
        } else if let groupThread = thread as? TSGroupThread {
            isGroup = true
            let accountManager = DependenciesBridge.shared.tsAccountManager
            guard let localIdentifiers = accountManager.localIdentifiers(tx: tx) else {
                owsFailDebug("Missing local identifiers")
                return nil
            }
            let groupMembership = groupThread.groupModel.groupMembership
            if let invitedAtServiceId = groupMembership.localUserInvitedAtServiceId(localIdentifiers: localIdentifiers) {
                aci = groupMembership.addedByAci(forInvitedMember: invitedAtServiceId)
            }
        } else {
            owsFailDebug("Unexpected thread type for reporting spam \(type(of: thread))")
            return nil
        }

        let infoMessage = TSInfoMessage(thread: thread, messageType: .reportedSpam)
        infoMessage.anyInsert(transaction: tx)

        guard let aci else {
            owsFailDebug("Missing ACI for reporting spam")
            return nil
        }

        // We only report a selection of the N most recent messages
        // in the conversation.
        let maxMessagesToReport = 3

        var guidsToReport = Set<String>()
        do {
            if isGroup {
                guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx) else {
                    owsFailDebug("Unable to find local identifiers")
                    return nil
                }
                try InteractionFinder(
                    threadUniqueId: thread.uniqueId,
                ).enumerateRecentGroupUpdateMessages(
                    transaction: tx,
                ) { infoMessage, stop in
                    guard let groupUpdateItems = infoMessage.computedGroupUpdateItems(localIdentifiers: localIdentifiers, tx: tx) else {
                        return
                    }

                    for item in groupUpdateItems {
                        if
                            let serverGuid = infoMessage.serverGuid,
                            let updaterAci = item.aciForSpamReporting,
                            updaterAci.wrappedValue == aci
                        {
                            guidsToReport.insert(serverGuid)
                        }
                    }
                    guard guidsToReport.count < maxMessagesToReport else {
                        stop = true
                        return
                    }
                }

            } else {
                try InteractionFinder(
                    threadUniqueId: thread.uniqueId,
                ).enumerateInteractionsForConversationView(
                    rowIdFilter: .newest,
                    tx: tx,
                ) { interaction -> Bool in
                    guard let incomingMessage = interaction as? TSIncomingMessage else { return true }
                    if let serverGuid = incomingMessage.serverGuid {
                        guidsToReport.insert(serverGuid)
                    }
                    if guidsToReport.count < maxMessagesToReport {
                        return true
                    }
                    return false
                }
            }
        } catch {
            owsFailDebug("Failed to lookup guids to report \(error)")
        }

        let reportingToken = SpamReportingTokenRecord.reportingToken(for: aci, database: tx.database)

        guard !guidsToReport.isEmpty else {
            Logger.warn("No messages with serverGuids to report.")
            return nil
        }

        return SpamReport(
            aci: aci,
            serverGuids: guidsToReport,
            reportingToken: reportingToken,
        )
    }
}