feat(cli): ninja plugin gallery commands (Phase 6)

Add `ninja plugin gallery list|add|remove|reorder|set-featured` for
author-side curation of a plugin/theme's registry screenshot gallery,
feeding the browse/detail surfaces shipped in Phases 4-5.

- Re-vendor plugin_registry.proto from orchestrator (adds
  PluginGalleryService) and regenerate the connect client.
- Wire the Gallery client into orchclient.Client.
- Target plugin defaults from plugin.mod with --scope/--name overrides;
  device-flow auth via resolveClient. list is a public read; add/remove/
  reorder/set-featured are RoleUser (scope/account membership enforced
  server-side). add derives content_type from the extension against the
  png/jpg/jpeg/webp allowlist and rejects >8MiB client-side.

Scope handling trims a leading @ client-side because ListScreenshots does
not trim server-side (GetPlugin does). Live-verified against the dev
orchestrator; go build + go test ./... + check-safety all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-05 12:35:09 +08:00
parent 64d81bd93f
commit b5e39121d2
7 changed files with 3903 additions and 392 deletions

View File

@ -37,6 +37,7 @@ func newPluginCmd() *cobra.Command {
newPluginBumpCmd(), newPluginBumpCmd(),
newPluginVersionCmd(), newPluginVersionCmd(),
newPluginTagsCmd(), newPluginTagsCmd(),
newPluginGalleryCmd(),
) )
return c return c
} }

View File

@ -0,0 +1,339 @@
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"connectrpc.com/connect"
"github.com/spf13/cobra"
core "git.dev.alexdunmow.com/block/core/plugin"
v1 "git.dev.alexdunmow.com/block/cli/internal/api/orchestrator/v1"
"git.dev.alexdunmow.com/block/cli/internal/creds"
"git.dev.alexdunmow.com/block/cli/internal/orchclient"
)
// maxScreenshotBytes mirrors the orchestrator's UploadScreenshot read cap
// (connect.WithReadMaxBytes(8<<20)); we reject oversize files client-side so
// the failure is a clear message instead of a truncated-stream RPC error.
const maxScreenshotBytes = 8 << 20
// newPluginGalleryCmd builds the `ninja plugin gallery` command group for
// curating a plugin/theme's registry screenshot gallery. Reads are public;
// writes require scope membership (or owning-account membership for a private
// plugin) — the orchestrator enforces this per-handler. The target plugin
// defaults to the one described by plugin.mod in the working directory; the
// --scope/--name flags override the coordinates so the gallery of any plugin
// you can manage can be curated from anywhere.
func newPluginGalleryCmd() *cobra.Command {
var scopeFlag, nameFlag string
cmd := &cobra.Command{
Use: "gallery",
Short: "Manage a plugin's registry screenshot gallery",
Long: `Curate the screenshot gallery shown on a plugin or theme's registry
detail page.
The target plugin defaults to the one described by plugin.mod in the current
directory. Use --scope/--name to target a different plugin.
Subcommands:
list Show the current gallery (public read)
add <image> Upload an image (png/jpg/jpeg/webp, <=8MiB, <=12 total)
remove <screenshot-id> Delete a screenshot
reorder <id>... Reorder the gallery (must list every current image once)
set-featured <id> Mark one screenshot featured (drives the card preview)`,
}
cmd.PersistentFlags().StringVar(&scopeFlag, "scope", "", "Target scope slug (default: plugin.mod scope)")
cmd.PersistentFlags().StringVar(&nameFlag, "name", "", "Target plugin name (default: plugin.mod name)")
cmd.AddCommand(
newGalleryListCmd(),
newGalleryAddCmd(),
newGalleryRemoveCmd(),
newGalleryReorderCmd(),
newGallerySetFeaturedCmd(),
)
return cmd
}
// galleryTarget is the resolved address of a plugin's gallery: a bare scope
// slug (no leading @), the plugin name, and — for a private plugin — the
// active account it belongs to.
type galleryTarget struct {
scopeSlug string
name string
private bool
accountID string
}
// resolveGalleryTarget derives the target plugin from plugin.mod (when present
// in the working directory) with --scope/--name overrides. A missing plugin.mod
// is not fatal as long as the flags supply scope+name.
func resolveGalleryTarget(c *cobra.Command, hc creds.HostCreds) (galleryTarget, error) {
scopeFlag, _ := c.Flags().GetString("scope")
nameFlag, _ := c.Flags().GetString("name")
var scope, name string
var private bool
if mod, err := readLocalMod(); err == nil {
scope = mod.Plugin.Scope
name = mod.Plugin.Name
private = mod.Plugin.Private
}
// Flag overrides. An explicit --scope names a public scope; the @private
// namespace is reached only via plugin.mod's private flag or by naming the
// private scope directly.
if scopeFlag != "" {
scope = scopeFlag
private = false
}
if nameFlag != "" {
name = nameFlag
}
scopeSlug := strings.TrimPrefix(strings.TrimSpace(scope), "@")
if scopeSlug == core.PrivateScopeSlug {
private = true
}
t := galleryTarget{scopeSlug: scopeSlug, name: strings.TrimSpace(name), private: private, accountID: hc.ActiveAccountID}
if t.name == "" {
return t, fmt.Errorf("no plugin name: run inside a plugin dir (with plugin.mod) or pass --name")
}
if t.private {
t.scopeSlug = core.PrivateScopeSlug
if t.accountID == "" {
return t, fmt.Errorf("private plugin gallery requires an active account; run `ninja account set <slug>`")
}
} else if t.scopeSlug == "" {
return t, fmt.Errorf("no scope: run inside a plugin dir (with plugin.mod) or pass --scope")
}
return t, nil
}
// resolvePluginID looks up the target's registry plugin ID, which every write
// RPC keys on. Mirrors the publish flow's GetPlugin call (private plugins are
// addressed by the @private scope plus the active account).
func (t galleryTarget) resolvePluginID(ctx context.Context, cli *orchclient.Client) (string, error) {
req := &v1.GetPluginRequest{ScopeSlug: t.scopeSlug, Name: t.name}
if t.private {
req.ScopeSlug = core.PrivateScopeSlug
req.ActiveAccountId = t.accountID
}
pr, err := cli.Reg.GetPlugin(ctx, connect.NewRequest(req))
if err != nil {
return "", fmt.Errorf("get plugin: %w", err)
}
return pr.Msg.Plugin.Id, nil
}
func newGalleryListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "Show the current gallery for the target plugin",
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
cli, _, _, hc, err := resolveClient(c)
if err != nil {
return err
}
t, err := resolveGalleryTarget(c, hc)
if err != nil {
return err
}
resp, err := cli.Gallery.ListScreenshots(context.Background(), connect.NewRequest(&v1.ListScreenshotsRequest{
ScopeSlug: t.scopeSlug,
PluginName: t.name,
}))
if err != nil {
return fmt.Errorf("list screenshots: %w", err)
}
shots := resp.Msg.Screenshots
if len(shots) == 0 {
fmt.Println("(no screenshots — add one with `ninja plugin gallery add <image>`)")
return nil
}
for _, s := range shots {
marker := " "
if s.Featured {
marker = "*"
}
fmt.Printf("%2d.%s %s %s %s\n", s.Position, marker, s.Id, s.ContentType, s.Url)
}
fmt.Println("\n(* = featured — drives the registry card preview image)")
return nil
},
}
}
func newGalleryAddCmd() *cobra.Command {
return &cobra.Command{
Use: "add <image>",
Short: "Upload an image to the gallery (png/jpg/jpeg/webp, <=8MiB)",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
cli, _, _, hc, err := resolveClient(c)
if err != nil {
return err
}
t, err := resolveGalleryTarget(c, hc)
if err != nil {
return err
}
path := args[0]
contentType, err := imageContentType(path)
if err != nil {
return err
}
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read image: %w", err)
}
if len(data) == 0 {
return fmt.Errorf("%s is empty", path)
}
if len(data) > maxScreenshotBytes {
return fmt.Errorf("%s is %d bytes; the gallery limit is %d (8MiB)", path, len(data), maxScreenshotBytes)
}
ctx := context.Background()
pluginID, err := t.resolvePluginID(ctx, cli)
if err != nil {
return err
}
resp, err := cli.Gallery.UploadScreenshot(ctx, connect.NewRequest(&v1.UploadScreenshotRequest{
PluginId: pluginID,
Image: data,
Filename: filepath.Base(path),
ContentType: contentType,
}))
if err != nil {
return fmt.Errorf("upload screenshot: %w", err)
}
s := resp.Msg.Screenshot
fmt.Printf("Uploaded %s (id=%s, position=%d)\n", filepath.Base(path), s.Id, s.Position)
return nil
},
}
}
func newGalleryRemoveCmd() *cobra.Command {
return &cobra.Command{
Use: "remove <screenshot-id>",
Short: "Delete a screenshot from the gallery",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
cli, _, _, hc, err := resolveClient(c)
if err != nil {
return err
}
t, err := resolveGalleryTarget(c, hc)
if err != nil {
return err
}
ctx := context.Background()
pluginID, err := t.resolvePluginID(ctx, cli)
if err != nil {
return err
}
if _, err := cli.Gallery.DeleteScreenshot(ctx, connect.NewRequest(&v1.DeleteScreenshotRequest{
PluginId: pluginID,
ScreenshotId: args[0],
})); err != nil {
return fmt.Errorf("delete screenshot: %w", err)
}
fmt.Printf("Deleted screenshot %s\n", args[0])
return nil
},
}
}
func newGalleryReorderCmd() *cobra.Command {
return &cobra.Command{
Use: "reorder <screenshot-id>...",
Short: "Reorder the gallery (list every current screenshot id exactly once)",
Args: cobra.MinimumNArgs(1),
RunE: func(c *cobra.Command, args []string) error {
cli, _, _, hc, err := resolveClient(c)
if err != nil {
return err
}
t, err := resolveGalleryTarget(c, hc)
if err != nil {
return err
}
ctx := context.Background()
pluginID, err := t.resolvePluginID(ctx, cli)
if err != nil {
return err
}
resp, err := cli.Gallery.ReorderScreenshots(ctx, connect.NewRequest(&v1.ReorderScreenshotsRequest{
PluginId: pluginID,
ScreenshotIdsInOrder: args,
}))
if err != nil {
return fmt.Errorf("reorder screenshots: %w", err)
}
fmt.Println("Reordered. New order:")
for _, s := range resp.Msg.Screenshots {
marker := " "
if s.Featured {
marker = "*"
}
fmt.Printf("%2d.%s %s\n", s.Position, marker, s.Id)
}
return nil
},
}
}
func newGallerySetFeaturedCmd() *cobra.Command {
return &cobra.Command{
Use: "set-featured <screenshot-id>",
Short: "Mark one screenshot featured (drives the registry card preview)",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
cli, _, _, hc, err := resolveClient(c)
if err != nil {
return err
}
t, err := resolveGalleryTarget(c, hc)
if err != nil {
return err
}
ctx := context.Background()
pluginID, err := t.resolvePluginID(ctx, cli)
if err != nil {
return err
}
if _, err := cli.Gallery.SetFeaturedScreenshot(ctx, connect.NewRequest(&v1.SetFeaturedScreenshotRequest{
PluginId: pluginID,
ScreenshotId: args[0],
})); err != nil {
return fmt.Errorf("set featured screenshot: %w", err)
}
fmt.Printf("Set screenshot %s as featured\n", args[0])
return nil
},
}
}
// imageContentType maps a file's extension to the content type the gallery
// accepts, rejecting anything outside the png/jpg/jpeg/webp allowlist the
// orchestrator enforces. Detecting from the extension (rather than sniffing
// bytes) keeps the CLI's rejection message aligned with the server's rule.
func imageContentType(path string) (string, error) {
switch strings.ToLower(filepath.Ext(path)) {
case ".png":
return "image/png", nil
case ".jpg", ".jpeg":
return "image/jpeg", nil
case ".webp":
return "image/webp", nil
default:
return "", fmt.Errorf("unsupported image type %q: gallery accepts png, jpg, jpeg, webp", filepath.Ext(path))
}
}

