Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Deploy Docs

on:
push:
branches: [ "main" ]
paths:
- 'docs/**'
- 'src/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
actions: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build-docs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Install docfx
run: dotnet tool install -g docfx

- name: Build docs
run: docfx docs/docfx.json

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs/_site'

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,8 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp

.serena/
.serena/

# DocFX generated site
docs/_site/
docs/api/
5 changes: 5 additions & 0 deletions docs/carpanet/public/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#logo
{
margin-right: 10px;
border-radius: 5px;
}
52 changes: 52 additions & 0 deletions docs/docfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"metadata": [
{
"src": [
{
"src": "..",
"files": [
"src/CarpaNet/*.csproj",
"src/CarpaNet.OAuth/*.csproj",
"src/CarpaNet.Jetstream/*.csproj",
"src/CarpaNet.AspNetCore/*.csproj"
]
}
],
"dest": "api",
"outputFormat": "apiPage"
}
],
"build": {
"content": [
{
"files": [
"**/*.{md,yml}"
],
"exclude": [
"_site/**"
]
}
],
"resource": [
{
"files": [
"images/**",
"favicon.png"
]
}
],
"output": "_site",
"template": [
"default",
"modern",
"carpanet"
],
"postProcessors": [ "ExtractSearchIndex" ],
"globalMetadata": {
"_appName": "CarpaNet",
"_appTitle": "CarpaNet",
"_enableSearch": true,
"_appFaviconPath": "favicon.png"
}
}
}
44 changes: 44 additions & 0 deletions docs/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Authentication

CarpaNet supports two authentication methods: app passwords (for scripts and bots) and [OAuth 2.0](oauth.md) (for user-facing apps).

## App Password

The simplest way to authenticate. Create an app password in your Bluesky account settings, then:

```csharp
var client = await ATProtoClient.CreateWithSessionAsync(
identifier: "alice.bsky.social",
password: "xxxx-xxxx-xxxx-xxxx",
options: new ATProtoClientOptions
{
JsonOptions = ATProtoJsonContext.DefaultOptions,
CborContext = ATProtoCborContext.Default,
});
```

## Session Persistence

Implement `ISessionStore` to persist sessions across app restarts:

```csharp
var client = ATProtoClientFactory.Create(new ATProtoClientOptions
{
SessionStore = new MySessionStore(),
});
bool restored = await client.RestoreSessionAsync(userDid);
```

## Token Refresh

Token refresh is handled automatically when `AutoRetryOnAuthFailure` is enabled (the default). Listen for refresh events to persist updated tokens:

```csharp
if (client.TokenProvider is { } provider)
{
provider.TokenRefreshed += (sender, args) =>
{
SaveTokens(args.Did, args.AccessToken, args.RefreshToken);
};
}
```
99 changes: 99 additions & 0 deletions docs/docs/common-operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Common Operations

## Get a Profile

```csharp
var profile = await client.AppBskyActorGetProfileAsync(
new AppBsky.Actor.GetProfileParameters { Actor = new ATHandle("alice.bsky.social") });
```

## Create a Post

```csharp
var post = new AppBsky.Feed.Post
{
Text = "Hello from CarpaNet!",
CreatedAt = DateTimeOffset.UtcNow,
};

var result = await client.ComAtprotoRepoCreateRecordAsync(
new ComAtproto.Repo.CreateRecordInput
{
Repo = new ATIdentifier(client.AuthenticatedDid!),
Collection = AppBsky.Feed.Post.RecordType,
Record = post.ToJson(),
});

Console.WriteLine($"Posted: {result.Uri}");
```

## Delete a Record

```csharp
var atUri = new ATUri(result.Uri.Value);

await client.ComAtprotoRepoDeleteRecordAsync(
new ComAtproto.Repo.DeleteRecordInput
{
Repo = new ATIdentifier(client.AuthenticatedDid!),
Collection = AppBsky.Feed.Post.RecordType,
Rkey = atUri.RecordKey!,
});
```

## List Records

```csharp
var records = await client.ComAtprotoRepoListRecordsAsync(
new ComAtproto.Repo.ListRecordsParameters
{
Repo = "did:plc:example",
Collection = AppBsky.Feed.Post.RecordType,
Limit = 50,
});

foreach (var record in records.Records)
{
var post = AppBsky.Feed.Post.FromJson(record.Value);
Console.WriteLine(post?.Text);
}
```

## Get a Timeline

