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