package knotfeed import ( "cmp" "encoding/json" "fmt" "log/slog" "strings" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/samber/lo" "tangled.org/core/api/tangled" "tangled.org/core/tapc" ) const ( legacyRefUpdateNSID syntax.NSID = tangled.GitRefUpdateNSID legacyCollaboratorUpdateNSID syntax.NSID = "sh.tangled.repo.collaboratorUpdate" collaboratorInviteCollection syntax.NSID = tangled.RepoCollaboratorInviteNSID ) type legacyAclOp string const ( legacyAclOpAdd legacyAclOp = "add" legacyAclOpRemove legacyAclOp = "remove" ) func (op legacyAclOp) action() tapc.RecordAction { return lo.Ternary(strings.EqualFold(string(op), string(legacyAclOpRemove)), tapc.RecordDeleteAction, tapc.RecordCreateAction) } type legacyFrame struct { Nsid syntax.NSID `json:"nsid"` Created int64 `json:"created"` Event json.RawMessage `json:"event"` } type legacyRefUpdate struct { Repo syntax.DID `json:"repo"` LegacyRepo syntax.DID `json:"repoDid"` // knots older than 1.14 name the repo this way Ref string `json:"ref"` OldSha string `json:"oldSha"` NewSha string `json:"newSha"` CommitterDid string `json:"committerDid"` PushOptions []string `json:"pushOptions"` } type legacyAclUpdate struct { Op legacyAclOp `json:"op"` Repo syntax.DID `json:"repo"` Subject syntax.DID `json:"subject"` } var legacyOps = map[syntax.NSID]func(json.RawMessage, *Commit) error{ legacyRefUpdateNSID: legacyRefOp, legacyCollaboratorUpdateNSID: legacyCollaboratorOp, } func decodeLegacy(data []byte, _ *slog.Logger) (Message, error) { var frame legacyFrame if err := json.Unmarshal(data, &frame); err != nil { return Message{}, fmt.Errorf("legacy frame: %w", err) } if frame.Created <= 0 { return Message{}, fmt.Errorf("legacy %s frame lacks its created stamp: %d", frame.Nsid, frame.Created) } commit := &Commit{Seq: frame.Created} if op, known := legacyOps[frame.Nsid]; known { if err := op(frame.Event, commit); err != nil { return Message{}, err } } return Message{Type: TypeCommit, Commit: commit}, nil } func legacyRefOp(payload json.RawMessage, commit *Commit) error { var event legacyRefUpdate if err := json.Unmarshal(payload, &event); err != nil { return fmt.Errorf("legacy ref update: %w", err) } rkey, short := EscapeRefname(event.Ref) newSha, isObject := ParseObjectID(event.NewSha) // pre-1.13 knots put the owner's did in repoDid, so this can miss on lookup; // the event still has to flow through so the host's cursor keeps moving repo := cmp.Or(event.Repo, event.LegacyRepo) if event.Ref == "" || repo == "" || !short || !isObject { return nil } op := RecordOp{ Action: tapc.RecordDeleteAction, Collection: GitRefCollection, Rkey: rkey, Prior: ParsePriorSha(event.OldSha), } if !newSha.IsZero() { // old knots leave committerDid empty; attribute those pushes to the repo itself editor, err := syntax.ParseDID(event.CommitterDid) if err != nil { editor = repo } record, err := encodeRefRecord(RefRecord{ Sha: newSha, Editor: editor, PushOptions: event.PushOptions, }) if err != nil { return fmt.Errorf("re-encoding legacy ref update: %w", err) } op.Action, op.Bytes = tapc.RecordCreateAction, record } commit.Repo = repo commit.Records = []RecordOp{op} return nil } func legacyCollaboratorOp(payload json.RawMessage, commit *Commit) error { var event legacyAclUpdate if err := json.Unmarshal(payload, &event); err != nil { return fmt.Errorf("legacy collaborator update: %w", err) } if event.Repo == "" || event.Subject == "" { return nil } rkey, err := syntax.ParseRecordKey(event.Subject.String()) if err != nil { return fmt.Errorf("legacy collaborator update names subject %q: %w", event.Subject, err) } commit.Repo = event.Repo commit.Records = []RecordOp{{ Action: event.Op.action(), Collection: collaboratorInviteCollection, Rkey: rkey, }} return nil }