Use atproto actions with ease in iOS shortcuts
1//
2// ListRecordsIntent.swift
3// shortcut
4//
5// Created by Bailey Townsend on 7/4/25.
6//
7
8import ATProtoKit
9import AppIntents
10import SwiftData
11import SwiftUI
12
13struct ListRecordsIntent: AppIntent {
14
15 @Parameter(
16 title: "Repo",
17 description: "The handle or DID of the repo"
18 )
19 var atIdentifier: String
20
21 @Parameter(
22 title: "Collection",
23 description: "The collection you want to write to, like app.bsky.feed.post")
24 var collection: String
25
26 @Parameter(
27 title: "Limit",
28 description: "The number of records to return. Between 2 and 100, defaults to 50",
29 default: nil)
30 var limit: Int?
31
32 @Parameter(
33 title: "Cursor",
34 description:
35 "This can be a Record Key to used as the last seen record top paginate the results",
36 default: nil
37 )
38 var cursor: String?
39
40 @Parameter(
41 title: "Reverse",
42 description:
43 "Flag to reverse the order of the returned records",
44 default: nil
45 )
46 var reverse: Bool?
47
48 static let title: LocalizedStringResource = "List Records"
49
50 static let description: IntentDescription = IntentDescription(
51 stringLiteral:
52 "List a range of records in a repository, matching a specific collection. Does not require auth"
53 )
54
55 static var parameterSummary: some ParameterSummary {
56 Summary("List \(\.$collection) records from \(\.$atIdentifier)") {
57 \.$limit
58 \.$cursor
59 \.$reverse
60 }
61 }
62
63 func perform() async throws -> some IntentResult & ReturnsValue<ListRecordsAppEntity> {
64 let atProtoManager = AtProtocolManager()
65 let lowerCaseCollection = self.collection.lowercased()
66 let lowerCaseAtIdentifier = self.atIdentifier.lowercased()
67 print(lowerCaseAtIdentifier)
68 let info = try await GetRemoteInfo.getRemoteRepoInfo(
69 possibleRepo: lowerCaseAtIdentifier, atProtoManager: atProtoManager)
70 if let limit = self.limit {
71 if limit < 2 {
72 throw GenericIntentError.message("Limit must be greater than or equal to 2")
73 }
74
75 if limit > 100 {
76 throw GenericIntentError.message("Limit cannot be greater than 100")
77 }
78 }
79
80 do {
81 let response = try await atProtoManager.listRecords(
82 pdsURL: info.pdsURL,
83 repository: info.repo,
84 collection: lowerCaseCollection,
85 limit: self.limit,
86 cursor: self.cursor,
87 isArrayReverse: self.reverse)
88
89 let result: ListRecordsAppEntity = ListRecordsAppEntity()
90 result.cursor = response.cursor
91 result.records = []
92 for record in response.records {
93 let recordAppEntity = RecordAppEntity()
94 recordAppEntity.uri = record.uri
95 recordAppEntity.cid = record.cid
96 if let attempt = try record.value?.toJSON() {
97 recordAppEntity.value = IntentFile(
98 data: attempt, filename: "\(record.uri).json")
99 }
100 result.records.append(recordAppEntity)
101 }
102
103 return .result(
104 value: result)
105 } catch let shortCutError as ShortcutErrors {
106 switch shortCutError {
107 case .NoSession:
108 throw GenericIntentError.message("No session found")
109 case .ErrorCreatingARecord(let errorMessage):
110 throw GenericIntentError.message(errorMessage)
111 case .AuthError(let authError):
112 throw MakeAPostIntentError.message(authError)
113 }
114 } catch let shortCutError as GenericIntentError {
115 throw shortCutError
116 } catch {
117 throw GenericIntentError.general
118 }
119 }
120
121}