mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 00:34:16 +00:00
152 lines
4.5 KiB
Go
152 lines
4.5 KiB
Go
package pds
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
"github.com/bluesky-social/indigo/repo"
|
|
"github.com/ipfs/go-cid"
|
|
)
|
|
|
|
// AddCrewMember adds a new crew member to the hold and commits to carstore
|
|
func (p *HoldPDS) AddCrewMember(ctx context.Context, memberDID, role string, permissions []string) (cid.Cid, error) {
|
|
crewRecord := &atproto.CrewRecord{
|
|
Type: atproto.CrewCollection,
|
|
Member: memberDID,
|
|
Role: role,
|
|
Permissions: permissions,
|
|
AddedAt: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
// Use repomgr for crew operations - auto-generated rkey is fine
|
|
_, recordCID, err := p.repomgr.CreateRecord(ctx, p.uid, atproto.CrewCollection, crewRecord)
|
|
if err != nil {
|
|
return cid.Undef, fmt.Errorf("failed to create crew record: %w", err)
|
|
}
|
|
|
|
return recordCID, nil
|
|
}
|
|
|
|
// GetCrewMember retrieves a crew member by their record key
|
|
func (p *HoldPDS) GetCrewMember(ctx context.Context, rkey string) (cid.Cid, *atproto.CrewRecord, error) {
|
|
// Use repomgr.GetRecord - our types are registered in init()
|
|
recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, atproto.CrewCollection, rkey, cid.Undef)
|
|
if err != nil {
|
|
return cid.Undef, nil, fmt.Errorf("failed to get crew record: %w", err)
|
|
}
|
|
|
|
// Type assert to our concrete type
|
|
crewRecord, ok := val.(*atproto.CrewRecord)
|
|
if !ok {
|
|
return cid.Undef, nil, fmt.Errorf("unexpected type for crew record: %T", val)
|
|
}
|
|
|
|
return recordCID, crewRecord, nil
|
|
}
|
|
|
|
// CrewMemberWithKey pairs a crew record with its rkey and CID
|
|
type CrewMemberWithKey struct {
|
|
Rkey string
|
|
Cid cid.Cid
|
|
Record *atproto.CrewRecord
|
|
}
|
|
|
|
// ListCrewMembers returns all crew members with their rkeys
|
|
func (p *HoldPDS) ListCrewMembers(ctx context.Context) ([]*CrewMemberWithKey, error) {
|
|
var crew []*CrewMemberWithKey
|
|
|
|
// Create read-only session for ForEach access
|
|
// repomgr doesn't expose ForEach, so we need direct repo access
|
|
session, err := p.carstore.ReadOnlySession(p.uid)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create read-only session: %w", err)
|
|
}
|
|
|
|
// Get repo head
|
|
head, err := p.carstore.GetUserRepoHead(ctx, p.uid)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get repo head: %w", err)
|
|
}
|
|
|
|
if !head.Defined() {
|
|
return nil, fmt.Errorf("repo not initialized")
|
|
}
|
|
|
|
// Open repo
|
|
r, err := repo.OpenRepo(ctx, session, head)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open repo: %w", err)
|
|
}
|
|
|
|
// Iterate over all crew records
|
|
err = r.ForEach(ctx, atproto.CrewCollection, func(k string, v cid.Cid) error {
|
|
// Extract collection and rkey from full path (k is like "io.atcr.hold.crew/3m37dr2ddit22")
|
|
parts := strings.Split(k, "/")
|
|
if len(parts) < 2 {
|
|
return nil // Skip invalid keys
|
|
}
|
|
|
|
// Extract actual collection and rkey
|
|
actualCollection := strings.Join(parts[:len(parts)-1], "/")
|
|
rkey := parts[len(parts)-1]
|
|
|
|
// MST keys are sorted, so once we hit a different collection, stop walking
|
|
if actualCollection != atproto.CrewCollection {
|
|
return repo.ErrDoneIterating
|
|
}
|
|
|
|
// Get the record directly from the repo we already have open
|
|
// (calling GetCrewMember would open a new session unnecessarily)
|
|
recordCID, recBytes, err := r.GetRecordBytes(ctx, k)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get crew record: %w", err)
|
|
}
|
|
|
|
// Unmarshal the CBOR bytes into our concrete type
|
|
var crewRecord atproto.CrewRecord
|
|
if err := crewRecord.UnmarshalCBOR(bytes.NewReader(*recBytes)); err != nil {
|
|
return fmt.Errorf("failed to decode crew record: %w", err)
|
|
}
|
|
|
|
crew = append(crew, &CrewMemberWithKey{
|
|
Rkey: rkey,
|
|
Cid: recordCID,
|
|
Record: &crewRecord,
|
|
})
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
// ErrDoneIterating is expected when we stop walking early
|
|
// Use errors.Is to handle wrapped errors (indigo wraps with %w in MST walk)
|
|
if errors.Is(err, repo.ErrDoneIterating) {
|
|
// Successfully stopped at collection boundary
|
|
} else if strings.Contains(err.Error(), "not found") {
|
|
// If the collection doesn't exist yet (empty repo or no records created),
|
|
// return empty list instead of error
|
|
return []*CrewMemberWithKey{}, nil
|
|
} else {
|
|
return nil, fmt.Errorf("failed to list crew members: %w", err)
|
|
}
|
|
}
|
|
|
|
return crew, nil
|
|
}
|
|
|
|
// RemoveCrewMember removes a crew member
|
|
func (p *HoldPDS) RemoveCrewMember(ctx context.Context, rkey string) error {
|
|
// Use repomgr.DeleteRecord - it will automatically commit!
|
|
// This fixes the bug where deletions weren't being committed
|
|
err := p.repomgr.DeleteRecord(ctx, p.uid, atproto.CrewCollection, rkey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete crew record: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|