Low-latency AT Protocol interaction indexer.
20

Configure Feed

Select the types of activity you want to include in your feed.

add getBacklinks

Aly Raffauf (Jul 2, 2026, 11:35 PM EDT) a5e15a02 4079f2d0

+161 -13
+50 -8
internal/api/backlinks.go
··· 1 1 package api 2 2 3 3 import ( 4 + "encoding/base64" 4 5 "encoding/json" 5 6 "log" 6 7 "net/http" 8 + "strconv" 9 + 10 + "github.com/alyraffauf/asterism/internal/store" 7 11 ) 8 12 9 - type backlinksCountResponse struct { 10 - Total uint64 `json:"total"` 13 + type getBacklinksResponse struct { 14 + Total uint64 `json:"total"` 15 + Records []store.Record `json:"records"` 16 + Cursor *string `json:"cursor"` 11 17 } 12 18 13 - func (s *Server) GetBacklinksCount(w http.ResponseWriter, r *http.Request) { 14 - subject := r.URL.Query().Get("subject") 15 - source := r.URL.Query().Get("source") 19 + func (s *Server) GetBacklinks(w http.ResponseWriter, r *http.Request) { 20 + query := r.URL.Query() 21 + 22 + subject := query.Get("subject") 23 + source := query.Get("source") 24 + dids := query["did"] 16 25 17 26 collection, path, err := parseSource(source) 18 27 if err != nil { ··· 20 29 return 21 30 } 22 31 23 - total, err := s.Store.CountBacklinks(r.Context(), subject, collection, path) 32 + limit := uint64(100) 33 + if raw := query.Get("limit"); raw != "" { 34 + parsed, err := strconv.ParseUint(raw, 10, 64) 35 + if err != nil || parsed == 0 || parsed > 1000 { 36 + http.Error(w, "limit must be a number between 1 and 1000", http.StatusBadRequest) 37 + return 38 + } 39 + limit = parsed 40 + } 41 + 42 + reverse := query.Get("reverse") == "true" 43 + 44 + var after int64 45 + if raw := query.Get("cursor"); raw != "" { 46 + decoded, err := base64.StdEncoding.DecodeString(raw) 47 + if err != nil { 48 + http.Error(w, "invalid cursor", http.StatusBadRequest) 49 + return 50 + } 51 + after, err = strconv.ParseInt(string(decoded), 10, 64) 52 + if err != nil { 53 + http.Error(w, "invalid cursor", http.StatusBadRequest) 54 + return 55 + } 56 + } 57 + 58 + total, records, err := s.Store.ListBacklinks(r.Context(), subject, collection, path, dids, reverse, after, limit) 24 59 if err != nil { 25 - log.Println("count backlinks:", err) 60 + log.Println("list backlinks:", err) 26 61 http.Error(w, "internal error", http.StatusInternalServerError) 27 62 return 28 63 } 29 64 65 + var cursor *string 66 + if uint64(len(records)) == limit { 67 + last := records[len(records)-1] 68 + encoded := base64.StdEncoding.EncodeToString([]byte(strconv.FormatInt(last.ID, 10))) 69 + cursor = &encoded 70 + } 71 + 30 72 w.Header().Set("Content-Type", "application/json") 31 - json.NewEncoder(w).Encode(backlinksCountResponse{Total: total}) 73 + json.NewEncoder(w).Encode(getBacklinksResponse{Total: total, Records: records, Cursor: cursor}) 32 74 }
+6 -4
internal/api/dids.go
··· 15 15 } 16 16 17 17 func (s *Server) GetBacklinkDids(w http.ResponseWriter, r *http.Request) { 18 - subject := r.URL.Query().Get("subject") 19 - source := r.URL.Query().Get("source") 18 + query := r.URL.Query() 19 + 20 + subject := query.Get("subject") 21 + source := query.Get("source") 20 22 21 23 collection, path, err := parseSource(source) 22 24 if err != nil { ··· 26 28 27 29 28 30 limit := uint64(100) 29 - if raw := r.URL.Query().Get("limit"); raw != "" { 31 + if raw := query.Get("limit"); raw != "" { 30 32 parsed, err := strconv.ParseUint(raw, 10, 64) 31 33 if err != nil || parsed == 0 || parsed > 1000 { 32 34 http.Error(w, "limit must be a number between 1 and 1000", http.StatusBadRequest) ··· 36 38 } 37 39 38 40 after := "" 39 - if raw := r.URL.Query().Get("cursor"); raw != "" { 41 + if raw := query.Get("cursor"); raw != "" { 40 42 decoded, err := base64.StdEncoding.DecodeString(raw) 41 43 if err != nil { 42 44 http.Error(w, "invalid cursor", http.StatusBadRequest)
+1 -1
internal/api/server.go
··· 14 14 mux := http.NewServeMux() 15 15 mux.HandleFunc("GET /xrpc/blue.microcosm.links.getBacklinksCount", s.GetBacklinksCount) 16 16 mux.HandleFunc("GET /xrpc/blue.microcosm.links.getBacklinkDids", s.GetBacklinkDids) 17 - 17 + mux.HandleFunc("GET /xrpc/blue.microcosm.links.getBacklinks", s.GetBacklinks) 18 18 19 19 return http.ListenAndServe(addr, mux) 20 20 }
+70
internal/store/query.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "strings" 6 7 ) 8 + 9 + type Record struct { 10 + ID int64 `json:"-"` 11 + ActorDid string `json:"did"` 12 + Collection string `json:"collection"` 13 + RecordKey string `json:"rkey"` 14 + } 7 15 8 16 func (s *Store) CountBacklinks(ctx context.Context, target, collection, fieldPath string) (uint64, error) { 9 17 var total uint64 ··· 52 60 53 61 return total, dids, nil 54 62 } 63 + 64 + func (s *Store) ListBacklinks(ctx context.Context, target, collection, fieldPath string, dids[]string, reverse bool, after int64, limit uint64) (total uint64, records []Record, err error) { 65 + where := `target = ? AND collection = ? AND field_path = ?` 66 + args := []any{target, collection, fieldPath} 67 + 68 + if len(dids) > 0 { 69 + placeholders := make([]string, len(dids)) 70 + for i, did := range dids { 71 + placeholders[i] = "?" 72 + args = append(args, did) 73 + } 74 + where += `AND actor_did IN (` + strings.Join(placeholders, ", ") + `)` 75 + 76 + err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM links WHERE `+where, args...).Scan(&total) 77 + if err!= nil { 78 + return 0, nil, fmt.Errorf("count backlinks: %w", err) 79 + } 80 + } else { 81 + total, err = s.CountBacklinks(ctx, target, collection, fieldPath) 82 + if err != nil { 83 + return 0, nil, err 84 + } 85 + } 86 + 87 + query := `SELECT id, actor_did, collection, record_key FROM links WHERE ` + where 88 + listArgs := append([]any{}, args...) 89 + 90 + if after != 0 { 91 + if reverse { 92 + query += ` AND id > ?` 93 + } else { 94 + query += ` AND id < ?` 95 + } 96 + listArgs = append(listArgs, after) 97 + } 98 + 99 + if reverse { 100 + query += ` ORDER BY id ASC LIMIT ?` 101 + } else { 102 + query += ` ORDER BY id DESC LIMIT ?` 103 + } 104 + listArgs = append(listArgs, limit) 105 + 106 + rows, err := s.db.QueryContext(ctx, query, listArgs...) 107 + if err != nil { 108 + return 0, nil, fmt.Errorf("query backlinks: %w", err) 109 + } 110 + defer rows.Close() 111 + 112 + for rows.Next() { 113 + var rec Record 114 + if err := rows.Scan(&rec.ID, &rec.ActorDid, &rec.Collection, &rec.RecordKey); err != nil { 115 + return 0, nil, fmt.Errorf("scan record: %w", err) 116 + } 117 + records = append(records, rec) 118 + } 119 + if err := rows.Err(); err != nil { 120 + return 0, nil, fmt.Errorf("iterate records: %w", err) 121 + } 122 + 123 + return total, records, nil 124 + }