Monorepo for Tangled tangled.org
2

Configure Feed

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

knotserver/xrpc: list members & collaborators

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 16, 2026, 9:04 PM +0300) commit d8a997b4 parent 732bdc91 change-id uokpuzmt
+153
+49
knotserver/xrpc/list.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strconv" 7 + 8 + "tangled.org/core/knotserver/db" 9 + ) 10 + 11 + func parseListParams(r *http.Request) (db.ListPage, error) { 12 + q := r.URL.Query() 13 + p := db.ListPage{Limit: db.ListDefaultLimit, Desc: true} 14 + 15 + if s := q.Get("limit"); s != "" { 16 + n, err := strconv.Atoi(s) 17 + if err != nil { 18 + return p, fmt.Errorf("limit must be an integer") 19 + } 20 + p.Limit = min(max(n, 1), db.ListMaxLimit) 21 + } 22 + 23 + if s := q.Get("cursor"); s != "" { 24 + n, err := strconv.Atoi(s) 25 + if err != nil { 26 + return p, fmt.Errorf("cursor must be an integer") 27 + } 28 + p.Cursor = &n 29 + } 30 + 31 + switch q.Get("order") { 32 + case "", "desc": 33 + p.Desc = true 34 + case "asc": 35 + p.Desc = false 36 + default: 37 + return p, fmt.Errorf("order must be 'asc' or 'desc'") 38 + } 39 + 40 + return p, nil 41 + } 42 + 43 + func mapSlice[T, U any](items []T, f func(T) U) []U { 44 + out := make([]U, len(items)) 45 + for i, it := range items { 46 + out[i] = f(it) 47 + } 48 + return out 49 + }
+55
knotserver/xrpc/list_collaborators.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/knotserver/db" 10 + xrpcerr "tangled.org/core/xrpc/errors" 11 + ) 12 + 13 + func (x *Xrpc) ListCollaborators(w http.ResponseWriter, r *http.Request) { 14 + subject := r.URL.Query().Get("subject") 15 + repoDid, err := syntax.ParseDID(subject) 16 + if err != nil { 17 + writeError(w, xrpcerr.InvalidRepoError(subject), http.StatusBadRequest) 18 + return 19 + } 20 + 21 + p, err := parseListParams(r) 22 + if err != nil { 23 + writeError(w, xrpcerr.NewXrpcError( 24 + xrpcerr.WithTag("InvalidRequest"), 25 + xrpcerr.WithMessage(err.Error()), 26 + ), http.StatusBadRequest) 27 + return 28 + } 29 + 30 + collaborators, next, err := db.ListCollaborators(x.Db, repoDid, p) 31 + if err != nil { 32 + x.Logger.Error("failed to list collaborators", "repoDid", repoDid, "error", err) 33 + writeError(w, xrpcerr.NewXrpcError( 34 + xrpcerr.WithTag("InternalServerError"), 35 + xrpcerr.WithMessage("failed to list collaborators"), 36 + ), http.StatusInternalServerError) 37 + return 38 + } 39 + 40 + response := tangled.RepoListCollaborators_Output{ 41 + Items: mapSlice(collaborators, func(c db.Collaborator) *tangled.RepoListCollaborators_ListItem { 42 + return &tangled.RepoListCollaborators_ListItem{ 43 + Subject: c.Subject.String(), 44 + AddedBy: c.AddedBy.String(), 45 + CreatedAt: c.Created, 46 + } 47 + }), 48 + } 49 + if next != nil { 50 + cur := strconv.Itoa(*next) 51 + response.Cursor = &cur 52 + } 53 + 54 + x.writeJson(w, response) 55 + }
+47
knotserver/xrpc/list_members.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + 7 + "tangled.org/core/api/tangled" 8 + "tangled.org/core/knotserver/db" 9 + xrpcerr "tangled.org/core/xrpc/errors" 10 + ) 11 + 12 + func (x *Xrpc) ListMembers(w http.ResponseWriter, r *http.Request) { 13 + p, err := parseListParams(r) 14 + if err != nil { 15 + writeError(w, xrpcerr.NewXrpcError( 16 + xrpcerr.WithTag("InvalidRequest"), 17 + xrpcerr.WithMessage(err.Error()), 18 + ), http.StatusBadRequest) 19 + return 20 + } 21 + 22 + members, next, err := db.ListKnotMembers(x.Db, p) 23 + if err != nil { 24 + x.Logger.Error("failed to list knot members", "error", err) 25 + writeError(w, xrpcerr.NewXrpcError( 26 + xrpcerr.WithTag("InternalServerError"), 27 + xrpcerr.WithMessage("failed to list knot members"), 28 + ), http.StatusInternalServerError) 29 + return 30 + } 31 + 32 + response := tangled.KnotListMembers_Output{ 33 + Items: mapSlice(members, func(m db.KnotMember) *tangled.KnotListMembers_ListItem { 34 + return &tangled.KnotListMembers_ListItem{ 35 + Subject: m.Subject.String(), 36 + AddedBy: m.Did.String(), 37 + CreatedAt: m.Created, 38 + } 39 + }), 40 + } 41 + if next != nil { 42 + cur := strconv.Itoa(*next) 43 + response.Cursor = &cur 44 + } 45 + 46 + x.writeJson(w, response) 47 + }
+2
knotserver/xrpc/xrpc.go
··· 83 83 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch) 84 84 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) 85 85 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) 86 + r.Get("/"+tangled.RepoListCollaboratorsNSID, x.ListCollaborators) 86 87 87 88 // knot query endpoints (no auth required) 88 89 r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys) 90 + r.Get("/"+tangled.KnotListMembersNSID, x.ListMembers) 89 91 r.Get("/"+tangled.KnotVersionNSID, x.Version) 90 92 91 93 // service query endpoints (no auth required)