package deliberi import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" 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 } // repoByRepoDid mirrors sh.tangled.repo.getRepoByRepoDid's output, but keeps the // record raw: the owner comes from the uri's authority, so a record we can't // type-decode still resolves an owner. type repoByRepoDid struct { Uri string `json:"uri"` Value json.RawMessage `json:"value"` } // RepoOwner resolves a repo DID to the did that holds its sh.tangled.repo // record, along with the repo's cosmetic name. 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) } owner := syntax.ATURI(out.Uri).Authority().String() if owner == "" { return "", "", fmt.Errorf("no authority in uri %q", out.Uri) } // the name is cosmetic, so a decode failure is not fatal. var rec struct { Name string `json:"name"` } _ = json.Unmarshal(out.Value, &rec) return owner, rec.Name, 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 }