View File

@ -0,0 +1,191 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/spf13/cobra"
core "git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/cli/internal/creds"
)
func TestImageContentType(t *testing.T) {
cases := []struct {
path string
want string
wantErr bool
}{
{path: "preview.png", want: "image/png"},
{path: "shot.jpg", want: "image/jpeg"},
{path: "shot.jpeg", want: "image/jpeg"},
{path: "shot.webp", want: "image/webp"},
{path: "SHOT.PNG", want: "image/png"}, // case-insensitive
{path: "dir/nested/x.jpg", want: "image/jpeg"},
{path: "shot.gif", wantErr: true},
{path: "shot.svg", wantErr: true},
{path: "noext", wantErr: true},
}
for _, c := range cases {
got, err := imageContentType(c.path)
if c.wantErr {
if err == nil {
t.Errorf("imageContentType(%q) = %q, want error", c.path, got)
}
continue
}
if err != nil {
t.Errorf("imageContentType(%q) err: %v", c.path, err)
continue
}
if got != c.want {
t.Errorf("imageContentType(%q) = %q, want %q", c.path, got, c.want)
}
}
}
// galleryTestCmd builds a command carrying the --scope/--name flags exactly as
// the gallery parent registers them, so resolveGalleryTarget can read them.
func galleryTestCmd(scope, name string) *cobra.Command {
c := &cobra.Command{Use: "gallery"}
c.Flags().String("scope", "", "")
c.Flags().String("name", "", "")
if scope != "" {
_ = c.Flags().Set("scope", scope)
}
if name != "" {
_ = c.Flags().Set("name", name)
}
return c
}
func writeGalleryMod(t *testing.T, dir, scope, name string, private bool) {
t.Helper()
m := &core.ModFile{Plugin: core.ModPlugin{
Name: name,
Scope: scope,
Version: "0.1.0",
Private: private,
}}
if err := writeMod(filepath.Join(dir, "plugin.mod"), m); err != nil {
t.Fatalf("writeMod: %v", err)
}
}
func TestResolveGalleryTarget_FromMod(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
writeGalleryMod(t, dir, "@themes", "gotham", false)
got, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{})
if err != nil {
t.Fatalf("resolveGalleryTarget: %v", err)
}
// Leading @ must be trimmed (ListScreenshots does not trim server-side).
if got.scopeSlug != "themes" || got.name != "gotham" || got.private {
t.Errorf("got %+v, want scopeSlug=themes name=gotham private=false", got)
}
}
func TestResolveGalleryTarget_FlagOverride(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
writeGalleryMod(t, dir, "@themes", "gotham", false)
got, err := resolveGalleryTarget(galleryTestCmd("acme", "widget"), creds.HostCreds{})
if err != nil {
t.Fatalf("resolveGalleryTarget: %v", err)
}
if got.scopeSlug != "acme" || got.name != "widget" {
t.Errorf("got %+v, want scopeSlug=acme name=widget", got)
}
}
func TestResolveGalleryTarget_NoModNeedsFlags(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir) // no plugin.mod written
// Flags supply both coordinates → resolves without a plugin.mod.
got, err := resolveGalleryTarget(galleryTestCmd("acme", "widget"), creds.HostCreds{})
if err != nil {
t.Fatalf("resolveGalleryTarget with flags: %v", err)
}
if got.scopeSlug != "acme" || got.name != "widget" {
t.Errorf("got %+v, want scopeSlug=acme name=widget", got)
}
// No plugin.mod and no flags → an actionable error.
if _, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{}); err == nil {
t.Error("expected error with no plugin.mod and no flags")
}
}
func TestResolveGalleryTarget_MissingScope(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
// Name but no scope (public) → error naming the missing scope.
_, err := resolveGalleryTarget(galleryTestCmd("", "widget"), creds.HostCreds{})
if err == nil {
t.Fatal("expected error for name without scope")
}
}
func TestResolveGalleryTarget_PrivateFromMod(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
writeGalleryMod(t, dir, "", "widget", true)
// Private plugin without an active account → error.
if _, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{}); err == nil {
t.Error("expected error: private gallery needs an active account")
}
// With an active account, the scope resolves to the @private namespace.
got, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{ActiveAccountID: "acct-1"})
if err != nil {
t.Fatalf("resolveGalleryTarget private: %v", err)
}
if !got.private || got.scopeSlug != core.PrivateScopeSlug || got.accountID != "acct-1" {
t.Errorf("got %+v, want private=true scopeSlug=%s accountID=acct-1", got, core.PrivateScopeSlug)
}
}
func TestResolveGalleryTarget_PrivateScopeNameImpliesPrivate(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
// Naming the private scope directly flips private on (and still needs an account).
got, err := resolveGalleryTarget(galleryTestCmd("@"+core.PrivateScopeSlug, "widget"), creds.HostCreds{ActiveAccountID: "acct-1"})
if err != nil {
t.Fatalf("resolveGalleryTarget: %v", err)
}
if !got.private || got.scopeSlug != core.PrivateScopeSlug {
t.Errorf("got %+v, want private=true scopeSlug=%s", got, core.PrivateScopeSlug)
}
}
func TestResolveGalleryTarget_ReadFileHelper(t *testing.T) {
// Guard that a valid image on disk round-trips through the same os.ReadFile
// path `add` uses (content-type derived from the extension).
dir := t.TempDir()
path := filepath.Join(dir, "preview.png")
if err := os.WriteFile(path, []byte("\x89PNG\r\n\x1a\nfake"), 0o644); err != nil {
t.Fatal(err)
}
ct, err := imageContentType(path)
if err != nil {
t.Fatalf("imageContentType: %v", err)
}
if ct != "image/png" {
t.Errorf("content type = %q, want image/png", ct)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
if len(data) == 0 || len(data) > maxScreenshotBytes {
t.Errorf("unexpected size %d", len(data))
}
}

View File

@ -27,6 +27,10 @@ const (
PluginRegistryServiceName = "orchestrator.v1.PluginRegistryService" PluginRegistryServiceName = "orchestrator.v1.PluginRegistryService"
// PluginModerationServiceName is the fully-qualified name of the PluginModerationService service. // PluginModerationServiceName is the fully-qualified name of the PluginModerationService service.
PluginModerationServiceName = "orchestrator.v1.PluginModerationService" PluginModerationServiceName = "orchestrator.v1.PluginModerationService"
// PluginGalleryServiceName is the fully-qualified name of the PluginGalleryService service.
PluginGalleryServiceName = "orchestrator.v1.PluginGalleryService"
// PluginReviewServiceName is the fully-qualified name of the PluginReviewService service.
PluginReviewServiceName = "orchestrator.v1.PluginReviewService"
// PluginPublishServiceName is the fully-qualified name of the PluginPublishService service. // PluginPublishServiceName is the fully-qualified name of the PluginPublishService service.
PluginPublishServiceName = "orchestrator.v1.PluginPublishService" PluginPublishServiceName = "orchestrator.v1.PluginPublishService"
// PluginAuthServiceName is the fully-qualified name of the PluginAuthService service. // PluginAuthServiceName is the fully-qualified name of the PluginAuthService service.
@ -107,6 +111,51 @@ const (
// PluginModerationServiceRequestChangesProcedure is the fully-qualified name of the // PluginModerationServiceRequestChangesProcedure is the fully-qualified name of the
// PluginModerationService's RequestChanges RPC. // PluginModerationService's RequestChanges RPC.
PluginModerationServiceRequestChangesProcedure = "/orchestrator.v1.PluginModerationService/RequestChanges" PluginModerationServiceRequestChangesProcedure = "/orchestrator.v1.PluginModerationService/RequestChanges"
// PluginModerationServiceListFlaggedReviewsProcedure is the fully-qualified name of the
// PluginModerationService's ListFlaggedReviews RPC.
PluginModerationServiceListFlaggedReviewsProcedure = "/orchestrator.v1.PluginModerationService/ListFlaggedReviews"
// PluginModerationServiceSoftDeleteReviewProcedure is the fully-qualified name of the
// PluginModerationService's SoftDeleteReview RPC.
PluginModerationServiceSoftDeleteReviewProcedure = "/orchestrator.v1.PluginModerationService/SoftDeleteReview"
// PluginModerationServiceSoftDeleteReplyProcedure is the fully-qualified name of the
// PluginModerationService's SoftDeleteReply RPC.
PluginModerationServiceSoftDeleteReplyProcedure = "/orchestrator.v1.PluginModerationService/SoftDeleteReply"
// PluginGalleryServiceListScreenshotsProcedure is the fully-qualified name of the
// PluginGalleryService's ListScreenshots RPC.
PluginGalleryServiceListScreenshotsProcedure = "/orchestrator.v1.PluginGalleryService/ListScreenshots"
// PluginGalleryServiceUploadScreenshotProcedure is the fully-qualified name of the
// PluginGalleryService's UploadScreenshot RPC.
PluginGalleryServiceUploadScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/UploadScreenshot"
// PluginGalleryServiceDeleteScreenshotProcedure is the fully-qualified name of the
// PluginGalleryService's DeleteScreenshot RPC.
PluginGalleryServiceDeleteScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/DeleteScreenshot"
// PluginGalleryServiceReorderScreenshotsProcedure is the fully-qualified name of the
// PluginGalleryService's ReorderScreenshots RPC.
PluginGalleryServiceReorderScreenshotsProcedure = "/orchestrator.v1.PluginGalleryService/ReorderScreenshots"
// PluginGalleryServiceSetFeaturedScreenshotProcedure is the fully-qualified name of the
// PluginGalleryService's SetFeaturedScreenshot RPC.
PluginGalleryServiceSetFeaturedScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/SetFeaturedScreenshot"
// PluginReviewServiceListReviewsProcedure is the fully-qualified name of the PluginReviewService's
// ListReviews RPC.
PluginReviewServiceListReviewsProcedure = "/orchestrator.v1.PluginReviewService/ListReviews"
// PluginReviewServiceGetMyReviewProcedure is the fully-qualified name of the PluginReviewService's
// GetMyReview RPC.
PluginReviewServiceGetMyReviewProcedure = "/orchestrator.v1.PluginReviewService/GetMyReview"
// PluginReviewServiceUpsertReviewProcedure is the fully-qualified name of the PluginReviewService's
// UpsertReview RPC.
PluginReviewServiceUpsertReviewProcedure = "/orchestrator.v1.PluginReviewService/UpsertReview"
// PluginReviewServiceDeleteReviewProcedure is the fully-qualified name of the PluginReviewService's
// DeleteReview RPC.
PluginReviewServiceDeleteReviewProcedure = "/orchestrator.v1.PluginReviewService/DeleteReview"
// PluginReviewServiceReplyToReviewProcedure is the fully-qualified name of the
// PluginReviewService's ReplyToReview RPC.
PluginReviewServiceReplyToReviewProcedure = "/orchestrator.v1.PluginReviewService/ReplyToReview"
// PluginReviewServiceDeleteReplyProcedure is the fully-qualified name of the PluginReviewService's
// DeleteReply RPC.
PluginReviewServiceDeleteReplyProcedure = "/orchestrator.v1.PluginReviewService/DeleteReply"
// PluginReviewServiceFlagReviewProcedure is the fully-qualified name of the PluginReviewService's
// FlagReview RPC.
PluginReviewServiceFlagReviewProcedure = "/orchestrator.v1.PluginReviewService/FlagReview"
// PluginPublishServicePublishVersionProcedure is the fully-qualified name of the // PluginPublishServicePublishVersionProcedure is the fully-qualified name of the
// PluginPublishService's PublishVersion RPC. // PluginPublishService's PublishVersion RPC.
PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion" PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion"
@ -714,6 +763,11 @@ type PluginModerationServiceClient interface {
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error) ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error) RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error) RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
// Star-review moderation (ADR 0021). Unrelated to the submission queue
// above: these act on user reviews (PluginReviewService), not plugins.
ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error)
SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error)
SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error)
} }
// NewPluginModerationServiceClient constructs a client for the // NewPluginModerationServiceClient constructs a client for the
@ -751,6 +805,24 @@ func NewPluginModerationServiceClient(httpClient connect.HTTPClient, baseURL str
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")), connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
connect.WithClientOptions(opts...), connect.WithClientOptions(opts...),
), ),
listFlaggedReviews: connect.NewClient[v1.ListFlaggedReviewsRequest, v1.ListFlaggedReviewsResponse](
httpClient,
baseURL+PluginModerationServiceListFlaggedReviewsProcedure,
connect.WithSchema(pluginModerationServiceMethods.ByName("ListFlaggedReviews")),
connect.WithClientOptions(opts...),
),
softDeleteReview: connect.NewClient[v1.SoftDeleteReviewRequest, v1.SoftDeleteReviewResponse](
httpClient,
baseURL+PluginModerationServiceSoftDeleteReviewProcedure,
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReview")),
connect.WithClientOptions(opts...),
),
softDeleteReply: connect.NewClient[v1.SoftDeleteReplyRequest, v1.SoftDeleteReplyResponse](
httpClient,
baseURL+PluginModerationServiceSoftDeleteReplyProcedure,
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReply")),
connect.WithClientOptions(opts...),
),
} }
} }
@ -760,6 +832,9 @@ type pluginModerationServiceClient struct {
approveSubmission *connect.Client[v1.ApproveSubmissionRequest, v1.ApproveSubmissionResponse] approveSubmission *connect.Client[v1.ApproveSubmissionRequest, v1.ApproveSubmissionResponse]
rejectSubmission *connect.Client[v1.RejectSubmissionRequest, v1.RejectSubmissionResponse] rejectSubmission *connect.Client[v1.RejectSubmissionRequest, v1.RejectSubmissionResponse]
requestChanges *connect.Client[v1.RequestChangesRequest, v1.RequestChangesResponse] requestChanges *connect.Client[v1.RequestChangesRequest, v1.RequestChangesResponse]
listFlaggedReviews *connect.Client[v1.ListFlaggedReviewsRequest, v1.ListFlaggedReviewsResponse]
softDeleteReview *connect.Client[v1.SoftDeleteReviewRequest, v1.SoftDeleteReviewResponse]
softDeleteReply *connect.Client[v1.SoftDeleteReplyRequest, v1.SoftDeleteReplyResponse]
} }
// ListPendingReviews calls orchestrator.v1.PluginModerationService.ListPendingReviews. // ListPendingReviews calls orchestrator.v1.PluginModerationService.ListPendingReviews.
@ -782,6 +857,21 @@ func (c *pluginModerationServiceClient) RequestChanges(ctx context.Context, req
return c.requestChanges.CallUnary(ctx, req) return c.requestChanges.CallUnary(ctx, req)
} }
// ListFlaggedReviews calls orchestrator.v1.PluginModerationService.ListFlaggedReviews.
func (c *pluginModerationServiceClient) ListFlaggedReviews(ctx context.Context, req *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error) {
return c.listFlaggedReviews.CallUnary(ctx, req)
}
// SoftDeleteReview calls orchestrator.v1.PluginModerationService.SoftDeleteReview.
func (c *pluginModerationServiceClient) SoftDeleteReview(ctx context.Context, req *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error) {
return c.softDeleteReview.CallUnary(ctx, req)
}
// SoftDeleteReply calls orchestrator.v1.PluginModerationService.SoftDeleteReply.
func (c *pluginModerationServiceClient) SoftDeleteReply(ctx context.Context, req *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error) {
return c.softDeleteReply.CallUnary(ctx, req)
}
// PluginModerationServiceHandler is an implementation of the // PluginModerationServiceHandler is an implementation of the
// orchestrator.v1.PluginModerationService service. // orchestrator.v1.PluginModerationService service.
type PluginModerationServiceHandler interface { type PluginModerationServiceHandler interface {
@ -789,6 +879,11 @@ type PluginModerationServiceHandler interface {
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error) ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error) RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error) RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
// Star-review moderation (ADR 0021). Unrelated to the submission queue
// above: these act on user reviews (PluginReviewService), not plugins.
ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error)
SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error)
SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error)
} }
// NewPluginModerationServiceHandler builds an HTTP handler from the service implementation. It // NewPluginModerationServiceHandler builds an HTTP handler from the service implementation. It
@ -822,6 +917,24 @@ func NewPluginModerationServiceHandler(svc PluginModerationServiceHandler, opts
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")), connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
pluginModerationServiceListFlaggedReviewsHandler := connect.NewUnaryHandler(
PluginModerationServiceListFlaggedReviewsProcedure,
svc.ListFlaggedReviews,
connect.WithSchema(pluginModerationServiceMethods.ByName("ListFlaggedReviews")),
connect.WithHandlerOptions(opts...),
)
pluginModerationServiceSoftDeleteReviewHandler := connect.NewUnaryHandler(
PluginModerationServiceSoftDeleteReviewProcedure,
svc.SoftDeleteReview,
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReview")),
connect.WithHandlerOptions(opts...),
)
pluginModerationServiceSoftDeleteReplyHandler := connect.NewUnaryHandler(
PluginModerationServiceSoftDeleteReplyProcedure,
svc.SoftDeleteReply,
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReply")),
connect.WithHandlerOptions(opts...),
)
return "/orchestrator.v1.PluginModerationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return "/orchestrator.v1.PluginModerationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case PluginModerationServiceListPendingReviewsProcedure: case PluginModerationServiceListPendingReviewsProcedure:
@ -832,6 +945,12 @@ func NewPluginModerationServiceHandler(svc PluginModerationServiceHandler, opts
pluginModerationServiceRejectSubmissionHandler.ServeHTTP(w, r) pluginModerationServiceRejectSubmissionHandler.ServeHTTP(w, r)
case PluginModerationServiceRequestChangesProcedure: case PluginModerationServiceRequestChangesProcedure:
pluginModerationServiceRequestChangesHandler.ServeHTTP(w, r) pluginModerationServiceRequestChangesHandler.ServeHTTP(w, r)
case PluginModerationServiceListFlaggedReviewsProcedure:
pluginModerationServiceListFlaggedReviewsHandler.ServeHTTP(w, r)
case PluginModerationServiceSoftDeleteReviewProcedure:
pluginModerationServiceSoftDeleteReviewHandler.ServeHTTP(w, r)
case PluginModerationServiceSoftDeleteReplyProcedure:
pluginModerationServiceSoftDeleteReplyHandler.ServeHTTP(w, r)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@ -857,6 +976,420 @@ func (UnimplementedPluginModerationServiceHandler) RequestChanges(context.Contex
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.RequestChanges is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.RequestChanges is not implemented"))
} }
func (UnimplementedPluginModerationServiceHandler) ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.ListFlaggedReviews is not implemented"))
}
func (UnimplementedPluginModerationServiceHandler) SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.SoftDeleteReview is not implemented"))
}
func (UnimplementedPluginModerationServiceHandler) SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.SoftDeleteReply is not implemented"))
}
// PluginGalleryServiceClient is a client for the orchestrator.v1.PluginGalleryService service.
type PluginGalleryServiceClient interface {
ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error)
UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error)
DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error)
ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error)
SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error)
}
// NewPluginGalleryServiceClient constructs a client for the orchestrator.v1.PluginGalleryService
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewPluginGalleryServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginGalleryServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
pluginGalleryServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginGalleryService").Methods()
return &pluginGalleryServiceClient{
listScreenshots: connect.NewClient[v1.ListScreenshotsRequest, v1.ListScreenshotsResponse](
httpClient,
baseURL+PluginGalleryServiceListScreenshotsProcedure,
connect.WithSchema(pluginGalleryServiceMethods.ByName("ListScreenshots")),
connect.WithClientOptions(opts...),
),
uploadScreenshot: connect.NewClient[v1.UploadScreenshotRequest, v1.UploadScreenshotResponse](
httpClient,
baseURL+PluginGalleryServiceUploadScreenshotProcedure,
connect.WithSchema(pluginGalleryServiceMethods.ByName("UploadScreenshot")),
connect.WithClientOptions(opts...),
),
deleteScreenshot: connect.NewClient[v1.DeleteScreenshotRequest, v1.DeleteScreenshotResponse](
httpClient,
baseURL+PluginGalleryServiceDeleteScreenshotProcedure,
connect.WithSchema(pluginGalleryServiceMethods.ByName("DeleteScreenshot")),
connect.WithClientOptions(opts...),
),
reorderScreenshots: connect.NewClient[v1.ReorderScreenshotsRequest, v1.ReorderScreenshotsResponse](
httpClient,
baseURL+PluginGalleryServiceReorderScreenshotsProcedure,
connect.WithSchema(pluginGalleryServiceMethods.ByName("ReorderScreenshots")),
connect.WithClientOptions(opts...),
),
setFeaturedScreenshot: connect.NewClient[v1.SetFeaturedScreenshotRequest, v1.SetFeaturedScreenshotResponse](
httpClient,
baseURL+PluginGalleryServiceSetFeaturedScreenshotProcedure,
connect.WithSchema(pluginGalleryServiceMethods.ByName("SetFeaturedScreenshot")),
connect.WithClientOptions(opts...),
),
}
}
// pluginGalleryServiceClient implements PluginGalleryServiceClient.
type pluginGalleryServiceClient struct {
listScreenshots *connect.Client[v1.ListScreenshotsRequest, v1.ListScreenshotsResponse]
uploadScreenshot *connect.Client[v1.UploadScreenshotRequest, v1.UploadScreenshotResponse]
deleteScreenshot *connect.Client[v1.DeleteScreenshotRequest, v1.DeleteScreenshotResponse]
reorderScreenshots *connect.Client[v1.ReorderScreenshotsRequest, v1.ReorderScreenshotsResponse]
setFeaturedScreenshot *connect.Client[v1.SetFeaturedScreenshotRequest, v1.SetFeaturedScreenshotResponse]
}
// ListScreenshots calls orchestrator.v1.PluginGalleryService.ListScreenshots.
func (c *pluginGalleryServiceClient) ListScreenshots(ctx context.Context, req *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error) {
return c.listScreenshots.CallUnary(ctx, req)
}
// UploadScreenshot calls orchestrator.v1.PluginGalleryService.UploadScreenshot.
func (c *pluginGalleryServiceClient) UploadScreenshot(ctx context.Context, req *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error) {
return c.uploadScreenshot.CallUnary(ctx, req)
}
// DeleteScreenshot calls orchestrator.v1.PluginGalleryService.DeleteScreenshot.
func (c *pluginGalleryServiceClient) DeleteScreenshot(ctx context.Context, req *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error) {
return c.deleteScreenshot.CallUnary(ctx, req)
}
// ReorderScreenshots calls orchestrator.v1.PluginGalleryService.ReorderScreenshots.
func (c *pluginGalleryServiceClient) ReorderScreenshots(ctx context.Context, req *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error) {
return c.reorderScreenshots.CallUnary(ctx, req)
}
// SetFeaturedScreenshot calls orchestrator.v1.PluginGalleryService.SetFeaturedScreenshot.
func (c *pluginGalleryServiceClient) SetFeaturedScreenshot(ctx context.Context, req *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error) {
return c.setFeaturedScreenshot.CallUnary(ctx, req)
}
// PluginGalleryServiceHandler is an implementation of the orchestrator.v1.PluginGalleryService
// service.
type PluginGalleryServiceHandler interface {
ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error)
UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error)
DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error)
ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error)
SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error)
}
// NewPluginGalleryServiceHandler builds an HTTP handler from the service implementation. It returns
// the path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewPluginGalleryServiceHandler(svc PluginGalleryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
pluginGalleryServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginGalleryService").Methods()
pluginGalleryServiceListScreenshotsHandler := connect.NewUnaryHandler(
PluginGalleryServiceListScreenshotsProcedure,
svc.ListScreenshots,
connect.WithSchema(pluginGalleryServiceMethods.ByName("ListScreenshots")),
connect.WithHandlerOptions(opts...),
)
pluginGalleryServiceUploadScreenshotHandler := connect.NewUnaryHandler(
PluginGalleryServiceUploadScreenshotProcedure,
svc.UploadScreenshot,
connect.WithSchema(pluginGalleryServiceMethods.ByName("UploadScreenshot")),
connect.WithHandlerOptions(opts...),
)
pluginGalleryServiceDeleteScreenshotHandler := connect.NewUnaryHandler(
PluginGalleryServiceDeleteScreenshotProcedure,
svc.DeleteScreenshot,
connect.WithSchema(pluginGalleryServiceMethods.ByName("DeleteScreenshot")),
connect.WithHandlerOptions(opts...),
)
pluginGalleryServiceReorderScreenshotsHandler := connect.NewUnaryHandler(
PluginGalleryServiceReorderScreenshotsProcedure,
svc.ReorderScreenshots,
connect.WithSchema(pluginGalleryServiceMethods.ByName("ReorderScreenshots")),
connect.WithHandlerOptions(opts...),
)
pluginGalleryServiceSetFeaturedScreenshotHandler := connect.NewUnaryHandler(
PluginGalleryServiceSetFeaturedScreenshotProcedure,
svc.SetFeaturedScreenshot,
connect.WithSchema(pluginGalleryServiceMethods.ByName("SetFeaturedScreenshot")),
connect.WithHandlerOptions(opts...),
)
return "/orchestrator.v1.PluginGalleryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case PluginGalleryServiceListScreenshotsProcedure:
pluginGalleryServiceListScreenshotsHandler.ServeHTTP(w, r)
case PluginGalleryServiceUploadScreenshotProcedure:
pluginGalleryServiceUploadScreenshotHandler.ServeHTTP(w, r)
case PluginGalleryServiceDeleteScreenshotProcedure:
pluginGalleryServiceDeleteScreenshotHandler.ServeHTTP(w, r)
case PluginGalleryServiceReorderScreenshotsProcedure:
pluginGalleryServiceReorderScreenshotsHandler.ServeHTTP(w, r)
case PluginGalleryServiceSetFeaturedScreenshotProcedure:
pluginGalleryServiceSetFeaturedScreenshotHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedPluginGalleryServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedPluginGalleryServiceHandler struct{}
func (UnimplementedPluginGalleryServiceHandler) ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.ListScreenshots is not implemented"))
}
func (UnimplementedPluginGalleryServiceHandler) UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.UploadScreenshot is not implemented"))
}
func (UnimplementedPluginGalleryServiceHandler) DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.DeleteScreenshot is not implemented"))
}
func (UnimplementedPluginGalleryServiceHandler) ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.ReorderScreenshots is not implemented"))
}
func (UnimplementedPluginGalleryServiceHandler) SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.SetFeaturedScreenshot is not implemented"))
}
// PluginReviewServiceClient is a client for the orchestrator.v1.PluginReviewService service.
type PluginReviewServiceClient interface {
ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error)
GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error)
UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error)
DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error)
ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error)
DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error)
FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error)
}
// NewPluginReviewServiceClient constructs a client for the orchestrator.v1.PluginReviewService
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewPluginReviewServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginReviewServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
pluginReviewServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginReviewService").Methods()
return &pluginReviewServiceClient{
listReviews: connect.NewClient[v1.ListReviewsRequest, v1.ListReviewsResponse](
httpClient,
baseURL+PluginReviewServiceListReviewsProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("ListReviews")),
connect.WithClientOptions(opts...),
),
getMyReview: connect.NewClient[v1.GetMyReviewRequest, v1.GetMyReviewResponse](
httpClient,
baseURL+PluginReviewServiceGetMyReviewProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("GetMyReview")),
connect.WithClientOptions(opts...),
),
upsertReview: connect.NewClient[v1.UpsertReviewRequest, v1.UpsertReviewResponse](
httpClient,
baseURL+PluginReviewServiceUpsertReviewProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("UpsertReview")),
connect.WithClientOptions(opts...),
),
deleteReview: connect.NewClient[v1.DeleteReviewRequest, v1.DeleteReviewResponse](
httpClient,
baseURL+PluginReviewServiceDeleteReviewProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReview")),
connect.WithClientOptions(opts...),
),
replyToReview: connect.NewClient[v1.ReplyToReviewRequest, v1.ReplyToReviewResponse](
httpClient,
baseURL+PluginReviewServiceReplyToReviewProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("ReplyToReview")),
connect.WithClientOptions(opts...),
),
deleteReply: connect.NewClient[v1.DeleteReplyRequest, v1.DeleteReplyResponse](
httpClient,
baseURL+PluginReviewServiceDeleteReplyProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReply")),
connect.WithClientOptions(opts...),
),
flagReview: connect.NewClient[v1.FlagReviewRequest, v1.FlagReviewResponse](
httpClient,
baseURL+PluginReviewServiceFlagReviewProcedure,
connect.WithSchema(pluginReviewServiceMethods.ByName("FlagReview")),
connect.WithClientOptions(opts...),
),
}
}
// pluginReviewServiceClient implements PluginReviewServiceClient.
type pluginReviewServiceClient struct {
listReviews *connect.Client[v1.ListReviewsRequest, v1.ListReviewsResponse]
getMyReview *connect.Client[v1.GetMyReviewRequest, v1.GetMyReviewResponse]
upsertReview *connect.Client[v1.UpsertReviewRequest, v1.UpsertReviewResponse]
deleteReview *connect.Client[v1.DeleteReviewRequest, v1.DeleteReviewResponse]
replyToReview *connect.Client[v1.ReplyToReviewRequest, v1.ReplyToReviewResponse]
deleteReply *connect.Client[v1.DeleteReplyRequest, v1.DeleteReplyResponse]
flagReview *connect.Client[v1.FlagReviewRequest, v1.FlagReviewResponse]
}
// ListReviews calls orchestrator.v1.PluginReviewService.ListReviews.
func (c *pluginReviewServiceClient) ListReviews(ctx context.Context, req *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error) {
return c.listReviews.CallUnary(ctx, req)
}
// GetMyReview calls orchestrator.v1.PluginReviewService.GetMyReview.
func (c *pluginReviewServiceClient) GetMyReview(ctx context.Context, req *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error) {
return c.getMyReview.CallUnary(ctx, req)
}
// UpsertReview calls orchestrator.v1.PluginReviewService.UpsertReview.
func (c *pluginReviewServiceClient) UpsertReview(ctx context.Context, req *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error) {
return c.upsertReview.CallUnary(ctx, req)
}
// DeleteReview calls orchestrator.v1.PluginReviewService.DeleteReview.
func (c *pluginReviewServiceClient) DeleteReview(ctx context.Context, req *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error) {
return c.deleteReview.CallUnary(ctx, req)
}
// ReplyToReview calls orchestrator.v1.PluginReviewService.ReplyToReview.
func (c *pluginReviewServiceClient) ReplyToReview(ctx context.Context, req *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error) {
return c.replyToReview.CallUnary(ctx, req)
}
// DeleteReply calls orchestrator.v1.PluginReviewService.DeleteReply.
func (c *pluginReviewServiceClient) DeleteReply(ctx context.Context, req *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error) {
return c.deleteReply.CallUnary(ctx, req)
}
// FlagReview calls orchestrator.v1.PluginReviewService.FlagReview.
func (c *pluginReviewServiceClient) FlagReview(ctx context.Context, req *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error) {
return c.flagReview.CallUnary(ctx, req)
}
// PluginReviewServiceHandler is an implementation of the orchestrator.v1.PluginReviewService
// service.
type PluginReviewServiceHandler interface {
ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error)
GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error)
UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error)
DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error)
ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error)
DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error)
FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error)
}
// NewPluginReviewServiceHandler builds an HTTP handler from the service implementation. It returns
// the path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewPluginReviewServiceHandler(svc PluginReviewServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
pluginReviewServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginReviewService").Methods()
pluginReviewServiceListReviewsHandler := connect.NewUnaryHandler(
PluginReviewServiceListReviewsProcedure,
svc.ListReviews,
connect.WithSchema(pluginReviewServiceMethods.ByName("ListReviews")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceGetMyReviewHandler := connect.NewUnaryHandler(
PluginReviewServiceGetMyReviewProcedure,
svc.GetMyReview,
connect.WithSchema(pluginReviewServiceMethods.ByName("GetMyReview")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceUpsertReviewHandler := connect.NewUnaryHandler(
PluginReviewServiceUpsertReviewProcedure,
svc.UpsertReview,
connect.WithSchema(pluginReviewServiceMethods.ByName("UpsertReview")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceDeleteReviewHandler := connect.NewUnaryHandler(
PluginReviewServiceDeleteReviewProcedure,
svc.DeleteReview,
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReview")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceReplyToReviewHandler := connect.NewUnaryHandler(
PluginReviewServiceReplyToReviewProcedure,
svc.ReplyToReview,
connect.WithSchema(pluginReviewServiceMethods.ByName("ReplyToReview")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceDeleteReplyHandler := connect.NewUnaryHandler(
PluginReviewServiceDeleteReplyProcedure,
svc.DeleteReply,
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReply")),
connect.WithHandlerOptions(opts...),
)
pluginReviewServiceFlagReviewHandler := connect.NewUnaryHandler(
PluginReviewServiceFlagReviewProcedure,
svc.FlagReview,
connect.WithSchema(pluginReviewServiceMethods.ByName("FlagReview")),
connect.WithHandlerOptions(opts...),
)
return "/orchestrator.v1.PluginReviewService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case PluginReviewServiceListReviewsProcedure:
pluginReviewServiceListReviewsHandler.ServeHTTP(w, r)
case PluginReviewServiceGetMyReviewProcedure:
pluginReviewServiceGetMyReviewHandler.ServeHTTP(w, r)
case PluginReviewServiceUpsertReviewProcedure:
pluginReviewServiceUpsertReviewHandler.ServeHTTP(w, r)
case PluginReviewServiceDeleteReviewProcedure:
pluginReviewServiceDeleteReviewHandler.ServeHTTP(w, r)
case PluginReviewServiceReplyToReviewProcedure:
pluginReviewServiceReplyToReviewHandler.ServeHTTP(w, r)
case PluginReviewServiceDeleteReplyProcedure:
pluginReviewServiceDeleteReplyHandler.ServeHTTP(w, r)
case PluginReviewServiceFlagReviewProcedure:
pluginReviewServiceFlagReviewHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedPluginReviewServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedPluginReviewServiceHandler struct{}
func (UnimplementedPluginReviewServiceHandler) ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.ListReviews is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.GetMyReview is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.UpsertReview is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.DeleteReview is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.ReplyToReview is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.DeleteReply is not implemented"))
}
func (UnimplementedPluginReviewServiceHandler) FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.FlagReview is not implemented"))
}
// PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service. // PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service.
type PluginPublishServiceClient interface { type PluginPublishServiceClient interface {
PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error) PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error)

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,11 @@ import (
type Client struct { type Client struct {
Host string Host string
Token string Token string
Auth orchestratorv1connect.PluginAuthServiceClient Auth orchestratorv1connect.PluginAuthServiceClient
Scope orchestratorv1connect.PluginScopeServiceClient Scope orchestratorv1connect.PluginScopeServiceClient
Reg orchestratorv1connect.PluginRegistryServiceClient Reg orchestratorv1connect.PluginRegistryServiceClient
Pub orchestratorv1connect.PluginPublishServiceClient Pub orchestratorv1connect.PluginPublishServiceClient
Gallery orchestratorv1connect.PluginGalleryServiceClient
} }
func New(host, token string) *Client { func New(host, token string) *Client {
@ -27,6 +28,7 @@ func New(host, token string) *Client {
c.Scope = orchestratorv1connect.NewPluginScopeServiceClient(httpClient, host, opts...) c.Scope = orchestratorv1connect.NewPluginScopeServiceClient(httpClient, host, opts...)
c.Reg = orchestratorv1connect.NewPluginRegistryServiceClient(httpClient, host, opts...) c.Reg = orchestratorv1connect.NewPluginRegistryServiceClient(httpClient, host, opts...)
c.Pub = orchestratorv1connect.NewPluginPublishServiceClient(httpClient, host, opts...) c.Pub = orchestratorv1connect.NewPluginPublishServiceClient(httpClient, host, opts...)
c.Gallery = orchestratorv1connect.NewPluginGalleryServiceClient(httpClient, host, opts...)
return c return c
} }

View File

@ -49,6 +49,36 @@ service PluginModerationService {
rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse); rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse);
rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse); rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse);
rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse); rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse);
// Star-review moderation (ADR 0021). Unrelated to the submission queue
// above: these act on user reviews (PluginReviewService), not plugins.
rpc ListFlaggedReviews(ListFlaggedReviewsRequest) returns (ListFlaggedReviewsResponse);
rpc SoftDeleteReview(SoftDeleteReviewRequest) returns (SoftDeleteReviewResponse);
rpc SoftDeleteReply(SoftDeleteReplyRequest) returns (SoftDeleteReplyResponse);
}
// PluginGalleryService manages a plugin's screenshot gallery. Reads are
// public; writes require scope (or owning-account) membership, enforced
// in-handler.
service PluginGalleryService {
rpc ListScreenshots(ListScreenshotsRequest) returns (ListScreenshotsResponse);
rpc UploadScreenshot(UploadScreenshotRequest) returns (UploadScreenshotResponse);
rpc DeleteScreenshot(DeleteScreenshotRequest) returns (DeleteScreenshotResponse);
rpc ReorderScreenshots(ReorderScreenshotsRequest) returns (ReorderScreenshotsResponse);
rpc SetFeaturedScreenshot(SetFeaturedScreenshotRequest) returns (SetFeaturedScreenshotResponse);
}
// PluginReviewService is the star-review surface for PUBLIC plugins
// (ADR 0021): verified-install gating, one review per user, author replies,
// user flags. Reads are public; writes require a logged-in orchestrator user.
service PluginReviewService {
rpc ListReviews(ListReviewsRequest) returns (ListReviewsResponse);
rpc GetMyReview(GetMyReviewRequest) returns (GetMyReviewResponse);
rpc UpsertReview(UpsertReviewRequest) returns (UpsertReviewResponse);
rpc DeleteReview(DeleteReviewRequest) returns (DeleteReviewResponse);
rpc ReplyToReview(ReplyToReviewRequest) returns (ReplyToReviewResponse);
rpc DeleteReply(DeleteReplyRequest) returns (DeleteReplyResponse);
rpc FlagReview(FlagReviewRequest) returns (FlagReviewResponse);
} }
// PluginPublishService is called by the ninja CLI to publish a version. // PluginPublishService is called by the ninja CLI to publish a version.
@ -117,10 +147,32 @@ message Plugin {
string latest_version = 13; string latest_version = 13;
repeated string tags = 14; repeated string tags = 14;
// preview_image_url is the PUBLIC, unsigned, long-lived image URL for this // preview_image_url is the PUBLIC, unsigned, long-lived image URL for this
// plugin's latest non-yanked version preview (a PNG screenshot), usable // plugin's card image, usable directly as an <img src>: the gallery's
// directly as an <img src>. Empty when no preview has been published yet // Featured screenshot when one exists, else the latest non-yanked version's
// the wizard card then degrades to display_name + tags (spec §5.2). // preview.png. Empty when neither exists the wizard card then degrades to
// display_name + tags (spec §5.2).
string preview_image_url = 15; string preview_image_url = 15;
// rating_avg/rating_count are the denormalised star-review aggregate over
// non-deleted reviews (0/0 when unreviewed).
float rating_avg = 16;
int32 rating_count = 17;
// install_count is the number of distinct CMS instances currently recorded
// as installers of this plugin (registry_install_events grain).
int32 install_count = 18;
}
// PluginScreenshot is one image in a plugin's managed gallery. The gallery is
// per-plugin (not per-version); images arrive either from a published
// artifact's screenshots/ dir or from author uploads, and the author orders
// them and marks one Featured (the card image).
message PluginScreenshot {
string id = 1;
// url is the public unsigned image URL (only public plugins serve gallery
// images in v1).
string url = 2;
bool featured = 3;
int32 position = 4;
string content_type = 5;
} }
message Category { message Category {
@ -199,6 +251,8 @@ message GetPluginResponse {
Plugin plugin = 1; Plugin plugin = 1;
repeated Version versions = 2; repeated Version versions = 2;
map<string,string> channels = 3; map<string,string> channels = 3;
// screenshots is the plugin's gallery in display order (Featured included).
repeated PluginScreenshot screenshots = 4;
} }
message ListPluginsRequest { message ListPluginsRequest {
@ -323,8 +377,10 @@ message ResolveInstallResponse {
// --- Instance-authenticated request shapes --- // --- Instance-authenticated request shapes ---
// The instance proves its identity with the per-account X-CMS-Secret header; // The instance proves its identity with the per-account X-CMS-Secret header;
// instance_id selects the instance whose owning account scopes the call. No // instance_id selects the instance whose owning account scopes the call. No
// active_account_id / user JWT is needed. These cover only private plugins // active_account_id / user JWT is needed. Listing covers only private plugins
// public plugins use the anonymous ListPlugins / ResolveInstall. // (public plugins use the anonymous ListPlugins); resolve accepts BOTH the
// instance identity is what lets the registry record the install event, which
// backs public install counts and verified-install review gating (ADR 0021).
message ListPrivatePluginsForInstanceRequest { message ListPrivatePluginsForInstanceRequest {
string instance_id = 1; string instance_id = 1;
@ -334,6 +390,10 @@ message ResolveInstallForInstanceRequest {
string instance_id = 1; string instance_id = 1;
string plugin_name = 2; string plugin_name = 2;
string version_or_channel = 3; string version_or_channel = 3;
// scope_slug selects the plugin's scope. Empty or "private" resolves the
// calling account's private plugin (the original behavior); any other value
// resolves a PUBLIC plugin in that scope (ADR 0021).
string scope_slug = 4;
} }
// --- Review / moderation --- // --- Review / moderation ---
@ -385,6 +445,180 @@ message RequestChangesRequest {
} }
message RequestChangesResponse {} message RequestChangesResponse {}
// --- Gallery service ---
message ListScreenshotsRequest {
string scope_slug = 1;
string plugin_name = 2;
}
message ListScreenshotsResponse {
repeated PluginScreenshot screenshots = 1;
}
message UploadScreenshotRequest {
string plugin_id = 1;
// image is the raw file bytes ( 8 MiB; png/jpg/jpeg/webp).
bytes image = 2;
string filename = 3;
string content_type = 4;
}
message UploadScreenshotResponse {
PluginScreenshot screenshot = 1;
}
message DeleteScreenshotRequest {
string plugin_id = 1;
string screenshot_id = 2;
}
message DeleteScreenshotResponse {}
message ReorderScreenshotsRequest {
string plugin_id = 1;
// screenshot_ids_in_order must name every current gallery image exactly once.
repeated string screenshot_ids_in_order = 2;
}
message ReorderScreenshotsResponse {
repeated PluginScreenshot screenshots = 1;
}
message SetFeaturedScreenshotRequest {
string plugin_id = 1;
string screenshot_id = 2;
}
message SetFeaturedScreenshotResponse {}
// --- Star reviews (ADR 0021) ---
message PluginReview {
string id = 1;
string plugin_id = 2;
string user_id = 3;
string author_display_name = 4;
int32 rating = 5; // 1..5
string title = 6;
string body = 7;
// version_at_review is the plugin version the reviewer's account had
// installed when the review was written (best effort; may be empty).
string version_at_review = 8;
bool edited = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
PluginReviewReply reply = 12; // unset when no (non-deleted) reply exists
// deleted/deleted_reason are populated only in moderation views; public
// listings exclude soft-deleted reviews entirely.
bool deleted = 13;
string deleted_reason = 14;
}
message PluginReviewReply {
string id = 1;
string author_display_name = 2;
string body = 3;
bool edited = 4;
google.protobuf.Timestamp created_at = 5;
}
message ReviewAggregate {
float rating_avg = 1;
int32 rating_count = 2;
// count1..count5 are the per-star histogram buckets.
int32 count1 = 3;
int32 count2 = 4;
int32 count3 = 5;
int32 count4 = 6;
int32 count5 = 7;
}
message ListReviewsRequest {
string scope_slug = 1;
string plugin_name = 2;
int32 limit = 3; // 0 server default
int32 offset = 4;
}
message ListReviewsResponse {
repeated PluginReview reviews = 1;
ReviewAggregate aggregate = 2;
}
message GetMyReviewRequest {
string plugin_id = 1;
}
message GetMyReviewResponse {
PluginReview review = 1; // unset when the caller has no review
// eligible reports whether the caller may write a review (verified install
// on their account, not a scope member). The form UI keys off this.
bool eligible = 2;
// ineligible_reason is a human-readable explanation when eligible = false
// ("no install on your account", "authors cannot review their own plugin").
string ineligible_reason = 3;
}
message UpsertReviewRequest {
string plugin_id = 1;
int32 rating = 2; // 1..5, required
string title = 3;
string body = 4;
}
message UpsertReviewResponse {
PluginReview review = 1;
}
message DeleteReviewRequest {
string plugin_id = 1;
}
message DeleteReviewResponse {}
message ReplyToReviewRequest {
string review_id = 1;
string body = 2;
}
message ReplyToReviewResponse {
PluginReviewReply reply = 1;
}
message DeleteReplyRequest {
string review_id = 1;
}
message DeleteReplyResponse {}
message FlagReviewRequest {
string review_id = 1;
string reason = 2; // required
}
message FlagReviewResponse {}
// --- Review moderation (superadmin) ---
message FlaggedReview {
PluginReview review = 1;
string scope_slug = 2;
string plugin_name = 3;
int32 flag_count = 4;
// flag_reasons carries the open flags' reasons, newest first.
repeated string flag_reasons = 5;
google.protobuf.Timestamp first_flagged_at = 6;
}
message ListFlaggedReviewsRequest {
int32 limit = 1;
int32 offset = 2;
}
message ListFlaggedReviewsResponse {
repeated FlaggedReview reviews = 1;
}
message SoftDeleteReviewRequest {
string review_id = 1;
string reason = 2; // required audit trail
}
message SoftDeleteReviewResponse {}
message SoftDeleteReplyRequest {
string review_id = 1;
string reason = 2; // required audit trail
}
message SoftDeleteReplyResponse {}
// --- Publish service --- // --- Publish service ---
message PublishVersionRequest { message PublishVersionRequest {