```csharp
var timeline = await client.AppBskyFeedGetTimelineAsync(
new AppBsky.Feed.GetTimelineParameters { Limit = 25 });

foreach (var item in timeline.Feed)
{
Console.WriteLine($"{item.Post.Author.Handle}: {item.Post.Record}");
}
```

## AT Protocol Types

CarpaNet provides strongly-typed wrappers for AT Protocol identifiers:

```csharp
// DID
var did = new ATDid("did:plc:z72i7hdynmk6r22z27h6tvur");
Console.WriteLine(did.Method); // "plc"

// Handle
var handle = new ATHandle("alice.bsky.social");

// AT URI
var uri = ATUri.Create("did:plc:example", "app.bsky.feed.post", "3k2la7k");
Console.WriteLine(uri.Collection); // "app.bsky.feed.post"
Console.WriteLine(uri.RecordKey); // "3k2la7k"

// AT Identifier (accepts either DID or Handle)
var id = new ATIdentifier("alice.bsky.social");
Console.WriteLine(id.IsHandle); // true

// CID
var cid = ATCid.FromSha256Hash(sha256Bytes);
```

All identifier types support equality, implicit string conversion, and JSON serialization.
80 changes: 80 additions & 0 deletions docs/docs/creating-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Creating Clients

## Public (Unauthenticated) Client

Uses the Bluesky public AppView — can only make GET requests:

```csharp
using CarpaNet;

// ATProtoClientFactory is source-generated with your JSON/CBOR contexts preconfigured
var client = ATProtoClientFactory.Create();

var profile = await client.AppBskyActorGetProfileAsync(
new AppBsky.Actor.GetProfileParameters { Actor = new ATHandle("alice.bsky.social") });

Console.WriteLine($"{profile.DisplayName} (@{profile.Handle})");
```

## Authenticated Client (App Password)

```csharp
var client = await ATProtoClient.CreateWithSessionAsync(
identifier: "alice.bsky.social", // handle, email, or DID
password: "xxxx-xxxx-xxxx-xxxx", // app password
options: new ATProtoClientOptions
{
JsonOptions = ATProtoJsonContext.DefaultOptions,
CborContext = ATProtoCborContext.Default,
});

// Now you can make POST requests
var timeline = await client.AppBskyFeedGetTimelineAsync(
new AppBsky.Feed.GetTimelineParameters { Limit = 10 });
```

## Authenticated Client with Custom Options

```csharp
var client = ATProtoClientFactory.Create(new ATProtoClientOptions
{
SessionStore = new MySessionStore(), // persist sessions across restarts
EnableRateLimitHandler = true, // automatic 429 retry (default: true)
AutoRetryOnAuthFailure = true, // retry on 401 with token refresh (default: true)
RateLimitMaxRetries = 3,
UserAgent = "MyApp/1.0",
LoggerFactory = loggerFactory,
});
```

## Restoring a Session

```csharp
// From explicit tokens
var client = ATProtoClient.CreateWithRestoredSession(
accessJwt: savedAccessJwt,
refreshJwt: savedRefreshJwt,
did: savedDid,
handle: savedHandle,
pdsUrl: new Uri(savedPdsUrl));

// Or from a session store
var client = ATProtoClientFactory.Create(new ATProtoClientOptions
{
SessionStore = new MySessionStore(),
});
bool restored = await client.RestoreSessionAsync(userDid);
```

## Listening for Token Refreshes

```csharp
if (client.TokenProvider is { } provider)
{
provider.TokenRefreshed += (sender, args) =>
{
// Persist new tokens
SaveTokens(args.Did, args.AccessToken, args.RefreshToken);
};
}
```
30 changes: 30 additions & 0 deletions docs/docs/custom-lexicons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Working with Custom Lexicons

You can use your own or third-party lexicons beyond Bluesky's. Place JSON files in your project and reference them:

```xml
<ItemGroup>
<LexiconFiles Include="lexicons/**/*.json" />

<!-- Or resolve third-party lexicons by authority -->
<LexiconResolveAuthority Include="blog.pckt" />
<LexiconResolveAuthority Include="site.standard" />
</ItemGroup>
```

The source generator produces the same type-safe bindings for custom lexicons. Use the generated `FromJson()` method to parse records:

```csharp
var records = await client.ComAtprotoRepoListRecordsAsync(
new ComAtproto.Repo.ListRecordsParameters
{
Repo = "did:plc:example",
Collection = MyCustom.Namespace.MyRecord.RecordType,
});

foreach (var record in records.Records)
{
var parsed = MyCustom.Namespace.MyRecord.FromJson(record.Value);
Console.WriteLine(parsed?.SomeField);
}
```
Loading
Loading