Commit Graph
9 Commits
Author SHA1 Message Date
mrcharles bb96849641 docs: README + env.example for search, PMs, webhook/SSO helpers
Wire table gains the search, private-message, webhook and SSO
rows; the library surface shows Search/SendMessage calls and a
server-side helper section (WebhookHandler, SSOLogin, secret
loaders); the CLI reference lists the four new commands including
their secret env vars. env.example documents the DISCOURSE_KEY
alias and the two commented-out secrets.

Part of Redmine 507 (Discourse Go client).
2026-08-29 16:55:31 -05:00
mrcharles 445df7a836 test: smoke coverage for search, private messages, webhook/SSO
The fake smoke Discourse now serves /search.json (title substring
matches over created topics), accepts private-message creation on
POST /posts.json (targets comma-joined on the wire, deliveries
recorded and exposed via /__pms.json), and the smoke script drives
the real CLI through it: search hits, PM send + recipient
assertion, webhook verify with a python-computed HMAC (good sig
exits 0, bad sig exits 2 with the typed message), and sso verify
decoding a signed Discourse Connect payload (tamper case exits 2).
No live forum is contacted.

Part of Redmine 507 (Discourse Go client).
2026-08-29 16:54:43 -05:00
mrcharles f13302778b feat(cli): search, messages send, webhook verify, sso verify
The CLI now covers the whole Redmine 507 surface: `search TERM
[-page N]` (operators pass through quoted), `messages send -title
-to user1,user2 [-group/-email] [-file|-raw]`, plus the two secret
helpers `webhook verify SIG` (delivery body on stdin, exit 0 only
on a valid HMAC) and `sso verify SSO SIG` (prints the decoded
Discourse Connect payload as JSON). The helpers run before client
construction so they work with only their secret env set, no
instance credentials needed. Usage text lists the new env vars.

Part of Redmine 507 (Discourse Go client).
2026-08-29 16:50:31 -05:00
mrcharles 5b95ac3c7e feat: accept DISCOURSE_KEY as env alias for the API key
NewFromEnv now falls back to DISCOURSE_KEY when DISCOURSE_API_KEY
is unset (the explicit name still wins), matching the shorter
DISCOURSE_URL/DISCOURSE_KEY convention used across the MOPAC fleet
scripts. Behavioral test proves both the alias and the precedence
against the fake server.

Part of Redmine 507 (Discourse Go client).
2026-08-29 16:47:17 -05:00
mrcharles 5bbb307b12 feat: webhook + SSO secret helpers (HMAC verification)
The server side of talking to Discourse, all stdlib crypto:
VerifyWebhook checks X-Discourse-Event-Signature (sha256 HMAC hex,
constant-time, with or without the sha256= prefix) over the raw
body; ParseWebhook/WebhookHandler turn that into a drop-in
http.HandlerFunc that 403s mis-signed deliveries before dispatch.
VerifySSO decodes + verifies a Discourse Connect login redirect
(HMAC over the base64 string), BuildSSOResponse signs the identity
answer (nonce + email/external_id/username/...), and SSOLogin does
the whole dance in one call, returning the redirect URL. Secrets
load from DISCOURSE_WEBHOOK_SECRET / DISCOURSE_SSO_SECRET, are
trimmed, never logged and never echoed in errors.

Part of Redmine 507 (Discourse Go client): the webhook helper is
what the fleet receiver and 495 delivery loop verify pushes with.
2026-08-29 16:43:59 -05:00
mrcharles 6891f3febc feat: private-message send via POST /posts.json targets
Client.SendMessage(title, raw, targets) creates a private-message
topic by posting to /posts.json with target_usernames /
target_group_names / target_emails (comma-joined on the wire, as
Discourse expects, never JSON arrays) instead of a category. The
result reuses the topic ids type, so URL() renders the PM link.
Local validation requires title, body and at least one target;
undeliverable recipients surface as the typed ErrForbidden class.
Fake-server tests assert the wire shape and the recorded recipients.

Part of Redmine 507 (Discourse Go client): PM delivery is how the
495 pipeline pings humans that a briefing has landed.
2026-08-29 16:36:54 -05:00
mrcharles 14cdaa0a2d feat: search — GET /search.json with grouped hits
Client.Search(term, page) url-encodes the term (operators like
@user / #category / order:latest pass through) and decodes the four
hit groups Discourse returns: posts (with blurb and highlighted
topic title), topics, users and categories. Empty term is rejected
locally with ErrInvalidRequest; server failures keep the typed
error classes. Fake-server tests cover encoding, parsing and the
403 path.

Part of Redmine 507 (Discourse Go client): search is how the 495
briefing pipeline finds landing spots and prior threads.
2026-08-29 16:25:29 -05:00
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
mrcharles f9cf30bc72 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
2026-08-29 06:12:22 -05:00