Covers the MOPAC briefing/report surface (Redmine 495 Part A): category
list/create (create needs the admin-scoped key; current key 403s, typed
as ErrForbidden), topic create/list/latest/get, post create/update/get,
current-user identity probe, and a Do() JSON passthrough so unmodeled
endpoints need no client release. Key is env/constructor-only, never a
flag, never logged; errors classify via errors.Is. Fake-server unit
tests + containerized end-to-end smoke (redaction sweep included); live
reads verified against community.turnsys.com.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
// Command discourse-go is a thin CLI over the client library: enough to
|
|
// drive and verify a Discourse instance by hand (categories, topics,
|
|
// posts, raw passthrough). Credentials arrive via environment only
|
|
// (DISCOURSE_URL / DISCOURSE_API_KEY / DISCOURSE_API_USERNAME, see
|
|
// env.example) — never as flags, so they never land in shell history or
|
|
// process listings.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"git.knownelement.com/ukrrs/mopac-discourse-go"
|
|
)
|
|
|
|
const usage = `discourse-go — thin Discourse client CLI (config via env)
|
|
|
|
Usage:
|
|
discourse-go whoami
|
|
discourse-go categories list
|
|
discourse-go categories create NAME [-color HEX] [-text-color HEX] [-perm GROUP=LEVEL]...
|
|
discourse-go topics list [-category ID] [-slug SLUG] [-latest]
|
|
discourse-go topics show ID
|
|
discourse-go topics create -title TITLE -category ID [-file PATH | -raw TEXT]
|
|
discourse-go posts create -topic ID [-file PATH | -raw TEXT]
|
|
discourse-go posts update ID [-file PATH | -raw TEXT] [-reason TEXT]
|
|
discourse-go raw METHOD PATH [-data JSON]
|
|
|
|
Environment (0600 env file, sourced before the call):
|
|
DISCOURSE_URL instance root, e.g. https://community.turnsys.com
|
|
DISCOURSE_API_KEY API key (never a flag, never logged)
|
|
DISCOURSE_API_USERNAME the user the key acts as (default: system)
|
|
|
|
Perm levels: 1 = reply/see, 2 = create posts, 3 = full.
|
|
raw PATH is everything after the instance root, e.g. /groups.json.
|
|
Exit codes: 0 ok · 1 usage/config · 2 API/transport error (message says
|
|
which class: forbidden/unauthorized/not-found/rate-limited/server/
|
|
unreachable/malformed).`
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:]))
|
|
}
|
|
|
|
func run(args []string) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprint(os.Stderr, usage)
|
|
return 1
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
client, err := discourse.NewFromEnv()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
var out any
|
|
switch args[0] {
|
|
case "whoami":
|
|
if len(args) != 1 {
|
|
return usageErr()
|
|
}
|
|
out, err = client.CurrentUser(ctx)
|
|
|
|
case "categories":
|
|
out, err = runCategories(ctx, client, args[1:])
|
|
case "topics":
|
|
out, err = runTopics(ctx, client, args[1:])
|
|
case "posts":
|
|
out, err = runPosts(ctx, client, args[1:])
|
|
case "raw":
|
|
out, err = runRaw(ctx, client, args[1:])
|
|
case "help", "-h", "--help":
|
|
fmt.Print(usage)
|
|
return 0
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "discourse-go: unknown command %q\n\n%s", args[0], usage)
|
|
return 1
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
|
|
if errors.Is(err, discourse.ErrInvalidRequest) {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
if out == nil {
|
|
return 0
|
|
}
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(out); err != nil {
|
|
fmt.Fprintf(os.Stderr, "discourse-go: encode output: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func usageErr() int {
|
|
fmt.Fprint(os.Stderr, usage)
|
|
return 1
|
|
}
|