Files
mopac-discourse-go/categories.go
T
mrcharles f51ca5900c style: gofmt struct-tag alignment across v0 files
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.
2026-08-29 16:20:44 -05:00

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)
}