|
| 1 | +# Avoiding the Agent |
| 2 | + |
| 3 | +The agents are meant as the main way to interact with Bluesky. However, there are scenarios where you might want to avoid |
| 4 | +using agents, and talk to the PDS directly, bypassing an app view or other intermediate API. |
| 5 | + |
| 6 | +The `AtProtoServer` class has methods to get, list create, update and delete records directly on a PDS, given appropriate credentials. |
| 7 | + |
| 8 | +This approach entails you discovering the resolve a user handle to a DID, then discovering the PDS endpoint for a user. At that point |
| 9 | +you can read and list records directly from that PDS. To create, update and delete records you will need to handle you must |
| 10 | +authenticate with that PDS to get a session, create access credentials from the session, then using the access credentials |
| 11 | +you can create, update and delete directly records on the PDS. You will also need to manually refresh sessions as they expire. |
| 12 | + |
| 13 | +The [idunno.AtProto.Lexicons](https://github.com/blowdart/idunno.AtProto.Lexcions) library aims to provide common third party |
| 14 | +record types as C# records, so you can work with strongly typed records. |
| 15 | + |
| 16 | +To discover the PDS endpoint for a user you first resolve the DID from the user handle, then resolve the PDS for the DID. |
| 17 | + |
| 18 | +```c# |
| 19 | +sring userHandle = "example.bsky.social"; |
| 20 | + |
| 21 | +var did = await idunno.AtProto.Resolution.ResolveHandle(userHandle, cancellationToken: cancellationToken); |
| 22 | +if (did is null) |
| 23 | +{ |
| 24 | + // Handle is invalid, error appropriately. |
| 25 | +} |
| 26 | + |
| 27 | +// Get the PDS for the DID. |
| 28 | +var pds = await idunno.AtProto.Resolution.ResolvePds(did, cancellationToken: cancellationToken); |
| 29 | +if (pds is null) |
| 30 | +{ |
| 31 | + // PDS could not be resolved, error appropriately. |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +Before you can use the `AtProtoServer` class you need to create a c# `HttpClient`. |
| 36 | + |
| 37 | +```c# |
| 38 | +var httpClientHandler = new HttpClientHandler() |
| 39 | +{ |
| 40 | + AutomaticDecompression = DecompressionMethods.All, |
| 41 | + UseCookies = false, |
| 42 | +}; |
| 43 | + |
| 44 | +var httpClient = new HttpClient(httpClientHandler); |
| 45 | +``` |
| 46 | + |
| 47 | +At this point you can use the `AtProtoServer` class to read and list records directly from the PDS, if you have a record definition. |
| 48 | +If you want to use raw JSON please see the [Sending raw AT Protocol requests](rawClient.md). |
| 49 | + |
| 50 | +For these examples we will use the [statusphere.xyz](https://statusphere.xyz) sample records, |
| 51 | +which are defined in the [idunno.AtProto.Lexicons](https://github.com/blowdart/idunno.AtProto.Lexicons) library. |
| 52 | + |
| 53 | +Assuming you have adding the `idunno.AtProto.Lexicons` nupkg, and added the appropriate `using` statements you could |
| 54 | +list the statusphere status for a user like this: |
| 55 | + |
| 56 | +```c# |
| 57 | +var listResult = await AtProtoServer.ListRecords<StatusphereStatus>( |
| 58 | + repo: did, |
| 59 | + collection: StatusphereConstants.Collection, |
| 60 | + limit: 25, |
| 61 | + cursor: null, |
| 62 | + reverse : false, |
| 63 | + accessCredentials: null, |
| 64 | + service: pds, |
| 65 | + httpClient: httpClient); |
| 66 | +``` |
| 67 | + |
| 68 | +You'll note that the AtProtoServer methods require you to specify a lot of parameters, and very few are optional. This is deliberate, |
| 69 | +the assumption is if you are using the `AtProtoServer` class directly you are likely building your own higher level abstraction on top of it. |
| 70 | + |
| 71 | +To retrieve an individual record you use `GetRecord`. This required credentials. So you would first need to authenticate with the PDS, |
| 72 | +via the `CreateSession` method, then create access credentials from the session. |
| 73 | + |
| 74 | +```c# |
| 75 | +AtProtoAccessCredentials? accessCredentials; |
| 76 | +var createSessionResult = await AtProtoServer.CreateSession( |
| 77 | + service: pds, |
| 78 | + identifier: userHandle, |
| 79 | + password: password, |
| 80 | + authFactorToken: null, |
| 81 | + authFactorToken: authCode, |
| 82 | + httpClient: httpClient); |
| 83 | + |
| 84 | +if (createSessionResult.Succeeded) |
| 85 | +{ |
| 86 | + accessCredentials = createSessionResult.Result.ToAccessCredentials(); |
| 87 | +} |
| 88 | +else |
| 89 | +{ |
| 90 | + // Handle error appropriately. |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +You must check the `CreateSessionResult` to ensure the session was created successfully. If a user has 2FA enabled |
| 95 | +the `CreateSession` call will fail and the `AtErrorDetails` property will have an `Error` value of `AuthFactorCodeRequired`. |
| 96 | +If that is returned you will need prompt the user for, and provide the MFA token in the `authFactorToken` parameter. |
| 97 | + |
| 98 | +Once you have the access credentials you perform create, update and delete operations. For example, to create a new |
| 99 | +statusphere status record with `CreateRecord` you would do: |
| 100 | +```c# |
| 101 | + |
| 102 | +var status = new StatusphereStatus |
| 103 | +{ |
| 104 | + Status = "😁" |
| 105 | +}; |
| 106 | + |
| 107 | +var createRecordResult = await AtProtoServer.CreateRecord( |
| 108 | + record: status, |
| 109 | + creator: did, |
| 110 | + collection: StatusphereConstants.Collection, |
| 111 | + rKey: TimestampIdentifier.Next(), |
| 112 | + validate: false, |
| 113 | + swapCommit: null, |
| 114 | + service: pds, |
| 115 | + accessCredentials: accessCredentials, |
| 116 | + httpClient: httpClient); |
| 117 | +``` |
| 118 | + |
| 119 | +When creating records it is typical to use `TimestampIdentifier.Next()` for the `rKey` parameter. This produces a unique |
| 120 | +record key based on the current timestamp. Some applications may use "self" as a record key to identify a record of which a single |
| 121 | +instance is being created (such as a user profile), or completely custom record keys. |
| 122 | + |
| 123 | +To get a record with `GetRecord` you need the repo (the user's DID), the collection, and the record key. For example, to get the record we just created |
| 124 | + |
| 125 | +```c# |
| 126 | +var getRecordResult = await AtProtoServer.GetRecord<StatusphereStatus>( |
| 127 | + repo: did, |
| 128 | + collection: StatusphereConstants.Collection, |
| 129 | + rKey: createRecordResult.Result.Uri.RecordKey!, |
| 130 | + cid: null, // get the latest version, |
| 131 | + service: pds, |
| 132 | + accessCredentials: null, |
| 133 | + httpClient: httpClient, |
| 134 | + cancellationToken: cancellationToken); |
| 135 | +``` |
| 136 | + |
| 137 | +The `PutRecord` method allows you to update an existing record. |
| 138 | +```c# |
| 139 | +// Update the record we just retrieved. |
| 140 | +StatusphereStatus statusToUpdate = getRecordResult.Result.Value; |
| 141 | +statusToUpdate.Status = "😎"; |
| 142 | + |
| 143 | +var putRecordResult = await AtProtoServer.PutRecord( |
| 144 | + record: statusToUpdate, |
| 145 | + collection: StatusphereConstants.Collection, |
| 146 | + creator: did, |
| 147 | + rKey: getRecordResult.Result.Uri.RecordKey!, |
| 148 | + validate: false, |
| 149 | + swapCommit: null, |
| 150 | + swapRecord: getRecordResult.Result.Cid, |
| 151 | + service: pds, |
| 152 | + accessCredentials: accessCredentials); |
| 153 | +``` |
| 154 | + |
| 155 | +Here you can use the swapRecord parameter to ensure you are updating the version of the record you think you are. If |
| 156 | +the record has been updated since you retrieved it the update will fail. |
| 157 | + |
| 158 | +Finally, to delete the record you just created you would do: |
| 159 | + |
| 160 | +```c# |
| 161 | +var deleteRecordResult = await AtProtoServer.DeleteRecord( |
| 162 | + repo: did, |
| 163 | + collection: StatusphereConstants.Collection, |
| 164 | + rKey: createResult.Result.Uri.RecordKey!, |
| 165 | + swapCommit: null, |
| 166 | + swapRecord: null, |
| 167 | + service: pds, |
| 168 | + accessCredentials: accessCredentials, |
| 169 | + httpClient: httpClient, |
| 170 | + loggerFactory: loggerFactory, |
| 171 | + cancellationToken: cancellationToken); |
| 172 | +``` |
0 commit comments