diff --git a/appview/db/db.go b/appview/db/db.go index 58fa50f0..6f6d0e41 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -527,8 +527,7 @@ func Make(dbPath string) (*DB, error) { -- label to subscribe to label_at text not null, - unique (repo_at, label_at), - foreign key (label_at) references label_definitions (at_uri) + unique (repo_at, label_at) ); create table if not exists migrations ( diff --git a/appview/db/repos.go b/appview/db/repos.go index b8a25284..48a94c74 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -345,14 +345,27 @@ func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { return &repo, nil } -func AddRepo(e Execer, repo *models.Repo) error { - _, err := e.Exec( +func AddRepo(tx *sql.Tx, repo *models.Repo) error { + _, err := tx.Exec( `insert into repos (did, name, knot, rkey, at_uri, description, source) values (?, ?, ?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, ) - return err + if err != nil { + return fmt.Errorf("failed to insert repo: %w", err) + } + + for _, dl := range repo.Labels { + if err := SubscribeLabel(tx, &models.RepoLabel{ + RepoAt: repo.RepoAt(), + LabelAt: syntax.ATURI(dl), + }); err != nil { + return fmt.Errorf("failed to subscribe to label: %w", err) + } + } + + return nil } func RemoveRepo(e Execer, did, name string) error { diff --git a/appview/ingester.go b/appview/ingester.go index 64d51a34..dbde661c 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" + "slices" "time" @@ -80,6 +82,8 @@ func (i *Ingester) Ingest() processFunc { err = i.ingestIssueComment(e) case tangled.LabelDefinitionNSID: err = i.ingestLabelDefinition(e) + case tangled.LabelOpNSID: + err = i.ingestLabelOp(e) } l = i.Logger.With("nsid", e.Commit.Collection) } @@ -953,3 +957,79 @@ func (i *Ingester) ingestLabelDefinition(e *jmodels.Event) error { return nil } + +func (i *Ingester) ingestLabelOp(e *jmodels.Event) error { + did := e.Did + rkey := e.Commit.RKey + + var err error + + l := i.Logger.With("handler", "ingestLabelOp", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) + l.Info("ingesting record") + + ddb, ok := i.Db.Execer.(*db.DB) + if !ok { + return fmt.Errorf("failed to index label op, invalid db cast") + } + + switch e.Commit.Operation { + case jmodels.CommitOperationCreate: + raw := json.RawMessage(e.Commit.Record) + record := tangled.LabelOp{} + err = json.Unmarshal(raw, &record) + if err != nil { + return fmt.Errorf("invalid record: %w", err) + } + + subject := syntax.ATURI(record.Subject) + collection := subject.Collection() + + var repo *models.Repo + switch collection { + case tangled.RepoIssueNSID: + i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject)) + if err != nil || len(i) != 1 { + return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i)) + } + repo = i[0].Repo + default: + return fmt.Errorf("unsupport label subject: %s", collection) + } + + actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels)) + if err != nil { + return fmt.Errorf("failed to build label application ctx: %w", err) + } + + ops := models.LabelOpsFromRecord(did, rkey, record) + + for _, o := range ops { + def, ok := actx.Defs[o.OperandKey] + if !ok { + return fmt.Errorf("failed to find label def for key: %s, expected: %q", o.OperandKey, slices.Collect(maps.Keys(actx.Defs))) + } + if err := i.Validator.ValidateLabelOp(def, &o); err != nil { + return fmt.Errorf("failed to validate labelop: %w", err) + } + } + + tx, err := ddb.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + for _, o := range ops { + _, err = db.AddLabelOp(tx, &o) + if err != nil { + return fmt.Errorf("failed to add labelop: %w", err) + } + } + + if err = tx.Commit(); err != nil { + return err + } + } + + return nil +} diff --git a/appview/labels/labels.go b/appview/labels/labels.go index 309ba1ce..ecd304b4 100644 --- a/appview/labels/labels.go +++ b/appview/labels/labels.go @@ -104,9 +104,6 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { return } - l.logger.Info("actx", "labels", labelAts) - l.logger.Info("actx", "defs", actx.Defs) - // calculate the start state by applying already known labels existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) if err != nil { diff --git a/appview/models/label.go b/appview/models/label.go index 60c99aaa..7c985696 100644 --- a/appview/models/label.go +++ b/appview/models/label.go @@ -1,16 +1,21 @@ package models import ( + "context" "crypto/sha1" "encoding/hex" + "encoding/json" "errors" "fmt" "slices" "time" + "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" "tangled.org/core/api/tangled" "tangled.org/core/consts" + "tangled.org/core/idresolver" ) type ConcreteType string @@ -471,3 +476,65 @@ func DefaultLabelDefs() []string { return defs } + +func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) { + resolved, err := r.ResolveIdent(context.Background(), consts.TangledDid) + if err != nil { + return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", consts.TangledDid, err) + } + pdsEndpoint := resolved.PDSEndpoint() + if pdsEndpoint == "" { + return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", consts.TangledDid) + } + client := &xrpc.Client{ + Host: pdsEndpoint, + } + + var labelDefs []LabelDefinition + + for _, dl := range DefaultLabelDefs() { + atUri := syntax.ATURI(dl) + parsedUri, err := syntax.ParseATURI(string(atUri)) + if err != nil { + return nil, fmt.Errorf("failed to parse AT-URI %s: %v", atUri, err) + } + record, err := atproto.RepoGetRecord( + context.Background(), + client, + "", + parsedUri.Collection().String(), + parsedUri.Authority().String(), + parsedUri.RecordKey().String(), + ) + if err != nil { + return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err) + } + + if record != nil { + bytes, err := record.Value.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err) + } + + raw := json.RawMessage(bytes) + labelRecord := tangled.LabelDefinition{} + err = json.Unmarshal(raw, &labelRecord) + if err != nil { + return nil, fmt.Errorf("invalid record for %s: %w", atUri, err) + } + + labelDef, err := LabelDefinitionFromRecord( + parsedUri.Authority().String(), + parsedUri.RecordKey().String(), + labelRecord, + ) + if err != nil { + return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err) + } + + labelDefs = append(labelDefs, *labelDef) + } + } + + return labelDefs, nil +} diff --git a/appview/pages/pages.go b/appview/pages/pages.go index df37834b..80eee26e 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -834,15 +834,16 @@ func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { } type RepoGeneralSettingsParams struct { - LoggedInUser *oauth.User - RepoInfo repoinfo.RepoInfo - Labels []models.LabelDefinition - DefaultLabels []models.LabelDefinition - SubscribedLabels map[string]struct{} - Active string - Tabs []map[string]any - Tab string - Branches []types.Branch + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Labels []models.LabelDefinition + DefaultLabels []models.LabelDefinition + SubscribedLabels map[string]struct{} + ShouldSubscribeAll bool + Active string + Tabs []map[string]any + Tab string + Branches []types.Branch } func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { diff --git a/appview/pages/templates/repo/settings/general.html b/appview/pages/templates/repo/settings/general.html index 2bae37a0..9afae837 100644 --- a/appview/pages/templates/repo/settings/general.html +++ b/appview/pages/templates/repo/settings/general.html @@ -46,12 +46,42 @@ {{ define "defaultLabelSettings" }}
- Manage your issues and pulls by creating labels to categorize them. Only - repository owners may configure labels. You may choose to subscribe to - default labels, or create entirely custom labels. -
++ Manage your issues and pulls by creating labels to categorize them. Only + repository owners may configure labels. You may choose to subscribe to + default labels, or create entirely custom labels. +
+