Files
mopac-discourse-go/messages.go
T
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

78 lines
2.8 KiB
Go

// 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
}