Files
2025-10-25 01:13:57 -05:00

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
}