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.
This commit is contained in:
+61
-2
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -28,6 +29,8 @@ type fakeDiscourse struct {
|
||||
topics map[int]topicRec
|
||||
nextT int
|
||||
searchFail int
|
||||
pms []pmRec // private messages, in creation order
|
||||
lastBody string // raw JSON body of the last POST /posts.json
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
@@ -43,6 +46,13 @@ type topicRec struct {
|
||||
Cat int
|
||||
}
|
||||
|
||||
type pmRec struct {
|
||||
TopicID int
|
||||
Title string
|
||||
Targets []string
|
||||
Raw string
|
||||
}
|
||||
|
||||
func newFake(t *testing.T) *fakeDiscourse {
|
||||
t.Helper()
|
||||
f := &fakeDiscourse{
|
||||
@@ -111,13 +121,47 @@ func newFake(t *testing.T) *fakeDiscourse {
|
||||
writeJSON(w, 405, nil)
|
||||
return
|
||||
}
|
||||
var req CreatePostRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeJSON(w, 400, map[string]any{"errors": []string{"bad body"}})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
CreatePostRequest
|
||||
TargetUsernames string `json:"target_usernames"`
|
||||
TargetGroupNames string `json:"target_group_names"`
|
||||
TargetEmails string `json:"target_emails"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
writeJSON(w, 400, map[string]any{"errors": []string{"bad json"}})
|
||||
return
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.lastBody = string(body)
|
||||
if req.TargetUsernames != "" || req.TargetGroupNames != "" || req.TargetEmails != "" {
|
||||
// private-message creation
|
||||
targets := splitCSV(req.TargetUsernames)
|
||||
for _, name := range append(append(targets, splitCSV(req.TargetGroupNames)...), splitCSV(req.TargetEmails)...) {
|
||||
if strings.HasPrefix(name, "denied") {
|
||||
writeJSON(w, 403, map[string]any{"errors": []string{"You are not permitted to view the requested resource."}})
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.Title) == "" {
|
||||
writeJSON(w, 422, map[string]any{"errors": []string{"Title can't be blank"}})
|
||||
return
|
||||
}
|
||||
tid := f.nextT
|
||||
f.nextT++
|
||||
slug := strings.ToLower(strings.ReplaceAll(req.Title, " ", "-"))
|
||||
f.pms = append(f.pms, pmRec{TopicID: tid, Title: req.Title, Targets: targets, Raw: req.Raw})
|
||||
pid := f.nextPost
|
||||
f.nextPost++
|
||||
f.posts[pid] = postRec{Post: Post{ID: pid, TopicID: tid, PostNumber: 1, Raw: req.Raw}}
|
||||
writeJSON(w, 200, map[string]any{"id": pid, "topic_id": tid, "topic_slug": slug})
|
||||
return
|
||||
}
|
||||
if req.TopicID == 0 { // topic creation
|
||||
if strings.TrimSpace(req.Title) == "" || req.Category == 0 {
|
||||
writeJSON(w, 422, map[string]any{"errors": []string{"Title can't be blank"}})
|
||||
@@ -264,6 +308,21 @@ func writeJSON(w http.ResponseWriter, code int, body any) {
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
// splitCSV splits a comma-joined target list ("ops,reachableceo").
|
||||
func splitCSV(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
out := parts[:0]
|
||||
for _, p := range parts {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func testClient(t *testing.T, f *fakeDiscourse) *Client {
|
||||
t.Helper()
|
||||
c, err := New(f.srv.URL, testKey, "")
|
||||
|
||||
Reference in New Issue
Block a user