package main import ( "context" "encoding/json" "flag" "fmt" "os" "strconv" "strings" "git.knownelement.com/ukrrs/mopac-discourse-go" ) func runCategories(ctx context.Context, c *discourse.Client, args []string) (any, error) { if len(args) == 0 { return nil, fmt.Errorf("categories needs a subcommand (list|create)") } switch args[0] { case "list": if len(args) != 1 { return nil, fmt.Errorf("categories list takes no arguments") } return c.ListCategories(ctx) case "create": fs := flag.NewFlagSet("categories create", flag.ContinueOnError) color := fs.String("color", "", "hex color, 6 digits") textColor := fs.String("text-color", "", "hex text color, 6 digits") perms := multiFlag{} fs.Var(&perms, "perm", "GROUP=LEVEL (1 reply/see, 2 create, 3 full); repeatable") if err := fs.Parse(flagsFirst(args[1:])); err != nil { return nil, err } if fs.NArg() != 1 { return nil, fmt.Errorf("categories create needs exactly one NAME") } req := discourse.CreateCategoryRequest{ Name: fs.Arg(0), Color: *color, TextColor: *textColor, } for _, p := range perms { parts := strings.SplitN(p, "=", 2) if len(parts) != 2 { return nil, fmt.Errorf("-perm %q must be GROUP=LEVEL", p) } lvl, err := strconv.Atoi(parts[1]) if err != nil || lvl < 1 || lvl > 3 { return nil, fmt.Errorf("-perm %q level must be 1, 2 or 3", p) } if req.Permissions == nil { req.Permissions = map[string]int{} } req.Permissions[parts[0]] = lvl } return c.CreateCategory(ctx, req) } return nil, fmt.Errorf("unknown categories subcommand %q", args[0]) } func runTopics(ctx context.Context, c *discourse.Client, args []string) (any, error) { if len(args) == 0 { return nil, fmt.Errorf("topics needs a subcommand (list|show|create)") } switch args[0] { case "list": fs := flag.NewFlagSet("topics list", flag.ContinueOnError) catID := fs.Int("category", 0, "category id") slug := fs.String("slug", "", "category slug") latest := fs.Bool("latest", false, "instance-wide latest topics") if err := fs.Parse(args[1:]); err != nil { return nil, err } if *latest { return c.LatestTopics(ctx) } return c.ListTopics(ctx, *catID, *slug) case "show": if len(args) != 2 { return nil, fmt.Errorf("topics show needs exactly one ID") } id, err := strconv.Atoi(args[1]) if err != nil { return nil, fmt.Errorf("topic id %q is not a number", args[1]) } return c.GetTopic(ctx, id) case "create": fs := flag.NewFlagSet("topics create", flag.ContinueOnError) title := fs.String("title", "", "topic title (required)") category := fs.Int("category", 0, "category id (required)") raw := fs.String("raw", "", "markdown body inline") file := fs.String("file", "", "markdown body from file (overrides -raw)") if err := fs.Parse(flagsFirst(args[1:])); err != nil { return nil, err } body, err := bodyFrom(*file, *raw) if err != nil { return nil, err } return c.CreateTopic(ctx, discourse.CreateTopicRequest{Title: *title, Raw: body, Category: *category}) } return nil, fmt.Errorf("unknown topics subcommand %q", args[0]) } func runPosts(ctx context.Context, c *discourse.Client, args []string) (any, error) { if len(args) == 0 { return nil, fmt.Errorf("posts needs a subcommand (create|update)") } switch args[0] { case "create": fs := flag.NewFlagSet("posts create", flag.ContinueOnError) topic := fs.Int("topic", 0, "topic id (required)") raw := fs.String("raw", "", "markdown body inline") file := fs.String("file", "", "markdown body from file (overrides -raw)") if err := fs.Parse(flagsFirst(args[1:])); err != nil { return nil, err } body, err := bodyFrom(*file, *raw) if err != nil { return nil, err } return c.CreatePost(ctx, discourse.CreatePostRequest{TopicID: *topic, Raw: body}) case "update": fs := flag.NewFlagSet("posts update", flag.ContinueOnError) raw := fs.String("raw", "", "markdown body inline") file := fs.String("file", "", "markdown body from file (overrides -raw)") reason := fs.String("reason", "", "edit reason") if err := fs.Parse(flagsFirst(args[1:])); err != nil { return nil, err } if fs.NArg() != 1 { return nil, fmt.Errorf("posts update needs exactly one ID") } id, err := strconv.Atoi(fs.Arg(0)) if err != nil { return nil, fmt.Errorf("post id %q is not a number", fs.Arg(0)) } body, err := bodyFrom(*file, *raw) if err != nil { return nil, err } return c.UpdatePost(ctx, id, body, *reason) } return nil, fmt.Errorf("unknown posts subcommand %q", args[0]) } func runRaw(ctx context.Context, c *discourse.Client, args []string) (any, error) { if len(args) < 2 { return nil, fmt.Errorf("raw needs METHOD and PATH") } method := strings.ToUpper(args[0]) path := args[1] fs := flag.NewFlagSet("raw", flag.ContinueOnError) data := fs.String("data", "", "JSON body inline or @file") if err := fs.Parse(args[2:]); err != nil { return nil, err } var body any if *data != "" { raw := []byte(*data) if strings.HasPrefix(*data, "@") { b, err := os.ReadFile(strings.TrimPrefix(*data, "@")) if err != nil { return nil, err } raw = b } if err := json.Unmarshal(raw, &body); err != nil { return nil, fmt.Errorf("-data is not valid JSON: %v", err) } } var out any if err := c.Do(ctx, method, path, body, &out); err != nil { return nil, err } return out, nil } func bodyFrom(file, raw string) (string, error) { if file != "" { b, err := os.ReadFile(file) if err != nil { return "", err } return string(b), nil } if raw == "" { return "", fmt.Errorf("body required: -raw TEXT or -file PATH") } return raw, nil } // flagsFirst lets flags appear after positional args ("create NAME -color // X"): every flag here takes a value, so tokens starting with "-" grab // their neighbor; the rest are positional. Reordered as flags-then- // positionals for flag.Parse. func flagsFirst(args []string) []string { var flags, pos []string for i := 0; i < len(args); i++ { if strings.HasPrefix(args[i], "-") && args[i] != "-" { flags = append(flags, args[i]) if i+1 < len(args) { i++ flags = append(flags, args[i]) } } else { pos = append(pos, args[i]) } } return append(flags, pos...) } // multiFlag collects repeatable string flags (-perm a=1 -perm b=2). type multiFlag []string func (m *multiFlag) String() string { return strings.Join(*m, ",") } func (m *multiFlag) Set(v string) error { *m = append(*m, v) return nil }