142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package compose
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// postPayload is the R-1 request body. additionalProperties is false on the
|
|
// schema, so this struct is the whole request: queue fields such as approved
|
|
// and publication are not sent. visual_url and tags are omitted; this
|
|
// strategy does not set them.
|
|
type postPayload struct {
|
|
PostID string `json:"post_id"`
|
|
Body string `json:"body"`
|
|
SourceEntryID string `json:"source_entry_id"`
|
|
SourceEntryURL string `json:"source_entry_url"`
|
|
ReviewedBy string `json:"reviewed_by"`
|
|
SubjectDisplayName string `json:"subject_display_name"`
|
|
SubjectConsent string `json:"subject_consent"`
|
|
Variant string `json:"variant"`
|
|
}
|
|
|
|
// Publish POSTs an approved draft to {adapterBase}/v1/channel-posts.
|
|
// It reads the draft from the queue and refuses when that record is not
|
|
// approved, when reviewed_by is empty, or when subject_consent is not one of
|
|
// the three caller-supplied enum values. HTTP 200 and 201 are recorded on
|
|
// the draft. Redirects to another host are not followed.
|
|
func (q *Queue) Publish(ctx context.Context, adapterBase, postID string) (Draft, error) {
|
|
d, err := q.Get(postID)
|
|
if err != nil {
|
|
return Draft{}, err
|
|
}
|
|
if err := publishable(d); err != nil {
|
|
return Draft{}, fmt.Errorf("publish %s: %w", postID, err)
|
|
}
|
|
endpoint, err := channelPostsURL(adapterBase)
|
|
if err != nil {
|
|
return Draft{}, err
|
|
}
|
|
payload := postPayload{
|
|
PostID: d.PostID,
|
|
Body: d.Body,
|
|
SourceEntryID: d.SourceEntryID,
|
|
SourceEntryURL: d.SourceEntryURL,
|
|
ReviewedBy: d.ReviewedBy,
|
|
SubjectDisplayName: d.SubjectDisplayName,
|
|
SubjectConsent: d.SubjectConsent,
|
|
Variant: d.Variant,
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return Draft{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw))
|
|
if err != nil {
|
|
return Draft{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := publishClient().Do(req)
|
|
if err != nil {
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
return Draft{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if err != nil {
|
|
return Draft{}, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
return Draft{}, fmt.Errorf("publish %s: HTTP %d", postID, resp.StatusCode)
|
|
}
|
|
d.Publication = &Publication{StatusCode: resp.StatusCode, Body: string(respBody)}
|
|
if err := q.save(d); err != nil {
|
|
return Draft{}, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func publishable(d Draft) error {
|
|
if !d.Approved {
|
|
return ErrNotApproved
|
|
}
|
|
if strings.TrimSpace(d.ReviewedBy) == "" {
|
|
return ErrReviewer
|
|
}
|
|
if _, ok := consentBases[d.SubjectConsent]; !ok {
|
|
return fmt.Errorf("%w: got %q", ErrConsent, d.SubjectConsent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func channelPostsURL(base string) (string, error) {
|
|
u, err := url.Parse(base)
|
|
if err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
|
return "", fmt.Errorf("adapter base URL must be absolute http(s)")
|
|
}
|
|
u.Path = strings.TrimRight(u.Path, "/") + "/v1/channel-posts"
|
|
u.RawQuery = ""
|
|
u.Fragment = ""
|
|
return u.String(), nil
|
|
}
|
|
|
|
func publishClient() *http.Client {
|
|
return &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
if len(via) == 0 {
|
|
return nil
|
|
}
|
|
if hostPort(req.URL) != hostPort(via[0].URL) {
|
|
return fmt.Errorf("%w: %s", ErrCrossHost, req.URL.Host)
|
|
}
|
|
if len(via) >= 5 {
|
|
return fmt.Errorf("too many redirects")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func hostPort(u *url.URL) string {
|
|
host := strings.ToLower(u.Hostname())
|
|
port := u.Port()
|
|
if port == "" {
|
|
if strings.EqualFold(u.Scheme, "https") {
|
|
port = "443"
|
|
} else {
|
|
port = "80"
|
|
}
|
|
}
|
|
return host + ":" + port
|
|
}
|