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:
+77
@@ -0,0 +1,77 @@
|
||||
// Private messages: sent through the same POST /posts.json endpoint as
|
||||
// topics, but with target_usernames / target_group_names / target_emails
|
||||
// instead of a category — Discourse then creates a private-message topic
|
||||
// (archetype private_message) visible only to sender and recipients.
|
||||
package discourse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SendMessageRequest sends a private message. Exactly one of the three
|
||||
// target lists is the common case; any non-empty one works. At least one
|
||||
// target overall is required — Discourse 422s without one.
|
||||
type SendMessageRequest struct {
|
||||
Title string
|
||||
Raw string
|
||||
TargetUsernames []string
|
||||
TargetGroupNames []string
|
||||
TargetEmails []string
|
||||
}
|
||||
|
||||
// sendMessageWire is the POST /posts.json body: Discourse expects the
|
||||
// target lists comma-joined strings, not JSON arrays.
|
||||
type sendMessageWire struct {
|
||||
Title string `json:"title"`
|
||||
Raw string `json:"raw"`
|
||||
TargetUsernames string `json:"target_usernames,omitempty"`
|
||||
TargetGroupNames string `json:"target_group_names,omitempty"`
|
||||
TargetEmails string `json:"target_emails,omitempty"`
|
||||
}
|
||||
|
||||
// SendMessageResult is the response: a fresh private-message topic,
|
||||
// same shape as a topic creation (URL() renders its link).
|
||||
type SendMessageResult = CreateTopicResult
|
||||
|
||||
// SendMessage posts a private message and returns the new PM topic's
|
||||
// ids. Unknown recipients fail server-side as 403 (ErrForbidden) —
|
||||
// that is the "not permitted to view the requested resource" case.
|
||||
func (c *Client) SendMessage(ctx context.Context, req SendMessageRequest) (*SendMessageResult, error) {
|
||||
if strings.TrimSpace(req.Title) == "" {
|
||||
return nil, fmt.Errorf("%w: message title is required", ErrInvalidRequest)
|
||||
}
|
||||
if strings.TrimSpace(req.Raw) == "" {
|
||||
return nil, fmt.Errorf("%w: message raw body is required", ErrInvalidRequest)
|
||||
}
|
||||
if len(req.TargetUsernames) == 0 && len(req.TargetGroupNames) == 0 && len(req.TargetEmails) == 0 {
|
||||
return nil, fmt.Errorf("%w: at least one target (username, group or email) is required", ErrInvalidRequest)
|
||||
}
|
||||
wire := sendMessageWire{
|
||||
Title: req.Title,
|
||||
Raw: req.Raw,
|
||||
TargetUsernames: strings.Join(cleanList(req.TargetUsernames), ","),
|
||||
TargetGroupNames: strings.Join(cleanList(req.TargetGroupNames), ","),
|
||||
TargetEmails: strings.Join(cleanList(req.TargetEmails), ","),
|
||||
}
|
||||
var out SendMessageResult
|
||||
if err := c.Post(ctx, "/posts.json", wire, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.TopicID == 0 {
|
||||
return nil, fmt.Errorf("%w: message sent but response carries no topic_id", ErrMalformedResponse)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// cleanList trims whitespace and drops empties from a target list.
|
||||
func cleanList(in []string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, v := range in {
|
||||
if v = strings.TrimSpace(v); v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user