Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
5.8 kB · 184 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185package xrpc
import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time"
"github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/api/tangled" "tangled.org/core/knotmirror/db")
const ( defaultCommitStatsMonths = 7 maxCommitStatsMonths = 24 committerCacheCapacity = 4_096 committerCacheFreshness = 30 * time.Second)
func (x *Xrpc) GetCommitStats(w http.ResponseWriter, r *http.Request) { actor, err := syntax.ParseDID(r.URL.Query().Get("actor")) if err != nil { _ = writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "InvalidRequest", Message: "actor must be a DID"}) return }
months := defaultCommitStatsMonths if raw := r.URL.Query().Get("months"); raw != "" { months, err = strconv.Atoi(raw) if err != nil || months < 1 || months > maxCommitStatsMonths { _ = writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "InvalidRequest", Message: "months must be between 1 and 24"}) return } }
now := time.Now().UTC() currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) committers := x.resolveCommitters(r.Context(), actor) counts, err := db.ListCommitCounts( r.Context(), x.db, committers, currentMonth.AddDate(0, -(months-1), 0), currentMonth.AddDate(0, 1, 0), months, ) if err != nil { x.logger.Error("listing commit stats failed", "actor", actor, "err", err) _ = writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "internal server error"}) return }
result := make([]*tangled.GitTemp2GetCommitStats_Month, len(counts)) for i, count := range counts { result[i] = &tangled.GitTemp2GetCommitStats_Month{ Start: count.Month.UTC().Format(time.RFC3339), Commits: count.Commits, } } _ = writeJson(w, http.StatusOK, tangled.GitTemp2GetCommitStats_Output{Months: result})}
// resolveCommitters maps an actor DID to its verified committer emails with a// short-lived LRU cache; concurrent lookups of the same actor share one call.// Failures are not cached: the caller falls back to the actor DID itself.func (x *Xrpc) resolveCommitters(ctx context.Context, actor syntax.DID) []string { key := actor.String() if committers, ok := x.cachedCommitters(key); ok { return committers } result, _, _ := x.committerGroup.Do(key, func() (any, error) { if committers, ok := x.cachedCommitters(key); ok { return committers, nil } committers, err := x.fetchResolvedCommitters(ctx, actor) if err != nil { committerResolverFailures.Inc() x.logger.Warn("resolving verified committer emails failed", "actor", actor, "err", err) return []string{key}, nil } if x.committers != nil { x.committers.Add(key, committers) } return committers, nil }) return result.([]string)}
func (x *Xrpc) cachedCommitters(key string) ([]string, bool) { if x.committers == nil { return nil, false } return x.committers.Get(key)}
func (x *Xrpc) fetchResolvedCommitters(ctx context.Context, actor syntax.DID) ([]string, error) { if x.cfg.DeliberiURL == "" || x.serviceSigner == nil { return []string{actor.String()}, nil } endpoint, err := url.JoinPath(x.cfg.DeliberiURL, "xrpc", tangled.IdentityResolveCommittersNSID) if err != nil { return nil, fmt.Errorf("building Deliberi endpoint: %w", err) } payload, err := json.Marshal(map[string]string{"actor": actor.String()}) if err != nil { return nil, err } requestCtx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() req, err := http.NewRequestWithContext(requestCtx, http.MethodPost, endpoint, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("building committer resolver request: %w", err) } audience, err := syntax.ParseDID(x.cfg.DeliberiDID) if err != nil { return nil, fmt.Errorf("parsing committer resolver DID: %w", err) } token, err := x.serviceSigner.Sign(audience, syntax.NSID(tangled.IdentityResolveCommittersNSID)) if err != nil { return nil, fmt.Errorf("signing committer resolver request: %w", err) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") client := x.internalClient if client == nil { client = x.httpClient } if client == nil { client = http.DefaultClient } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("calling committer resolver: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("committer resolver returned %s", resp.Status) } body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024+1)) if err != nil { return nil, fmt.Errorf("reading committer resolver response: %w", err) } if len(body) > 64*1024 { return nil, fmt.Errorf("committer resolver response is too large") } var output tangled.IdentityResolveCommitters_Output if err := json.Unmarshal(body, &output); err != nil { return nil, fmt.Errorf("decoding committer resolver response: %w", err) }
committers := make([]string, 0, min(len(output.Committers)+1, 251)) committers = append(committers, actor.String()) seen := map[string]struct{}{actor.String(): {}} for _, value := range output.Committers { value = strings.ToLower(strings.TrimSpace(value)) if value == "" || len(value) > 320 || strings.ContainsRune(value, 0) { continue } if did, err := syntax.ParseDID(value); err == nil { if did.String() != actor.String() { continue } } else if !strings.Contains(value, "@") { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} committers = append(committers, value) if len(committers) == 251 { break } } return committers, nil}