// Package connection — see connection.go for package doc. package connection import ( "context" "fmt" "time" "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" ) const nsidListRecords = "com.atproto.repo.listRecords" // ListEntry is one decoded quest.atmo.connection record returned by List. type ListEntry struct { URI string // at-uri of this record With syntax.DID // the connected person ConnectedAt time.Time EventURI string // optional at-uri of the associated event } // listRecordsResponse is the wire shape of com.atproto.repo.listRecords output. type listRecordsResponse struct { Records []listRecordsEntry `json:"records"` Cursor string `json:"cursor,omitempty"` } type listRecordsEntry struct { URI string `json:"uri"` CID string `json:"cid"` Value connectionValue `json:"value"` } type connectionValue struct { Type string `json:"$type"` With string `json:"with"` ConnectedAt string `json:"connectedAt"` Event string `json:"event,omitempty"` } // List reads all quest.atmo.connection records from the user's PDS via // com.atproto.repo.listRecords (public, unauthenticated). Paginated // internally — returns the full set. func List(ctx context.Context, pdsHost string, did syntax.DID) ([]ListEntry, error) { c := atclient.NewAPIClient(pdsHost) var all []ListEntry cursor := "" for { params := map[string]any{ "repo": did.String(), "collection": NSID, "limit": 100, } if cursor != "" { params["cursor"] = cursor } var resp listRecordsResponse if err := c.Get(ctx, syntax.NSID(nsidListRecords), params, &resp); err != nil { return nil, fmt.Errorf("listRecords %s: %w", NSID, err) } for _, r := range resp.Records { targetDID, err := syntax.ParseDID(r.Value.With) if err != nil { continue // skip malformed } t, _ := time.Parse(time.RFC3339, r.Value.ConnectedAt) all = append(all, ListEntry{ URI: r.URI, With: targetDID, ConnectedAt: t, EventURI: r.Value.Event, }) } if resp.Cursor == "" || len(resp.Records) == 0 { break } cursor = resp.Cursor } return all, nil } // Deduplicate returns a new slice with at most one entry per target DID. // When the same person appears across multiple records (e.g. connected at // different events), the entry with the most recent ConnectedAt is kept. // Insertion order of the first occurrence is otherwise preserved. func Deduplicate(entries []ListEntry) []ListEntry { seen := make(map[syntax.DID]int, len(entries)) // DID → index in result result := make([]ListEntry, 0, len(entries)) for _, e := range entries { if idx, ok := seen[e.With]; ok { if e.ConnectedAt.After(result[idx].ConnectedAt) { result[idx] = e } } else { seen[e.With] = len(result) result = append(result, e) } } return result } // HasConnection checks if the owner already has a connection record with // the given target at the given event (or with no event if eventURI is empty). // Returns true if a matching record exists. Uses the public listRecords // endpoint (unauthenticated). func HasConnection(ctx context.Context, pdsHost string, ownerDID, targetDID syntax.DID, eventURI string) bool { entries, err := List(ctx, pdsHost, ownerDID) if err != nil { return false // can't tell — allow the write } for _, e := range entries { if e.With == targetDID && e.EventURI == eventURI { return true } } return false }