Go library implementing a lightweight XRPC client for the AT Protocol.
atproto atprotocol lightweight xrpc
Go 100.0%
76 1 4

Clone this repository

https://tangled.org/anhgelus.world/xrpc https://tangled.org/did:plc:vtqucb4iga7b5wzza3zbz4so/xrpc
git@tangled.org:anhgelus.world/xrpc git@tangled.org:did:plc:vtqucb4iga7b5wzza3zbz4so/xrpc

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

XRPC#

Go library implementing a lightweight XRPC client for the AT Protocol.

Main repository is hosted on Tangled, an ATProto forge.

Why?#

The official Bluesky library (Indigo) is heavy and use a ton of dependencies. In addition to this, their API does not provide an incredible developer experiences.

Use this library if you want a lightweight client. Use Indigo if you want a feature-complete XRPC implementations.

Scope#

This project wants to provide a lightweight application-agnostic XRPC client. It reimplements the required foundations of the ATProto.

This library is low-level. We are just creating an HTTP client with ATProto-specific features. For example, we do not plan to add a randomized exponential backoff.

If it is possible, we will try to make this library compatible with Indigo.

Usage#

Get the module with

go get -u tangled.org/anhgelus.world/xrpc

XRPC is fully documented, you can check the online documentation at: https://pkg.go.dev/tangled.org/anhgelus.world/xrpc or with go doc.

ATProto primitives are in atproto package.

You can create a new simple XRPC client with:

var httpClient *http.Client
client := xrpc.NewClient(httpClient)

To create a new request, you can use client.NewRequest:

// creating the request
req := client.NewRequest().
    Server("https://..."). // URL of the XRPC server (PDS, relay...)
    Endpoint(atproto.NewNSIDBuilder("org.example").Name("fooBar").Build()). // XRPC endpoint called
    Params(nil). // optional url.Values
    Build() // can panic if something is wrong
// XRPC query
b, err := client.Query(context.TODO(), req)
if err != nil {
    panic(err)
}
// XRPC procedure
body := xrpc.RawBodyRequest{[]byte("Hello world :D"), "text/plain"} // procedure body
b, err := client.Procedure(context.TODO(), req, body)
if err != nil {
    panic(err)
}

Using simple records#

Another way to interact with an XRPC server is to use the Record interface. It describes an ATProto record (an object based on a lexicon) and must be serialized into JSON and into a map.

type MyRecord struct {
    Hey string `json:"hey"`
}

var myRecordType = atproto.NewNSIDBuilder("org.example").Name("foo").Build()

func (r *MyRecord) Collection() *atproto.NSID {
    return myRecordType
}

Then, you can use the higher level API:

// get a record
var did *atproto.DID // did of a user
var rkey atproto.RecordKey // rkey of the record
rec, err := xrpc.GetRecord[*MyRecord](
    context.TODO(), 
    client, 
    "https://...", // URL of the XRPC server (PDS, relay...)
    did,
    rkey,
    nil,
)
if err != nil {
    panic(err)
}
// list records
recs, err := xrpc.ListRecords[*MyRecord](
    context.TODO(), 
    client, 
    "https://...", // URL of the XRPC server (PDS, relay...)
    did,
    0, "", false, // not required values
)
if err != nil {
    panic(err)
}
WARNING

You should always call xrpc.Marshal when marshaling a Record into JSON!

Complexe records#

When your record is sent, it is firstly marshaled to a map following the json tags you have defined. This automatic process may produce invalid results for complexe types. You can implement xrpc.MapMarshaler to specify how it must be marshaled:

func (r *MyRecord) MarshalMap() (any, error) {
    return map[string]any{"foo": r.Hey}, nil
}

You can use xrpc.MarshalToMap to marshal anything.

The data is sent by the server in JSON. The library decode it automatically. You can implement json.Umarshaler to specify how it must be unmarshaled:

func (r *MyRecord) UnmarshalJSON(b []byte) error {
    var v struct {
        Hey string `json:"foo"`
    }
    err := json.Unmarshal(b, &v)
    r.Hey = v.Hey
    return err
}

Translating lexicons into Go types#

Every ATProto specific type like DID, TID, NSID... must be represented using their definition in the package atproto.

An open union must be represented by *xrpc.Union.

A custom record can be safely represented by its own type.

type MyComplexeRecord struct {
    URI atproto.RawURI `json:"uri"`
    Record *MyRecord `json:"record"`
    RKey atproto.RecordKey `json:"rkey"`
    Anything *xrpc.Union `json:"anything"`
}
// no need to implement xrpc.MapMarshaler or json.Unmarshaler here :D

Compatibility with Indigo#

XRPC has a CompatClient that is compatible with Indigo's LexClient. You can wrap any Client created with XRPC as a CompatClient to use them in lexicons generated by Indigo.

Examples#

If you want real examples, you can look at:

  • Lasa, a stateless proxy that generates a RSS or an Atom feed from a Standard.site publication.
  • GoAT Site, a library that implements Standard.site in Go.

Roadmap#