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 ATProtoKit
9import AppIntents
10import SwiftData
11import SwiftUI
12
13struct UpdateProfileIntent: AppIntent {
14
15 @Parameter(
16 title: "AT Identifier",
17 description:
18 "The saved AT Identifier of the account you want to use to authenticate with and update the profile of",
19 requestValueDialog: IntentDialog("Choose a logged in AT Identifier"))
20 public var atIdentifier: AtIdentifierAppEntity
21
22 @Parameter(
23 title: "Display Name",
24 description:
25 "Enter a value if you would like to update your display name. Leave empty to not update your display name"
26 )
27 var displayName: String?
28
29 @Parameter(
30 title: "Description",
31 description:
32 "Enter a value if you would like to update your profile's description. Leave empty to not update your profile's description",
33 default: nil)
34 var description: String?
35
36 @Parameter(
37 title: "Profile Picture",
38 description: "Updates your profile picture if provided, if not keeps the current one",
39 supportedContentTypes: [.jpeg, .png, .heic])
40 var profilePic: IntentFile?
41
42 @Parameter(
43 title: "Banner Picture",
44 description: "Updates your banner picture if provided, if not keeps the current one",
45 supportedContentTypes: [.jpeg, .png, .heic])
46 var bannerPic: IntentFile?
47
48 static let title: LocalizedStringResource = "Update your Bluesky profile"
49
50 static let description: IntentDescription =
51 "Updates your Bluesky profile with the provided data. If the field does not have a value it keeps the current value"
52
53 static var parameterSummary: some ParameterSummary {
54 Summary("Update \(\.$atIdentifier)'s profile") {
55 \.$displayName
56 \.$description
57 \.$profilePic
58 \.$bannerPic
59 }
60 }
61
62 func perform() async throws -> some ReturnsValue<StrongReferenceAppEntity> {
63
64 var profileUpdates: [ATProtoBluesky.UpdatedProfileRecordField] = []
65
66 if let displayName = self.displayName {
67 profileUpdates.append(.displayName(with: displayName))
68 }
69
70 if let description = self.description {
71 profileUpdates.append(.description(with: description))
72 }
73
74 if let profilePic = self.profilePic {
75 let imageQuery = try await IntentFileToImageQuery(files: [profilePic], altText: [])
76 profileUpdates.append(.avatarImage(with: imageQuery.first))
77 }
78
79 if let bannerPic = self.bannerPic {
80 let imageQuery = try await IntentFileToImageQuery(files: [bannerPic], altText: [])
81 profileUpdates.append(.bannerImage(with: imageQuery.first))
82 }
83
84 if profileUpdates.isEmpty {
85 throw GenericIntentError.message("No updates provided")
86 }
87
88 let atProtoManager = AtProtocolManager()
89
90 do {
91 let recordref = try await atProtoManager.updateProfile(
92 sessionId: self.atIdentifier.id, usersDID: self.atIdentifier.did,
93 updates: profileUpdates)
94
95 let strongRef: StrongReferenceAppEntity = StrongReferenceAppEntity()
96 strongRef.recordCID = recordref.recordCID
97 strongRef.recordURI = recordref.recordURI
98
99 return .result(
100 value: strongRef)
101 } catch let shortCutError as ShortcutErrors {
102 switch shortCutError {
103 case .NoSession:
104 throw GenericIntentError.message("No session found")
105 case .ErrorCreatingARecord(let errorMessage):
106 throw GenericIntentError.message(errorMessage)
107 case .AuthError(let authError):
108 throw GenericIntentError.message(authError)
109 }
110 } catch let shortCutError as GenericIntentError {
111 throw shortCutError
112 } catch {
113 throw GenericIntentError.general
114 }
115 }
116}