// Command compose is the temporary quoted-remembrance draft tool. // // It writes a review queue the caller names and, once a person has approved // a draft with an explicit consent basis, POSTs that draft to an adapter's // /v1/channel-posts. It has no path into the adapter package or Telegram. package main import ( "context" "encoding/json" "errors" "flag" "fmt" "os" "path/filepath" "github.com/tegwick/fluid-telegram/internal/compose" ) const usage = `compose — temporary quoted-remembrance drafts compose draft --entry --queue (--source-url | --base-url ) compose approve --queue --post --reviewer --consent explicit|standing|not-required compose publish --queue --post --adapter compose visual [--entry ] --hall-dir --hall-raw-base --fallback visual prints the image a post should carry as JSON: the entry's rendered portrait, else "needs-render" with its prompt, else (no prompt) the fallback. Consent is never defaulted. A draft is not published until approve has recorded a reviewer and a consent basis the caller supplied. ` func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "compose:", err) os.Exit(1) } } func run(args []string) error { if len(args) < 1 { return errors.New(usage) } switch args[0] { case "draft": return cmdDraft(args[1:]) case "approve": return cmdApprove(args[1:]) case "publish": return cmdPublish(args[1:]) case "visual": return cmdVisual(args[1:]) case "-h", "--help", "help": fmt.Fprint(os.Stderr, usage) return nil default: return fmt.Errorf("unknown command %q\n%s", args[0], usage) } } func cmdDraft(args []string) error { fs := flag.NewFlagSet("draft", flag.ContinueOnError) fs.SetOutput(os.Stderr) entry := fs.String("entry", "", "hall entry markdown") queue := fs.String("queue", "", "review queue directory") sourceURL := fs.String("source-url", "", "explicit source entry URL") baseURL := fs.String("base-url", "", "base URL; /hall-of-helix/ is appended") if err := fs.Parse(args); err != nil { return err } if *entry == "" || *queue == "" { return errors.New("draft requires --entry and --queue") } raw, err := os.ReadFile(*entry) if err != nil { return err } d, err := compose.Compose(string(raw), compose.Source{URL: *sourceURL, Base: *baseURL}) if err != nil { return err } q, err := compose.Open(*queue) if err != nil { return err } if err := q.Put(d); err != nil { return err } fmt.Println(d.PostID) return nil } func cmdApprove(args []string) error { fs := flag.NewFlagSet("approve", flag.ContinueOnError) fs.SetOutput(os.Stderr) queue := fs.String("queue", "", "review queue directory") post := fs.String("post", "", "draft post_id") reviewer := fs.String("reviewer", "", "person approving the draft") consent := fs.String("consent", "", "explicit, standing, or not-required") if err := fs.Parse(args); err != nil { return err } if *queue == "" || *post == "" || *reviewer == "" { return errors.New("approve requires --queue, --post, and --reviewer") } if *consent == "" { return errors.New("--consent is required (explicit, standing, or not-required); it is not defaulted") } q, err := compose.Open(*queue) if err != nil { return err } if _, err := q.Approve(*post, *reviewer, *consent); err != nil { return err } fmt.Println(*post) return nil } func cmdPublish(args []string) error { fs := flag.NewFlagSet("publish", flag.ContinueOnError) fs.SetOutput(os.Stderr) queue := fs.String("queue", "", "review queue directory") post := fs.String("post", "", "draft post_id") adapter := fs.String("adapter", "", "adapter base URL") if err := fs.Parse(args); err != nil { return err } if *queue == "" || *post == "" || *adapter == "" { return errors.New("publish requires --queue, --post, and --adapter") } q, err := compose.Open(*queue) if err != nil { return err } d, err := q.Publish(context.Background(), *adapter, *post) if err != nil { return err } fmt.Printf("published %s %d\n", d.PostID, d.Publication.StatusCode) return nil } func cmdVisual(args []string) error { fs := flag.NewFlagSet("visual", flag.ContinueOnError) fs.SetOutput(os.Stderr) entry := fs.String("entry", "", "hall entry markdown; omit for a post with no source entry") hallDir := fs.String("hall-dir", "", "hall-of-helix checkout, to confirm a portrait file exists") rawBase := fs.String("hall-raw-base", "", "public raw URL of the hall repository root") fallback := fs.String("fallback", "", "public URL of the campaign's main image") if err := fs.Parse(args); err != nil { return err } var markdown string if *entry != "" { raw, err := os.ReadFile(*entry) if err != nil { return err } markdown = string(raw) } v, err := compose.ResolveVisual(markdown, *rawBase, *fallback) if err != nil { return err } if v.Kind == compose.VisualPortrait { if *hallDir == "" { return errors.New("visual: --hall-dir is required to confirm the portrait exists") } if _, err := os.Stat(filepath.Join(*hallDir, v.Path)); err != nil { return fmt.Errorf("visual: entry links %s, which is not in the hall checkout", v.Path) } } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(v) }