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).
This commit is contained in:
@@ -9,10 +9,12 @@ land). AGPLv3.
|
|||||||
Status: 2026-08-29 — v0 complete and green: categories (list/create),
|
Status: 2026-08-29 — v0 complete and green: categories (list/create),
|
||||||
topics (create/list/latest/get), posts (create/update/get), current-user
|
topics (create/list/latest/get), posts (create/update/get), current-user
|
||||||
identity probe, raw JSON passthrough for everything else, typed error
|
identity probe, raw JSON passthrough for everything else, typed error
|
||||||
classes, thin CLI. Built and tested entirely against a fake Discourse
|
classes, thin CLI. Same-day additions (Redmine 507): full-text search,
|
||||||
(no live writes). Live reads verified against community.turnsys.com;
|
private-message send, and the webhook + SSO (Discourse Connect) secret
|
||||||
category creation 403s with the current key (needs the admin-scoped key)
|
helpers. Built and tested entirely against a fake Discourse (no live
|
||||||
— see "Live verification" below.
|
writes). Live reads verified against community.turnsys.com; category
|
||||||
|
creation 403s with the current key (needs the admin-scoped key) — see
|
||||||
|
"Live verification" below.
|
||||||
|
|
||||||
## What it implements
|
## What it implements
|
||||||
|
|
||||||
@@ -30,6 +32,10 @@ The wire protocol, in plain REST with stdlib:
|
|||||||
| posts create | `POST /posts.json` — `topic_id`+`raw` |
|
| posts create | `POST /posts.json` — `topic_id`+`raw` |
|
||||||
| posts update | `PUT /posts/<id>.json` — `{post: {raw, edit_reason}}` |
|
| posts update | `PUT /posts/<id>.json` — `{post: {raw, edit_reason}}` |
|
||||||
| posts get | `GET /posts/<id>.json` |
|
| posts get | `GET /posts/<id>.json` |
|
||||||
|
| search | `GET /search.json?term=...&page=N` — grouped posts/topics/users/categories hits; term operators (`@user`, `#category`, `order:latest`) pass through |
|
||||||
|
| message send | `POST /posts.json` — `title`+`raw`+`target_usernames`/`target_group_names`/`target_emails` (comma-joined) instead of a category; creates a private-message topic |
|
||||||
|
| webhook verify | `X-Discourse-Event-Signature` — sha256-HMAC hex (with `sha256=` prefix) over the raw body, constant-time compare |
|
||||||
|
| SSO verify/response | Discourse Connect — sha256-HMAC hex over the base64 `sso` string; payload = urlencoded fields (nonce, return_sso_url / email, external_id, ...) |
|
||||||
| raw anything | `Do(ctx, METHOD, path, body, out)` — JSON in, JSON out; unmodeled endpoints never block on a client release |
|
| raw anything | `Do(ctx, METHOD, path, body, out)` — JSON in, JSON out; unmodeled endpoints never block on a client release |
|
||||||
|
|
||||||
Auth is the header pair `Api-Key` + `Api-Username` on every request.
|
Auth is the header pair `Api-Key` + `Api-Username` on every request.
|
||||||
@@ -55,8 +61,37 @@ res.URL(baseURL) // https://forum/t/<slug>/<topic_id>
|
|||||||
|
|
||||||
p, _ := c.CreatePost(ctx, discourse.CreatePostRequest{TopicID: res.TopicID, Raw: "reply"})
|
p, _ := c.CreatePost(ctx, discourse.CreatePostRequest{TopicID: res.TopicID, Raw: "reply"})
|
||||||
_, _ = c.UpdatePost(ctx, p.PostID, "edited", "typo")
|
_, _ = c.UpdatePost(ctx, p.PostID, "edited", "typo")
|
||||||
|
|
||||||
|
hits, _ := c.Search(ctx, discourse.SearchRequest{Term: "fleet briefing", Page: 1})
|
||||||
|
// hits.Posts[0].TopicID / .Blurb; also .Topics, .Users, .Categories
|
||||||
|
|
||||||
|
pm, _ := c.SendMessage(ctx, discourse.SendMessageRequest{
|
||||||
|
Title: "Briefing landed", Raw: markdown,
|
||||||
|
TargetUsernames: []string{"ops", "reachableceo"},
|
||||||
|
})
|
||||||
|
pm.URL(c.BaseURL()) // private-message topic link
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Webhook + SSO helpers (server side)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// webhook receiver: verify before trusting anything
|
||||||
|
http.Handle("/hooks/discourse", discourse.WebhookHandler(secret, func(evt *discourse.WebhookEvent) {
|
||||||
|
// evt.Event == "post_created" etc.; evt.Body is the raw JSON
|
||||||
|
}))
|
||||||
|
err := discourse.VerifyWebhook(secret, body, sigHeader) // standalone
|
||||||
|
|
||||||
|
// Discourse Connect (SSO) login endpoint
|
||||||
|
redirect, err := discourse.SSOLogin(ctx, secret, r.FormValue("sso"), r.FormValue("sig"),
|
||||||
|
map[string]string{"email": u.Email, "external_id": u.ID, "username": u.Handle})
|
||||||
|
http.Redirect(w, r, redirect, http.StatusFound)
|
||||||
|
|
||||||
|
// secrets: env-only, trimmed, never logged
|
||||||
|
secret, _ := discourse.WebhookSecretFromEnv() // DISCOURSE_WEBHOOK_SECRET
|
||||||
|
secret, _ := discourse.SSOSecretFromEnv() // DISCOURSE_SSO_SECRET
|
||||||
|
```
|
||||||
|
Bad signatures are the typed `ErrBadSignature` — drop the delivery.
|
||||||
|
|
||||||
Errors classify via `errors.Is`: `ErrForbidden` (403 — key lacks scope),
|
Errors classify via `errors.Is`: `ErrForbidden` (403 — key lacks scope),
|
||||||
`ErrUnauthorized` (401), `ErrNotFound`, `ErrRateLimited` (429, with
|
`ErrUnauthorized` (401), `ErrNotFound`, `ErrRateLimited` (429, with
|
||||||
`APIError.RetryAfter`), `ErrServer`, `ErrUnreachable`,
|
`APIError.RetryAfter`), `ErrServer`, `ErrUnreachable`,
|
||||||
@@ -84,7 +119,8 @@ End-to-end smoke — builds the CLI, boots the fake Discourse in a
|
|||||||
container on `127.0.0.1:8610`, drives the real binary from the host
|
container on `127.0.0.1:8610`, drives the real binary from the host
|
||||||
through a 0600 env file (whoami / category list+create / topic create /
|
through a 0600 env file (whoami / category list+create / topic create /
|
||||||
topic list by id and slug / post reply / post update / raw passthrough /
|
topic list by id and slug / post reply / post update / raw passthrough /
|
||||||
typed 404 / redaction sweep / bad-key exit code):
|
search / private-message send / webhook verify good+bad sig / sso
|
||||||
|
verify good+bad sig / typed 404 / redaction sweep / bad-key exit code):
|
||||||
```sh
|
```sh
|
||||||
./dev.sh smoke
|
./dev.sh smoke
|
||||||
```
|
```
|
||||||
@@ -99,7 +135,8 @@ Credentials NEVER arrive via flags or arguments:
|
|||||||
```sh
|
```sh
|
||||||
mkdir -p ~/.config/discourse-go && umask 077
|
mkdir -p ~/.config/discourse-go && umask 077
|
||||||
cp env.example ~/.config/discourse-go/env
|
cp env.example ~/.config/discourse-go/env
|
||||||
# fill in DISCOURSE_URL / DISCOURSE_API_KEY / DISCOURSE_API_USERNAME
|
# fill in DISCOURSE_URL / DISCOURSE_API_KEY (or DISCOURSE_KEY) /
|
||||||
|
# DISCOURSE_API_USERNAME, and the webhook/SSO secrets if used
|
||||||
```
|
```
|
||||||
Then `set -a; . ~/.config/discourse-go/env; set +a` before the CLI (or
|
Then `set -a; . ~/.config/discourse-go/env; set +a` before the CLI (or
|
||||||
source it in the service env). For the harness, the key resolves through
|
source it in the service env). For the harness, the key resolves through
|
||||||
@@ -116,11 +153,16 @@ discourse-go topics show ID
|
|||||||
discourse-go topics create -title TITLE -category ID [-file PATH | -raw TEXT]
|
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 create -topic ID [-file PATH | -raw TEXT]
|
||||||
discourse-go posts update ID [-file PATH | -raw TEXT] [-reason TEXT]
|
discourse-go posts update ID [-file PATH | -raw TEXT] [-reason TEXT]
|
||||||
|
discourse-go search TERM [-page N]
|
||||||
|
discourse-go messages send -title TITLE -to USER[,USER...] [-group NAMES] [-email ADDRS] [-file PATH | -raw TEXT]
|
||||||
|
discourse-go webhook verify SIG # delivery body on stdin, secret from DISCOURSE_WEBHOOK_SECRET
|
||||||
|
discourse-go sso verify SSO SIG # secret from DISCOURSE_SSO_SECRET; prints the payload as JSON
|
||||||
discourse-go raw METHOD PATH [-data JSON|@file]
|
discourse-go raw METHOD PATH [-data JSON|@file]
|
||||||
```
|
```
|
||||||
Perm levels: 1 = reply/see, 2 = create posts, 3 = full. Output is JSON.
|
Perm levels: 1 = reply/see, 2 = create posts, 3 = full. Output is JSON.
|
||||||
Exit codes: 0 ok, 1 usage/config, 2 API/transport error (the message
|
Exit codes: 0 ok, 1 usage/config, 2 API/transport error (the message
|
||||||
names the class).
|
names the class). `webhook verify` / `sso verify` need no instance
|
||||||
|
credentials — only their secret.
|
||||||
|
|
||||||
## Live verification (community.turnsys.com, 2026-08-29)
|
## Live verification (community.turnsys.com, 2026-08-29)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,17 @@ DISCOURSE_URL=https://community.turnsys.com
|
|||||||
|
|
||||||
# Discourse API key (Admin API > Keys, or a user API key). Creating
|
# Discourse API key (Admin API > Keys, or a user API key). Creating
|
||||||
# categories requires an admin-scoped key; a standard key 403s there.
|
# categories requires an admin-scoped key; a standard key 403s there.
|
||||||
|
# DISCOURSE_KEY is accepted as an alias when DISCOURSE_API_KEY is unset.
|
||||||
DISCOURSE_API_KEY=replace-me
|
DISCOURSE_API_KEY=replace-me
|
||||||
|
|
||||||
# The user the key acts as (must match the key's allowed username, or be
|
# The user the key acts as (must match the key's allowed username, or be
|
||||||
# "system" for global keys).
|
# "system" for global keys).
|
||||||
DISCOURSE_API_USERNAME=reachableceo
|
DISCOURSE_API_USERNAME=reachableceo
|
||||||
|
|
||||||
|
# Webhook payload secret (Admin > Settings > API > web hook). Only the
|
||||||
|
# `webhook verify` command and the server-side helpers need it.
|
||||||
|
#DISCOURSE_WEBHOOK_SECRET=replace-me
|
||||||
|
|
||||||
|
# Discourse Connect (SSO) shared secret (Admin > Settings > Login >
|
||||||
|
# discourse connect secret). Only the SSO helpers need it.
|
||||||
|
#DISCOURSE_SSO_SECRET=replace-me
|
||||||
|
|||||||
Reference in New Issue
Block a user