···2233GoAT Site implements [Standard.site](https://standard.site/) in Go.
4455-Use official [`bluesky-social/indigo`](https://github.com/bluesky-social/indigo/).
55+Use [`anhgelus.world/xrpc`](https://tangled.org/anhgelus.world/xrpc/), a lightweight XRPC client.
6677Main repository is hosted on [Tangled](https://tangled.org/anhgelus.world/goat-site/), an ATProto forge.
8899## Usage
1010+1111+> [!NOTE] Check [anhgelus.world/xrpc's documentation](https://tangled.org/anhgelus.world/xrpc) first!
10121113Get the module with:
1214```bash
···1820- `Document` is for `site.standard.document`;
1921- `Subscription` is for `site.standard.graph.subscription`.
20222121-These types implement `Record`, an interface describing records.
2323+These types implement `xrpc.Record`, an interface describing records.
22242325You can get, list, create, update or delete them with functions:
2424-- `GetRecord[*site.Publication]` to get a publication;
2525-- `ListRecords[*site.Document]` to list documents;
2626-- `CreateRecord[*site.Document]` to create a new document;
2727-- `UpdateRecord[*site.Subscription]` to update a subscription;
2828-- `DeleteRecord[*site.Publication]` to delete a publication.
2626+- `xrpc.GetRecord[*site.Publication]` to get a publication;
2727+- `xrpc.ListRecords[*site.Document]` to list documents;
2828+- `xrpc.CreateRecord[*site.Document]` to create a new document;
2929+- `xrpc.UpdateRecord[*site.Subscription]` to update a subscription;
3030+- `xrpc.DeleteRecord[*site.Publication]` to delete a publication.
29313032You can [verify](https://standard.site/docs/verification/) a publication with `Publication.Verify` and a document with
3133`Document.Verify`:
3234```go
3335var pub *site.Publication
3434-var client *atclient.APIClient
3535-valid, err := pub.Verify(context.Background(), client, "did:plc:123", "pub_rkey")
3636+var did *atproto.DID
3737+var client xrpc.Client
3838+valid, err := pub.Verify(context.Background(), client, did, "pub_rkey")
3639if err != nil {
3740 panic(err)
3841}
···4144}
42454346var doc *site.Document
4444-pubUrl, err := doc.PublicationURL(context.Background(), client.Client)
4747+pubUrl, err := doc.PublicationURL(context.Background(), client)
4848+if err != nil {
4949+ panic(err)
5050+}
5151+valid, err = doc.Verify(context.Background(), client, pubUrl, did, "doc_rkey")
4552if err != nil {
4653 panic(err)
4754}
4848-valid, err = doc.Verify(context.Background(), client, pubUrl, "did:plc:123", "doc_rkey")
4955if !valid {
5056 panic("invalid document :(")
5157}
···65716672To use it, you have to implement `site.Record`:
6773```go
6868-const CollectionContent = `tld.example.content`
7474+var CollectionContent = atproto.NewNSIDBuilder(`tld.example`).Name("content").Build()
69757070-func (c *Content) Type() string {
7676+func (c *Content) Collection() *atproto.NSID {
7177 return CollectionContent
7278}
7379```
74807575-But if you use `site.GetRecord[*site.Document]` to retrieve one, it will return a simple `site.Document` without your
8181+But if you use `xrpc.GetRecord[*site.Document]` to retrieve one, it will return a simple `site.Document` without your
7682custom content!
7777-The `Document.Content` field is a `site.RecordJSON`, a wrapper.
7878-You can get the type of the content with `RecordJSON.Type` and the raw bytes with `RecordJSON.Raw`.
7979-You can also directly parse your `Content` with `RecordJSON.As`:
8383+The `Document.Content` field is a `xrpc.Union`, a type representing an open union.
8484+You can get the collection of the content with `Union.Collection()` and the raw bytes with `Union.Raw`.
8585+You can also directly parse your `Content` with `Union.As`:
8086```go
8187var doc *site.Document
8282-var c *Content
8888+c := new(Content)
8389// returns an error if it cannot parse or if the type is invalid
8484-err := doc.Content.As(c)
8585-if err != nil {
8686- panic(err)
9090+if !doc.Content.As(c) {
9191+ panic("not a Content :(")
8792}
8893```
89949095### Marshal/Unmarshal
91969292-When your record is sent, it is firstly marshaled to a map.
9393-We provide `site.MarshalToMap` which works like the standard `encoding/json`:
9494-```go
9595-var c *Content
9696-// mp is the map[string]any created
9797-mp, err := site.MarshalToMap(c)
9898-if err != nil {
9999- panic(err)
100100-}
101101-/*
102102-mp = map[string]any{"content": []string{}}
103103-*/
104104-```
105105-106106-It uses the `json` tag to determine how to marshal the content.
107107-It supports `omitempty` and embedded type from the standard library.
108108-109109-If you want to marshal your field into a string, you can set `string` in the field tag (with the key `map` here).
110110-It automatically calls the `String() string` method.
111111-```go
112112-type Content struct {
113113- Hey *url.URL `json:"hey" map:"string"`
114114-}
115115-/*
116116-mp = map[string]any{"hey": "https://example.org/"}
117117-*/
118118-```
119119-120120-If you are using complexe types, you may have to implement `json.Unmarshaler` to unmarshal from JSON and
121121-`site.MarshalerMap` to marshal to a map.
122122-```go
123123-func (c *Content) MarshalMap() (map[string]any, error) {
124124- mp := make(map[string]any, 1)
125125- mp["foo"] = "bar"
126126- return mp, nil
127127-}
128128-// the future call to site.MarshalToMap on *Content
129129-// will return map[string]any{"foo":"bar"}.
130130-```
9797+See [anhgelus.world/xrpc documentation](https://tangled.org/anhgelus.world/xrpc/#complexe-records).
1319813299## Extending lexicons
133100···141108}
142109```
143110144144-You can call any functions with this new lexicon: the embedded base lexicon already implements the `Record` interface!
111111+You can call any functions with this new lexicon: the embedded base lexicon already implements the `xrpc.Record`
112112+interface!
+33-30
document.go
···1111 "strings"
1212 "time"
13131414- "github.com/bluesky-social/indigo/atproto/syntax"
1515- lexutil "github.com/bluesky-social/indigo/lex/util"
1614 "golang.org/x/net/html"
1515+ "tangled.org/anhgelus.world/xrpc"
1616+ "tangled.org/anhgelus.world/xrpc/atproto"
1717)
18181919-const CollectionDocument = CollectionBase + ".document"
1919+var CollectionDocument = CollectionBase.Name("document").Build()
20202121// Document may be standalone or associated with a [Publication].
2222-// This [Record] can be used to store a document's content and its associated metadata.
2222+// This [xrpc.Record] can be used to store a document's content and its associated metadata.
2323type Document struct {
2424 // Site points to a [Publication] record `at://` or a [Publication.URL] `https://` for loose documents.
2525 // Avoid trailing slashes.
2626- Site *URL `json:"site" map:"string"`
2626+ Site *URL `json:"site"`
2727 // Title of the [Document].
2828 // Max length: 5000.
2929 // Max graphemes: 500.
3030 Title string `json:"title"`
3131 // PublishedAt is the [time.Time] of the [Document]'s publish time.
3232- PublishedAt time.Time `json:"-"`
3232+ PublishedAt time.Time `json:"publishedAt"`
3333 // Path is combined with [Document.Site] or [Publication.URL] to construct a canonical URL to the document.
3434 // A slash should be included at the beginning of this value.
3535 Path *string `json:"path,omitempty"`
···3939 Description *string `json:"description,omitempty"`
4040 // CoverImage to used for thumbnail or cover.
4141 // Less than 1MB in size.
4242- CoverImage *Blob `json:"coverImage,omitempty"`
4343- // Content is a custom [Record] used to define the [Document]'s content.
4444- Content *RecordJSON `json:"content,omitempty"`
4242+ CoverImage *xrpc.Blob `json:"coverImage,omitempty"`
4343+ // Content is an [xrpc.Union] used to define the [Document]'s content.
4444+ Content *xrpc.Union `json:"content,omitempty"`
4545 // TextContent is a plaintext representation of the [Document.Content].
4646 // Should not contain markdown or other formatting.
4747 TextContent string `json:"textContent,omitempty"`
···5959 // Max graphemes: 128.
6060 Tags []string `json:"tags,omitempty"`
6161 // UpdatedAt is the [time.Time] of the [Document]'s last edit.
6262- UpdatedAt *time.Time `json:"-"`
6262+ UpdatedAt *time.Time `json:"updatedAt,omitempty"`
6363}
64646565-func (d *Document) Type() string {
6565+func (d *Document) Collection() *atproto.NSID {
6666 return CollectionDocument
6767}
68686969-func (d *Document) MarshalMap() (map[string]any, error) {
6969+func (d *Document) MarshalMap() (any, error) {
7070 type t Document
7171- mp, err := MarshalToMap(t(*d))
7171+ mpp, err := xrpc.MarshalToMap(t(*d))
7272 if err != nil {
7373 return nil, err
7474 }
7575+ mp := mpp.(map[string]any)
7576 if v, ok := mp["path"]; ok && !strings.HasPrefix(v.(string), "/") {
7677 mp["path"] = "/" + v.(string)
7777- }
7878- mp["publishedAt"] = d.PublishedAt.UTC().Format(TimeFormat)
7979- if d.UpdatedAt != nil {
8080- tn := d.UpdatedAt.UTC().Format(TimeFormat)
8181- mp["updatedAt"] = &tn
8278 }
8379 return mp, nil
8480}
···9490 if err != nil {
9591 return err
9692 }
9797- v.t.PublishedAt, err = ParseTime(v.PublishedAt)
9393+ v.t.PublishedAt, err = atproto.ParseTime(v.PublishedAt)
9894 if err != nil {
9995 return err
10096 }
10197 if v.UpdatedAt != nil {
10298 v.t.UpdatedAt = new(time.Time)
103103- *v.t.UpdatedAt, err = ParseTime(*v.UpdatedAt)
9999+ *v.t.UpdatedAt, err = atproto.ParseTime(*v.UpdatedAt)
104100 if err != nil {
105101 return err
106102 }
···113109//
114110// Prefer using [GetRecord] if you plan to retrieve the record.
115111// Under the hood, this method retrieves the [Publication] record if [Document.Site] is an [ATURL].
116116-func (d *Document) PublicationURL(ctx context.Context, client lexutil.LexClient) (*url.URL, error) {
112112+func (d *Document) PublicationURL(ctx context.Context, client xrpc.Client) (*url.URL, error) {
117113 if !d.Site.IsAT() {
118114 return d.Site.URL(), nil
119115 }
120120- at := d.Site.AT()
121121- pub, err := GetRecord[*Publication](ctx, client, at.Authority(), at.RecordKey())
116116+ uri, err := d.Site.AT().URI(ctx, client.Directory())
122117 if err != nil {
123118 return nil, err
124119 }
120120+ pub := new(Publication)
121121+ union, err := client.FetchURI(ctx, uri)
122122+ if err != nil {
123123+ return nil, err
124124+ }
125125+ if !union.Value.As(pub) {
126126+ return nil, ErrInvalidCollection{union.Value.Collection(), pub.Collection()}
127127+ }
125128 return pub.URL, nil
126129}
127130128131var ErrCannotVerifyWithNoPath = errors.New("cannot verify a document with no path")
129132130133// Verify the [Document].
131131-func (d *Document) Verify(ctx context.Context, client *http.Client, pubURL *url.URL, repo syntax.AtIdentifier, rkey syntax.RecordKey) (bool, error) {
134134+func (d *Document) Verify(ctx context.Context, client *http.Client, pubURL *url.URL, did *atproto.DID, rkey atproto.RecordKey) (bool, error) {
132135 if d.Path == nil {
133136 return false, ErrCannotVerifyWithNoPath
134137 }
···156159 case "href":
157160 href = attr.Val
158161 }
159159- if rel != "" && rel != CollectionDocument {
162162+ if rel != "" && rel != CollectionDocument.String() {
160163 break
161164 }
162162- if href == getDocumentVerification(repo, rkey) {
165165+ if href == getDocumentVerification(did, rkey) {
163166 return true, nil
164167 }
165168 }
···169172}
170173171174// GetDocumentVerificationTag returns the HTML link tag checked during the verification of the [Document].
172172-func GetDocumentVerificationTag(repo syntax.AtIdentifier, rkey syntax.RecordKey) template.HTML {
175175+func GetDocumentVerificationTag(repo *atproto.DID, rkey atproto.RecordKey) template.HTML {
173176 // We don't use /> to end the tag, because it is only valid for HTML5 and only required for XHTML.
174177 // See https://blog.novalistic.com/archives/2017/08/optional-end-tags-in-html/
175178 return template.HTML(
176176- fmt.Sprintf(`<link rel="%s" href="%s">`, CollectionDocument, createAtURL(repo, CollectionDocument, rkey)),
179179+ fmt.Sprintf(`<link rel="%s" href="%s">`, CollectionDocument, getDocumentVerification(repo, rkey)),
177180 )
178181}
179182180183// getPublicationVerification returns the string used during the verification of the [Publication].
181181-func getDocumentVerification(repo syntax.AtIdentifier, rkey syntax.RecordKey) string {
182182- return createAtURL(repo, CollectionDocument, rkey).String()
184184+func getDocumentVerification(repo *atproto.DID, rkey atproto.RecordKey) string {
185185+ return atproto.NewURI(repo, CollectionDocument, rkey).String()
183186}