package knotfeed
import (
"cmp"
"encoding/json"
"fmt"
"log/slog"
"strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/samber/lo"
"tangled.org/core/tapc"
)
const (
legacyRefUpdateNSID syntax.NSID = "sh.tangled.git.refUpdate"
legacyCollaboratorUpdateNSID syntax.NSID = "sh.tangled.repo.collaboratorUpdate"
collaboratorInviteCollection syntax.NSID = "sh.tangled.repo.collaboratorInvite"
)
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
}