261 lines
9.5 KiB
Swift
261 lines
9.5 KiB
Swift
//
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
@objc(TSOutgoingMessageRecipientState)
|
|
public final class TSOutgoingMessageRecipientState: NSObject, NSSecureCoding, NSCopying {
|
|
/// The status of the outgoing message send to this recipient.
|
|
public private(set) var status: OWSOutgoingMessageRecipientStatus
|
|
|
|
/// Represents the time at which `status` was last updated.
|
|
///
|
|
/// - Important
|
|
/// This may be a locally-generated timestamp for `status` cases that refer
|
|
/// to local state, or a remote timestamp for `status` cases that refer to
|
|
/// to remote state.
|
|
///
|
|
/// For example, `status: .sending` will contain a locally-generated
|
|
/// timestamp, but `status: .read` will contain a timestamp pulled from a
|
|
/// read receipt. `status: .sent` may contain a local timestamp for
|
|
/// messages sent on this device, or a timestamp pulled from a sent message
|
|
/// transcript for messages sent on a linked device.
|
|
///
|
|
/// - Note
|
|
/// May be 0 for legacy recipient states depending on `status`. Legacy
|
|
/// recipient states only tracked a timestamp for `status` values of
|
|
/// `.delivered`, `.read.`, and `.viewed`.
|
|
public private(set) var statusTimestamp: UInt64
|
|
|
|
/// Whether the send used unidentified delivery, aka Sealed Sender.
|
|
///
|
|
/// - Note
|
|
/// This is always `false` until a send has been completed.
|
|
public var wasSentByUD: Bool
|
|
|
|
/// Represents an error that occurred during sending. Will only be present
|
|
/// if `canHaveErrorCode` is true.
|
|
public var errorCode: Int?
|
|
|
|
/// If true, this state supports errors. If false, it doesn't. The
|
|
/// `.sending` and `.pending` states may have transient failures that
|
|
/// haven't yet become terminal failures.
|
|
var canHaveErrorCode: Bool {
|
|
switch self.status {
|
|
case .sending, .pending, .failed:
|
|
return true
|
|
case .skipped, .sent, .delivered, .read, .viewed:
|
|
return false
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public convenience init(status: OWSOutgoingMessageRecipientStatus) {
|
|
self.init(
|
|
status: status,
|
|
statusTimestamp: Date().ows_millisecondsSince1970,
|
|
wasSentByUD: false,
|
|
errorCode: nil,
|
|
)
|
|
}
|
|
|
|
public init(
|
|
status: OWSOutgoingMessageRecipientStatus,
|
|
statusTimestamp: UInt64,
|
|
wasSentByUD: Bool,
|
|
errorCode: Int?,
|
|
) {
|
|
self.status = status
|
|
self.statusTimestamp = statusTimestamp
|
|
self.wasSentByUD = wasSentByUD
|
|
self.errorCode = errorCode
|
|
|
|
super.init()
|
|
}
|
|
|
|
func updateStatusIfPossible(
|
|
_ newStatus: OWSOutgoingMessageRecipientStatus,
|
|
statusTimestamp: UInt64 = Date().ows_millisecondsSince1970,
|
|
) {
|
|
if newStatus.priorityValue < self.status.priorityValue {
|
|
Logger.warn("Ignoring status update to '\(newStatus)' that would move backwards from '\(self.status)'")
|
|
return
|
|
}
|
|
self.status = newStatus
|
|
self.statusTimestamp = statusTimestamp
|
|
if !self.canHaveErrorCode {
|
|
self.errorCode = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - NSSecureCoding
|
|
|
|
fileprivate enum CoderKeys: String {
|
|
case status = "state"
|
|
case wasSentByUD
|
|
case statusTimestamp
|
|
case errorCode
|
|
}
|
|
|
|
public static var supportsSecureCoding: Bool { true }
|
|
|
|
public required init?(coder: NSCoder) {
|
|
guard
|
|
let statusRawValue = coder.decodeObject(of: NSNumber.self, forCoderKey: .status)?.uintValue,
|
|
let status = OWSOutgoingMessageRecipientStatus(rawValue: statusRawValue)
|
|
else {
|
|
owsFailDebug("Missing or unrecognized fields!")
|
|
return nil
|
|
}
|
|
|
|
if let statusTimestamp = coder.decodeObject(of: NSNumber.self, forCoderKey: .statusTimestamp) {
|
|
self.status = status
|
|
self.statusTimestamp = statusTimestamp.uint64Value
|
|
} else {
|
|
/// Legacy recipient states represented "delivered", "read", and
|
|
/// "viewed" by setting `status = .sent` and storing a dedicated
|
|
/// timestamp for each of those states. We use the presence of those
|
|
/// dedicated timestamps to migrate the legacy states to the current
|
|
/// representation, which uses more precise `status` cases along with a
|
|
/// general `statusTimestamp`.
|
|
if let viewedTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "viewedTimestamp") {
|
|
owsAssertDebug(status == .sent)
|
|
self.status = .viewed
|
|
self.statusTimestamp = viewedTimestamp.uint64Value
|
|
} else if let readTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "readTimestamp") {
|
|
owsAssertDebug(status == .sent)
|
|
self.status = .read
|
|
self.statusTimestamp = readTimestamp.uint64Value
|
|
} else if let deliveryTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "deliveryTimestamp") {
|
|
owsAssertDebug(status == .sent)
|
|
self.status = .delivered
|
|
self.statusTimestamp = deliveryTimestamp.uint64Value
|
|
} else {
|
|
self.status = status
|
|
self.statusTimestamp = 0
|
|
}
|
|
}
|
|
|
|
if let wasSentByUD = coder.decodeObject(of: NSNumber.self, forCoderKey: .wasSentByUD) as? Bool {
|
|
self.wasSentByUD = wasSentByUD
|
|
} else {
|
|
/// Truly old recipient states may not have had this property
|
|
/// serialized, so we'll default to `false` to match the zero-value
|
|
/// it would've gotten when deserialized.
|
|
self.wasSentByUD = false
|
|
}
|
|
|
|
self.errorCode = coder.decodeObject(of: NSNumber.self, forCoderKey: .errorCode)?.intValue
|
|
}
|
|
|
|
public func encode(with coder: NSCoder) {
|
|
coder.encode(NSNumber(value: status.rawValue), forCoderKey: .status)
|
|
coder.encode(NSNumber(value: statusTimestamp), forCoderKey: .statusTimestamp)
|
|
coder.encode(NSNumber(booleanLiteral: wasSentByUD), forCoderKey: .wasSentByUD)
|
|
if let errorCode {
|
|
coder.encode(NSNumber(value: errorCode), forCoderKey: .errorCode)
|
|
}
|
|
}
|
|
|
|
// MARK: - NSCopying
|
|
|
|
public func copy(with zone: NSZone? = nil) -> Any {
|
|
return Self(
|
|
status: status,
|
|
statusTimestamp: statusTimestamp,
|
|
wasSentByUD: wasSentByUD,
|
|
errorCode: errorCode,
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private extension NSCoder {
|
|
func decodeObject<DecodedObjectType: NSObject & NSSecureCoding>(
|
|
of cls: DecodedObjectType.Type,
|
|
forCoderKey key: TSOutgoingMessageRecipientState.CoderKeys,
|
|
) -> DecodedObjectType? {
|
|
return decodeObject(of: cls, forKey: key.rawValue)
|
|
}
|
|
|
|
func encode<EncodedObjectType: NSObject & NSSecureCoding>(
|
|
_ object: EncodedObjectType,
|
|
forCoderKey key: TSOutgoingMessageRecipientState.CoderKeys,
|
|
) {
|
|
encode(object, forKey: key.rawValue)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public enum OWSOutgoingMessageRecipientStatus: UInt, CustomDebugStringConvertible {
|
|
/// The message could not be sent to this recipient.
|
|
case failed = 0
|
|
/// The message is being sent (enqueued, uploading) to this recipient.
|
|
case sending = 1
|
|
/// The message was not sent to this recipient because they are invalid or
|
|
/// have already received the message via another channel. For example, the
|
|
/// same recipient may be on multiple story distribution lists, or may have
|
|
/// left a group we are sending to.
|
|
case skipped = 2
|
|
/// The message has been sent to the service.
|
|
case sent = 3
|
|
/// The message has been delivered to this recipient.
|
|
case delivered = 5
|
|
/// The message has been read by the recipient.
|
|
case read = 6
|
|
/// The message has been viewed by the recipient. This only applies to
|
|
/// messages that are "viewed" explicitly, such as view-once media and voice
|
|
/// messages.
|
|
case viewed = 7
|
|
/// The message was rejected by the server until some other condition is
|
|
/// satisfied. For example, the message send may be pending a CAPTCHA.
|
|
case pending = 4
|
|
|
|
/// A "priority" for a given status.
|
|
///
|
|
/// We can never move from a higher priority to a lower priority.
|
|
///
|
|
/// For example, once we've received a delivery receipt for a message, we
|
|
/// can never mark it as "sent". If we have a delivery receipt, that implies
|
|
/// that it was sent, and if we mark it as "sent", we'd lose information
|
|
/// indicating that it was delivered.
|
|
var priorityValue: Int {
|
|
switch self {
|
|
case .sending, .pending, .failed:
|
|
// We swap freely amongst these values as we send messages.
|
|
return 1
|
|
case .skipped:
|
|
// If we try to "skip" a message that was already "sent", it should remain
|
|
// "sent" because we *did* send it.
|
|
return 2
|
|
case .sent:
|
|
return 3
|
|
case .delivered:
|
|
return 4
|
|
case .read:
|
|
return 5
|
|
case .viewed:
|
|
return 6
|
|
}
|
|
}
|
|
|
|
public var debugDescription: String {
|
|
switch self {
|
|
case .failed: "failed"
|
|
case .sending: "sending"
|
|
case .skipped: "skipped"
|
|
case .sent: "sent"
|
|
case .delivered: "delivered"
|
|
case .read: "read"
|
|
case .viewed: "viewed"
|
|
case .pending: "pending"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This type is `Codable`-serialized by `StoryMessage`.
|
|
extension OWSOutgoingMessageRecipientStatus: Codable {}
|