Skip to content

Commit 5bdfb25

Browse files
committed
Add documentation for using AtProtoServer methods directly
1 parent 8683ee2 commit 5bdfb25

File tree

3 files changed

+175
-0
lines changed

3 files changed

+175
-0
lines changed

docs/docs/avoidingTheAgent.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
```

docs/docs/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
href: labels.md
5656
- name: Sending "raw" requests
5757
href: rawClient.md
58+
- name: Performing CRUD operations directly without an agent.
59+
href: avoidingTheAgent.md
5860
- name: Using the Jetstream
5961
href: jetstream.md
6062
- name: Configuring logging

idunno.Bluesky.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
</Folder>
4040
<Folder Name="/docs/docs/">
4141
<File Path="docs/docs/asp.net.md" />
42+
<File Path="docs/docs/avoidingTheAgent.md" />
4243
<File Path="docs/docs/commonTerms.md" />
4344
<File Path="docs/docs/connecting.md" />
4445
<File Path="docs/docs/conversationsAndMessages.md" />

0 commit comments

Comments
 (0)