Use atproto actions with ease in iOS shortcuts
1//
2// CreateARecordIntent.swift
3// shortcut
4//
5// Created by Bailey Townsend on 6/30/25.
6//
7
8import ATCommonWeb
9import ATProtoKit
10import AppIntents
11import SwiftData
12import SwiftUI
13
14struct PutARecordIntent: AppIntent {
15
16 @Parameter(
17 title: "AT Identifier",
18 description:
19 "The saved AT Identifier of the account you want to use to authenticate with and write the record to"
20 )
21 var atIdentifier: AtIdentifierAppEntity
22
23 @Parameter(
24 title: "Collection",
25 description: "The collection you want to write to, like app.bsky.feed.post", )
26 var collection: String
27
28 @Parameter(
29 title: "Record Key",
30 description:
31 "The record key for the new post, optional. A tid will be used if not provided",
32 default: nil)
33 var recordKey: String?
34
35 @Parameter(
36 title: "Should Validate",
37 description:
38 "You will probably not use this unless you are writing known atproto records. i.e. the ones found in the atproto repo",
39 default: false
40 )
41 var shouldValidate: Bool
42
43 @Parameter(
44 title: "Record",
45 description:
46 "This is most likely a Dictionary Variable, or a JSON file. But it is the supplied atproto record. We add the $type from the type parameter to this record",
47 )
48 var record: IntentFile
49
50 static let title: LocalizedStringResource = "Put a Record"
51
52 static let description: IntentDescription? =
53 "Either updates a record or create one if it doesn't exist"
54
55 static var parameterSummary: some ParameterSummary {
56 Summary(
57 "Update or create a \(\.$collection) record for \(\.$atIdentifier) with \(\.$record)"
58 ) {
59 \.$recordKey
60 \.$shouldValidate
61 }
62 }
63
64 func perform() async throws -> some ReturnsValue<StrongReferenceAppEntity> {
65
66 do {
67 let lowercaseType = self.collection.lowercased()
68
69 let decoder = JSONDecoder()
70
71 guard
72 case .dictionary(var dict) = try decoder.decode(
73 CodableValue.self, from: record.data)
74 else {
75 throw GenericIntentError.message("Could not parse JSON")
76 }
77 dict["$type"] = CodableValue(stringLiteral: lowercaseType)
78
79 let unknownRecord = UnknownType.unknown(dict)
80
81 let atProtoManager = AtProtocolManager()
82 var putRecordKey = ""
83 if let recordKey = self.recordKey {
84 if recordKey.isEmpty {
85 var generator = TIDGenerator()
86 putRecordKey = generator.generateTID()
87 } else {
88 putRecordKey = self.recordKey ?? ""
89 }
90 } else {
91 var generator = TIDGenerator()
92 putRecordKey = generator.generateTID()
93 }
94
95 let recordref = try await atProtoManager.putARecord(
96 sessionId: self.atIdentifier.id,
97 repositoryDID: self.atIdentifier.did.lowercased(),
98 collection: lowercaseType, recordKey: putRecordKey,
99 record: unknownRecord)
100
101 let strongRef: StrongReferenceAppEntity = StrongReferenceAppEntity()
102 strongRef.recordCID = recordref.recordCID
103 strongRef.recordURI = recordref.recordURI
104
105 return .result(
106 value: strongRef)
107 } catch let shortCutError as ShortcutErrors {
108 switch shortCutError {
109 case .NoSession:
110 throw GenericIntentError.message("No session found")
111 case .ErrorCreatingARecord(let errorMessage):
112 throw GenericIntentError.message(errorMessage)
113 case .AuthError(let authError):
114 throw GenericIntentError.message(authError)
115 }
116 } catch let shortCutError as GenericIntentError {
117 throw shortCutError
118
119 } catch _ as DecodingError {
120 throw GenericIntentError.message(
121 "There was an error decoding the record. Please make sure your record is valid JSON or use a shortcut dictionary."
122 )
123 } catch {
124 throw GenericIntentError.general
125 }
126 // return .result(dialog: "Okay, making a Bluesky Post.")
127 }
128}