Path: blob/main/SignalServiceKit/Messages/Interactions/Polls/OutgoingPollTerminate.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
class OutgoingPollTerminateMessage: TransientOutgoingMessage {
override class var supportsSecureCoding: Bool { true }
required init?(coder: NSCoder) {
guard let targetPollTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "targetPollTimestamp") else {
return nil
}
self.targetPollTimestamp = targetPollTimestamp.uint64Value
super.init(coder: coder)
}
override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(NSNumber(value: self.targetPollTimestamp), forKey: "targetPollTimestamp")
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(super.hash)
hasher.combine(targetPollTimestamp)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Self else { return false }
guard super.isEqual(object) else { return false }
guard self.targetPollTimestamp == object.targetPollTimestamp else { return false }
return true
}
let targetPollTimestamp: UInt64
init(
thread: TSThread,
targetPollTimestamp: UInt64,
expiresInSeconds: UInt32 = 0,
tx: DBReadTransaction,
) {
self.targetPollTimestamp = targetPollTimestamp
let builder: TSOutgoingMessageBuilder = .withDefaultValues(
thread: thread,
expiresInSeconds: expiresInSeconds,
)
super.init(
outgoingMessageWith: builder,
additionalRecipients: [],
explicitRecipients: [],
skippedRecipients: [],
transaction: tx,
)
}
override var contentHint: SealedSenderContentHint { .implicit }
override func dataMessageBuilder(
with thread: TSThread,
transaction: DBReadTransaction,
) -> SSKProtoDataMessageBuilder? {
guard
let dataMessageBuilder = super.dataMessageBuilder(
with: thread,
transaction: transaction,
)
else {
return nil
}
let pollTerminateBuilder = SSKProtoDataMessagePollTerminate.builder()
pollTerminateBuilder.setTargetSentTimestamp(targetPollTimestamp)
dataMessageBuilder.setPollTerminate(
pollTerminateBuilder.buildInfallibly(),
)
return dataMessageBuilder
}
override func updateWithAllSendingRecipientsMarkedAsFailed(
error: (any Error)? = nil,
transaction tx: DBWriteTransaction,
) {
super.updateWithAllSendingRecipientsMarkedAsFailed(error: error, transaction: tx)
revertLocalStateIfFailedForEveryone(tx: tx)
}
private func revertLocalStateIfFailedForEveryone(tx: DBWriteTransaction) {
// Do nothing if we successfully delivered to anyone. Only cleanup
// local state if we fail to deliver to anyone.
guard sentRecipientAddresses().isEmpty else {
Logger.warn("Failed to send poll terminate to some recipients")
return
}
Logger.error("Failed to send poll terminate to all recipients.")
do {
guard
let targetMessage = try DependenciesBridge.shared.interactionStore.fetchMessage(
timestamp: targetPollTimestamp,
incomingMessageAuthor: nil,
threadUniqueId: threadUniqueId,
transaction: tx,
),
let interactionId = targetMessage.grdbId?.int64Value
else {
Logger.error("Can't find target poll")
return
}
try PollStore().revertPollTerminate(interactionId: interactionId, transaction: tx)
// TODO: delete info message.
SSKEnvironment.shared.databaseStorageRef.touch(interaction: targetMessage, shouldReindex: false, tx: tx)
} catch {
Logger.error("Failed to revert poll terminate: \(error)")
}
}
}