package deliberi import ( "context" "fmt" "net/http" "strings" "time" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/samber/lo" "tangled.org/core/api/org_tangled" "tangled.org/core/api/tangled" ) type recipientResolver interface { ListRecipients(ctx context.Context, uri string, collection string) ([]string, error) RepoOwner(ctx context.Context, repoDid string) (ownerDid, name string, err error) } type inviteLister interface { ListCollaboratorInvitesBy(ctx context.Context, subjectDid string) (InviteAnswer, error) } type InviteOffer struct { Uri string `json:"uri"` KnotDid string `json:"knot"` RepoDid string `json:"repo"` AddedBy string `json:"addedBy"` CreatedAt time.Time `json:"createdAt"` } type InviteAnswer struct { Offers []InviteOffer `json:"items"` Pending []string `json:"pending"` Truncated bool `json:"truncated"` } type bobbinClient struct { xc *indigoxrpc.Client } func newBobbinClient(apiUrl string) *bobbinClient { return &bobbinClient{ xc: &indigoxrpc.Client{ Host: strings.TrimRight(apiUrl, "/"), Client: &http.Client{Timeout: 10 * time.Second}, }, } } func (c *bobbinClient) ListRecipients(ctx context.Context, uri string, collection string) ([]string, error) { out, err := org_tangled.TempNotificationListRecipients(ctx, c.xc, collection, uri) if err != nil { return nil, fmt.Errorf("calling %s: %w", org_tangled.TempNotificationListRecipientsNSID, err) } return out.Dids, nil } // bobbin serves getRepoByRepoDid from the repo's org.tangled.repo.manifest type repoByRepoDid struct { Value repoManifest `json:"value"` } func (c *bobbinClient) RepoOwner(ctx context.Context, repoDid string) (string, string, error) { var out repoByRepoDid params := map[string]any{"repoDid": repoDid} if err := c.xc.Do(ctx, indigoxrpc.Query, "", tangled.RepoGetRepoByRepoDidNSID, params, nil, &out); err != nil { return "", "", fmt.Errorf("calling %s: %w", tangled.RepoGetRepoByRepoDidNSID, err) } decl := out.Value.Declaration if decl == nil || decl.Owner == "" { return "", "", fmt.Errorf("manifest for %s has no declared owner", repoDid) } return decl.Owner, decl.Slug, nil } const repoListCollaboratorInvitesByNSID = "sh.tangled.repo.listCollaboratorInvitesBy" func (c *bobbinClient) ListCollaboratorInvitesBy(ctx context.Context, subjectDid string) (InviteAnswer, error) { var out InviteAnswer params := map[string]any{"subject": subjectDid} if err := c.xc.Do(ctx, indigoxrpc.Query, "", repoListCollaboratorInvitesByNSID, params, nil, &out); err != nil { return InviteAnswer{}, fmt.Errorf("calling %s: %w", repoListCollaboratorInvitesByNSID, err) } if blank, found := lo.Find(out.Offers, func(offer InviteOffer) bool { return offer.Uri == "" || offer.KnotDid == "" || offer.RepoDid == "" }); found { missing := "record" if blank.Uri != "" { missing = lo.Ternary(blank.KnotDid == "", "knot", "repo") } return InviteAnswer{}, fmt.Errorf( "%s served offer missing its %s, so we couldn't tell it apart from a withdrawal", repoListCollaboratorInvitesByNSID, missing, ) } return out, nil }