v0 landed with a few misaligned struct tags and fields; gofmt -l flagged categories.go, topics.go, discourse_test.go and the smoke fake. Alignment only, no behavior change, so the tree passes the gofmt gate cleanly.
103 lines
3.4 KiB
Go
103 lines
3.4 KiB
Go
// Categories: list (GET /categories.json) and create (POST /categories.json).
|
|
// Creation needs an admin-scoped key — the current MOPAC key gets 403
|
|
// there (ErrForbidden); listing works with a standard key.
|
|
package discourse
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// Category is the subset of the Discourse category payload callers use.
|
|
type Category struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
TextColor string `json:"text_color"`
|
|
Position int `json:"position"`
|
|
TopicCount int `json:"topic_count"`
|
|
}
|
|
|
|
// CreateCategoryRequest is the POST /categories.json body. Permissions
|
|
// maps a GROUP NAME to an access level: 1 = reply/see, 2 = create posts,
|
|
// 3 = full (the security group numbers Discourse's admin UI uses). Empty
|
|
// means "default: everyone full" — for restricted categories set it
|
|
// explicitly, e.g. {"staff": 3, "trust_level_0": 1}.
|
|
type CreateCategoryRequest struct {
|
|
Name string `json:"name"`
|
|
Color string `json:"color"` // 6 hex digits, no '#'
|
|
TextColor string `json:"text_color"` // 6 hex digits, no '#'
|
|
Permissions map[string]int `json:"permissions,omitempty"`
|
|
Position int `json:"position,omitempty"`
|
|
ParentCategoryID int `json:"parent_category_id,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// ListCategories returns the instance's categories (one request; the
|
|
// endpoint returns the full list).
|
|
func (c *Client) ListCategories(ctx context.Context) ([]Category, error) {
|
|
var payload struct {
|
|
CategoryList struct {
|
|
Categories []Category `json:"categories"`
|
|
} `json:"category_list"`
|
|
}
|
|
if err := c.Get(ctx, "/categories.json", &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload.CategoryList.Categories, nil
|
|
}
|
|
|
|
// FindCategory resolves a category by id or by slug (case-insensitive).
|
|
// One of the two must be non-zero/non-empty.
|
|
func (c *Client) FindCategory(ctx context.Context, id int, slug string) (*Category, error) {
|
|
cats, err := c.ListCategories(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range cats {
|
|
if (id != 0 && cats[i].ID == id) ||
|
|
(slug != "" && strings.EqualFold(cats[i].Slug, slug)) {
|
|
return &cats[i], nil
|
|
}
|
|
}
|
|
return nil, &APIError{
|
|
Method: "GET",
|
|
Path: "/categories.json",
|
|
Status: 404,
|
|
Errors: []string{fmt.Sprintf("category id=%d slug=%q not found", id, slug)},
|
|
}
|
|
}
|
|
|
|
// CreateCategory creates a category and returns the created record.
|
|
// Colors are normalized (uppercased, '#' stripped); Discourse requires
|
|
// valid hex or it 422s.
|
|
func (c *Client) CreateCategory(ctx context.Context, req CreateCategoryRequest) (*Category, error) {
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
return nil, fmt.Errorf("%w: category name is required", ErrInvalidRequest)
|
|
}
|
|
req.Color = normHex(req.Color, "3AB54A")
|
|
req.TextColor = normHex(req.TextColor, "FFFFFF")
|
|
var created Category
|
|
if err := c.Post(ctx, "/categories.json", req, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
return &created, nil
|
|
}
|
|
|
|
func normHex(v, fallback string) string {
|
|
v = strings.TrimPrefix(strings.TrimSpace(strings.ToUpper(v)), "#")
|
|
if v == "" {
|
|
return fallback
|
|
}
|
|
return v
|
|
}
|
|
|
|
// quotePathSegment escapes a path segment (slugs in /c/<slug>/<id>.json).
|
|
func quotePathSegment(s string) string {
|
|
return url.PathEscape(s)
|
|
}
|