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 }