v0: stdlib-only Discourse client — categories/topics/posts + raw passthrough
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
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user