field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraGeneratedSubmissionOperation.swift (8435B)


      1 import Foundation
      2 import TeraKitBindings
      3 
      4 extension TeraSubmissionRequest {
      5   var generatedValue: FfiSubmissionReservationRequest {
      6     FfiSubmissionReservationRequest(schemaVersion: 1, commandId: commandID, scope: scope.generatedValue,
      7                                     composerId: composerID, expectedRevision: expectedRevision)
      8   }
      9 }
     10 
     11 extension TeraSubmissionMediaRequest {
     12   var generatedValue: FfiSubmissionMediaInput {
     13     FfiSubmissionMediaInput(schemaVersion: 1, request: request.generatedValue,
     14                             expectedRevision: expectedRevision, media: media.generatedValue)
     15   }
     16 }
     17 
     18 extension TeraGeneratedSubmission {
     19   static func request(_ value: FfiSubmissionReservationRequest) throws -> TeraSubmissionRequest {
     20     try version(value.schemaVersion)
     21     guard validID(value.commandId), validID(value.composerId), value.expectedRevision > 0,
     22           value.expectedRevision <= UInt64(Int64.max) else { throw mismatch() }
     23     return try TeraSubmissionRequest(commandID: value.commandId, scope: value.scope.composerAppValue,
     24                                      composerID: value.composerId, expectedRevision: value.expectedRevision)
     25   }
     26 
     27   static func operation(_ value: FfiSubmissionOperationRecord, expected: TeraSubmissionRequest) throws -> TeraSubmissionStatus {
     28     try version(value.schemaVersion)
     29     try version(value.settlement.schemaVersion)
     30     let actualRequest = try request(value.request)
     31     let captured = try value.captured.composerAppValue
     32     guard actualRequest == expected, validID(value.intentId), validID(value.operationId),
     33           value.revision > 0, value.revision <= UInt64(Int64.max),
     34           captured.scope == expected.scope, captured.id == expected.composerID,
     35           captured.revision == expected.expectedRevision, captured.editSequence > 0,
     36           value.committedAtUnixMs > 0, value.updatedAtUnixMs >= value.committedAtUnixMs,
     37           value.updatedAtUnixMs <= UInt64(Int64.max), value.media.count == captured.form.media.count,
     38           value.media.count <= 20 else { throw mismatch() }
     39     let media = try zip(value.media, captured.form.media).map { progress, source in
     40       guard progress.progress.schemaVersion == 2,
     41             progress.opaqueReference == source.opaqueReference,
     42             progress.progress.uploadUrl?.isEmpty == false,
     43             !progress.progress.url.isEmpty else { throw mismatch() }
     44       guard progress.authorizations.count <= 5 else { throw mismatch() }
     45       let attempts = try progress.authorizations.map { attempt in
     46         guard validID(attempt.operationId), attempt.expirationUnixS > 0,
     47               attempt.expirationUnixS <= UInt64(Int64.max) / 1000,
     48               attempt.revision.map({ $0 > 0 && $0 <= value.revision }) ?? true else { throw mismatch() }
     49         return TeraUploadAttemptIdentity(operationID: attempt.operationId, revision: attempt.revision,
     50                                          expirationUnixSeconds: attempt.expirationUnixS)
     51       }
     52       guard Set(attempts.map(\.operationID)).count == attempts.count else { throw mismatch() }
     53       return TeraSubmissionMedia(opaqueReference: progress.opaqueReference, progress: progress.progress.appValue, authorizations: attempts)
     54     }
     55     return try TeraSubmissionStatus(request: actualRequest, intentID: value.intentId, operationID: value.operationId,
     56                                     revision: value.revision, captured: captured, state: value.state.appValue,
     57                                     committedAtUnixMilliseconds: value.committedAtUnixMs,
     58                                     updatedAtUnixMilliseconds: value.updatedAtUnixMs,
     59                                     media: media, settlement: value.settlement.appValue,
     60                                     delivery: TeraPublicationEvidence.decode(value.delivery),
     61                                     targetDetails: TeraPublicationTargets.decode(value.targetDetails),
     62                                     retry: TeraPublicationRetry.decode(value.retry))
     63   }
     64 
     65   static func page(_ value: FfiSubmissionPageRecord, scope: TeraComposerScope, limit: UInt16) throws -> TeraSubmissionPage {
     66     try version(value.schemaVersion)
     67     guard try value.scope.composerAppValue == scope, value.entries.count <= Int(limit),
     68           limit > 0, limit <= 256, value.nextCursor?.isEmpty != true else { throw mismatch() }
     69     let entries: [TeraSubmissionEntry] = try value.entries.map { entry in
     70       switch entry {
     71       case let .submission(wire, reservationID, reservedAt, state):
     72         let request = try request(wire)
     73         guard request.scope == scope, validID(reservationID), reservedAt > 0,
     74               reservedAt <= UInt64(Int64.max) else { throw mismatch() }
     75         let summaryState = try summary(state)
     76         return .submission(TeraSubmissionSummary(request: request, reservationID: reservationID,
     77                                                  reservedAtUnixMilliseconds: reservedAt, state: summaryState))
     78       case let .repair(key, revision, reason):
     79         // A repair key is a locator, not authority for an editing or mutation API.
     80         guard key.utf8.count == 32, revision > 0, revision <= UInt64(Int64.max) else { throw mismatch() }
     81         let repair: TeraSubmissionRepairReason = switch reason {
     82         case .unsupportedSchema: .unsupportedSchema
     83         case .corruptRecord: .corruptRecord
     84         case .needsAttention: .needsAttention
     85         }
     86         return .repair(key: key, revision: revision, reason: repair)
     87       }
     88     }
     89     guard Set(entries.map(\.id)).count == entries.count else { throw mismatch() }
     90     return TeraSubmissionPage(scope: scope, entries: entries, nextCursor: value.nextCursor)
     91   }
     92 
     93   private static func summary(_ state: FfiSubmissionSummaryState) throws -> TeraSubmissionSummaryState {
     94     switch state {
     95     case .reserved: return .reserved
     96     case let .operation(intentID, operationID, revision, state):
     97       guard validID(intentID), validID(operationID), revision > 0,
     98             revision <= UInt64(Int64.max) else { throw mismatch() }
     99       return .operation(intentID: intentID, operationID: operationID, revision: revision, state: state.appValue)
    100     }
    101   }
    102 
    103   static func upload(_ value: FfiSubmissionUploadJobRecord, input: TeraSubmissionMediaRequest) throws -> TeraSubmissionUploadJob {
    104     guard value.schemaVersion == 2, !value.uploadUrl.isEmpty else { throw mismatch() }
    105     let status = try operation(value.submission, expected: input.request)
    106     guard input.expectedRevision < UInt64(Int64.max), status.revision == input.expectedRevision + 1,
    107           validID(value.operationId), value.expectedSha256 == input.media.media.sha256,
    108           value.mediaType == input.media.media.mediaType, value.byteSize == input.media.media.byteSize,
    109           value.authorizationHeader.hasPrefix("Nostr "),
    110           status.media.contains(where: { $0.opaqueReference == input.media.media.opaqueReference
    111               && $0.progress.url == value.remoteUrl && $0.progress.uploadURL == value.uploadUrl
    112               && $0.progress.stage == .uploading
    113           }) else { throw mismatch() }
    114     return TeraSubmissionUploadJob(submission: status, transfer: TeraNativeTransferJob(
    115       ownerID: status.intentID, expectedRevision: status.revision, operationID: value.operationId,
    116       remoteURL: value.remoteUrl, uploadURL: value.uploadUrl, authorizationHeader: value.authorizationHeader,
    117       expectedSHA256: value.expectedSha256, mediaType: value.mediaType, byteSize: value.byteSize
    118     ))
    119   }
    120 }
    121 
    122 extension FfiDraftMediaRecord {
    123   var appValue: TeraDraftMediaStatus {
    124     TeraDraftMediaStatus(
    125       url: url,
    126       stage: stage.appValue,
    127       uploadAttempts: uploadAttempts,
    128       verifiedAtUnixMilliseconds: verifiedAtUnixMs,
    129       possibleOrphan: possibleOrphan,
    130       orphanReasonCode: orphanReasonCode,
    131       orphanRecordedAtUnixMilliseconds: orphanRecordedAtUnixMs,
    132       uploadURL: schemaVersion == 2 ? uploadUrl : nil
    133     )
    134   }
    135 }
    136 
    137 extension FfiNativeUploadJobRecord {
    138   var appValue: TeraNativeUploadJob {
    139     get throws {
    140       guard schemaVersion == 2, !uploadUrl.isEmpty else { throw TeraGeneratedSubmission.mismatch() }
    141       return TeraNativeUploadJob(
    142         operationID: operationId,
    143         draft: draft.appValue,
    144         remoteURL: remoteUrl,
    145         uploadURL: uploadUrl,
    146         authorizationHeader: authorizationHeader,
    147         expectedSHA256: expectedSha256,
    148         mediaType: mediaType,
    149         byteSize: byteSize
    150       )
    151     }
    152   }
    153 }