Path: blob/main/SignalServiceKit/Messages/Interactions/Polls/OutgoingPollVote.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
public class OutgoingPollVoteMessage: TransientOutgoingMessage {
override public class var supportsSecureCoding: Bool { true }
public required init?(coder: NSCoder) {
guard
let targetPollAuthorAciBinary = coder.decodeObject(of: NSData.self, forKey: "targetPollAuthorAciBinary") as Data?,
let targetPollAuthorAci = try? Aci.parseFrom(serviceIdBinary: targetPollAuthorAciBinary)
else {
return nil
}
self.targetPollAuthorAci = targetPollAuthorAci
guard let targetPollTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "targetPollTimestamp") else {
return nil
}
self.targetPollTimestamp = targetPollTimestamp.uint64Value
guard let voteCount = coder.decodeObject(of: NSNumber.self, forKey: "voteCount") else {
return nil
}
self.voteCount = voteCount.uint32Value
guard let voteOptionIndexes = coder.decodeArrayOfObjects(ofClass: NSNumber.self, forKey: "voteOptionIndexes") else {
return nil
}
self.voteOptionIndexes = voteOptionIndexes.map(\.uint32Value)
super.init(coder: coder)
}
override public func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(self.targetPollAuthorAci.serviceIdBinary, forKey: "targetPollAuthorAciBinary")
coder.encode(NSNumber(value: self.targetPollTimestamp), forKey: "targetPollTimestamp")
coder.encode(NSNumber(value: self.voteCount), forKey: "voteCount")
coder.encode(self.voteOptionIndexes, forKey: "voteOptionIndexes")
}
override public var hash: Int {
var hasher = Hasher()
hasher.combine(super.hash)
hasher.combine(targetPollAuthorAci)
hasher.combine(targetPollTimestamp)
hasher.combine(voteCount)
hasher.combine(voteOptionIndexes)
return hasher.finalize()
}
override public func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Self else { return false }
guard super.isEqual(object) else { return false }
guard self.targetPollAuthorAci == object.targetPollAuthorAci else { return false }
guard self.targetPollTimestamp == object.targetPollTimestamp else { return false }
guard self.voteCount == object.voteCount else { return false }
guard self.voteOptionIndexes == object.voteOptionIndexes else { return false }
return true
}
let targetPollTimestamp: UInt64
let targetPollAuthorAci: Aci
let voteOptionIndexes: [UInt32]
let voteCount: UInt32
public init(
thread: TSThread,
targetPollTimestamp: UInt64,
targetPollAuthorAci: Aci,
voteOptionIndexes: [UInt32],
voteCount: UInt32,
tx: DBReadTransaction,
) {
self.targetPollTimestamp = targetPollTimestamp
self.targetPollAuthorAci = targetPollAuthorAci
self.voteOptionIndexes = voteOptionIndexes
self.voteCount = voteCount
super.init(
outgoingMessageWith: .withDefaultValues(thread: thread),
additionalRecipients: [],
explicitRecipients: [],
skippedRecipients: [],
transaction: tx,
)
}
override var contentHint: SealedSenderContentHint { .implicit }
override public func dataMessageBuilder(
with thread: TSThread,
transaction: DBReadTransaction,
) -> SSKProtoDataMessageBuilder? {
guard
let dataMessageBuilder = super.dataMessageBuilder(
with: thread,
transaction: transaction,
)
else {
return nil
}
let pollVoteBuilder = SSKProtoDataMessagePollVote.builder()
pollVoteBuilder.setTargetSentTimestamp(targetPollTimestamp)
pollVoteBuilder.setTargetAuthorAciBinary(targetPollAuthorAci.serviceIdBinary)
pollVoteBuilder.setOptionIndexes(voteOptionIndexes)
pollVoteBuilder.setVoteCount(voteCount)
dataMessageBuilder.setPollVote(
pollVoteBuilder.buildInfallibly(),
)
return dataMessageBuilder
}
override public func updateWithSendSuccess(tx: DBWriteTransaction) {
do {
try DependenciesBridge.shared.pollMessageManager.processPollVoteMessageDidSend(
targetPollTimestamp: targetPollTimestamp,
targetPollAuthorAci: targetPollAuthorAci,
optionIndexes: voteOptionIndexes,
voteCount: voteCount,
threadUniqueId: threadUniqueId,
tx: tx,
)
} catch {
Logger.error("Failed to update vote message as sent: \(error)")
}
}
override public 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 vote to some recipients")
return
}
guard
let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.aci,
let localRecipientId = DependenciesBridge.shared.recipientDatabaseTable.fetchRecipient(
serviceId: localAci,
transaction: tx,
)?.id
else {
owsFailDebug("Missing local aci or recipient")
return
}
Logger.error("Failed to send vote to all recipients.")
do {
guard
let targetMessage = try DependenciesBridge.shared.interactionStore.fetchMessage(
timestamp: targetPollTimestamp,
incomingMessageAuthor: targetPollAuthorAci == localAci ? nil : targetPollAuthorAci,
threadUniqueId: threadUniqueId,
transaction: tx,
),
let interactionId = targetMessage.grdbId?.int64Value
else {
Logger.error("Can't find target poll")
return
}
try PollStore().revertVoteCount(
voteCount: Int32(voteCount),
interactionId: interactionId,
voteAuthorId: localRecipientId,
transaction: tx,
)
SSKEnvironment.shared.databaseStorageRef.touch(interaction: targetMessage, shouldReindex: false, tx: tx)
} catch {
Logger.error("Failed to revert vote count: \(error)")
}
}
}