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