feat: expose Markdown fields in webhook payload (#14931)

Fixes: https://github.com/coder/coder/issues/14930
This commit is contained in:
Marcin Tojek
2024-10-02 15:38:22 +02:00
committed by GitHub
parent 2f043d7ab9
commit 0aa84b18a1
5 changed files with 41 additions and 32 deletions

View File

@ -28,43 +28,47 @@ type WebhookHandler struct {
// WebhookPayload describes the JSON payload to be delivered to the configured webhook endpoint. // WebhookPayload describes the JSON payload to be delivered to the configured webhook endpoint.
type WebhookPayload struct { type WebhookPayload struct {
Version string `json:"_version"` Version string `json:"_version"`
MsgID uuid.UUID `json:"msg_id"` MsgID uuid.UUID `json:"msg_id"`
Payload types.MessagePayload `json:"payload"` Payload types.MessagePayload `json:"payload"`
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` TitleMarkdown string `json:"title_markdown"`
Body string `json:"body"`
BodyMarkdown string `json:"body_markdown"`
} }
func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger) *WebhookHandler { func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger) *WebhookHandler {
return &WebhookHandler{cfg: cfg, log: log, cl: &http.Client{}} return &WebhookHandler{cfg: cfg, log: log, cl: &http.Client{}}
} }
func (w *WebhookHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string) (DeliveryFunc, error) { func (w *WebhookHandler) Dispatcher(payload types.MessagePayload, titleMarkdown, bodyMarkdown string) (DeliveryFunc, error) {
if w.cfg.Endpoint.String() == "" { if w.cfg.Endpoint.String() == "" {
return nil, xerrors.New("webhook endpoint not defined") return nil, xerrors.New("webhook endpoint not defined")
} }
title, err := markdown.PlaintextFromMarkdown(titleTmpl) titlePlaintext, err := markdown.PlaintextFromMarkdown(titleMarkdown)
if err != nil { if err != nil {
return nil, xerrors.Errorf("render title: %w", err) return nil, xerrors.Errorf("render title: %w", err)
} }
body, err := markdown.PlaintextFromMarkdown(bodyTmpl) bodyPlaintext, err := markdown.PlaintextFromMarkdown(bodyMarkdown)
if err != nil { if err != nil {
return nil, xerrors.Errorf("render body: %w", err) return nil, xerrors.Errorf("render body: %w", err)
} }
return w.dispatch(payload, title, body, w.cfg.Endpoint.String()), nil return w.dispatch(payload, titlePlaintext, titleMarkdown, bodyPlaintext, bodyMarkdown, w.cfg.Endpoint.String()), nil
} }
func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, title, body, endpoint string) DeliveryFunc { func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, titlePlaintext, titleMarkdown, bodyPlaintext, bodyMarkdown, endpoint string) DeliveryFunc {
return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) { return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) {
// Prepare payload. // Prepare payload.
payload := WebhookPayload{ payload := WebhookPayload{
Version: "1.0", Version: "1.1",
MsgID: msgID, MsgID: msgID,
Title: title, Title: titlePlaintext,
Body: body, TitleMarkdown: titleMarkdown,
Payload: msgPayload, Body: bodyPlaintext,
BodyMarkdown: bodyMarkdown,
Payload: msgPayload,
} }
m, err := json.Marshal(payload) m, err := json.Marshal(payload)
if err != nil { if err != nil {

View File

@ -28,17 +28,15 @@ func TestWebhook(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
titleTemplate = "this is the title ({{.Labels.foo}})" titlePlaintext = "this is the title"
bodyTemplate = "this is the body ({{.Labels.baz}})" titleMarkdown = "this *is* _the_ title"
bodyPlaintext = "this is the body"
bodyMarkdown = "~this~ is the `body`"
) )
msgPayload := types.MessagePayload{ msgPayload := types.MessagePayload{
Version: "1.0", Version: "1.0",
NotificationName: "test", NotificationName: "test",
Labels: map[string]string{
"foo": "bar",
"baz": "quux",
},
} }
tests := []struct { tests := []struct {
@ -61,6 +59,11 @@ func TestWebhook(t *testing.T) {
assert.Equal(t, msgID, payload.MsgID) assert.Equal(t, msgID, payload.MsgID)
assert.Equal(t, msgID.String(), r.Header.Get("X-Message-Id")) assert.Equal(t, msgID.String(), r.Header.Get("X-Message-Id"))
assert.Equal(t, titlePlaintext, payload.Title)
assert.Equal(t, titleMarkdown, payload.TitleMarkdown)
assert.Equal(t, bodyPlaintext, payload.Body)
assert.Equal(t, bodyMarkdown, payload.BodyMarkdown)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(fmt.Sprintf("received %s", payload.MsgID))) _, err = w.Write([]byte(fmt.Sprintf("received %s", payload.MsgID)))
assert.NoError(t, err) assert.NoError(t, err)
@ -138,7 +141,7 @@ func TestWebhook(t *testing.T) {
Endpoint: *serpent.URLOf(endpoint), Endpoint: *serpent.URLOf(endpoint),
} }
handler := dispatch.NewWebhookHandler(cfg, logger.With(slog.F("test", tc.name))) handler := dispatch.NewWebhookHandler(cfg, logger.With(slog.F("test", tc.name)))
deliveryFn, err := handler.Dispatcher(msgPayload, titleTemplate, bodyTemplate) deliveryFn, err := handler.Dispatcher(msgPayload, titleMarkdown, bodyMarkdown)
require.NoError(t, err) require.NoError(t, err)
retryable, err := deliveryFn(ctx, msgID) retryable, err := deliveryFn(ctx, msgID)

View File

@ -249,7 +249,7 @@ func TestWebhookDispatch(t *testing.T) {
// THEN: the webhook is received by the mock server and has the expected contents // THEN: the webhook is received by the mock server and has the expected contents
payload := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, sent) payload := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, sent)
require.EqualValues(t, "1.0", payload.Version) require.EqualValues(t, "1.1", payload.Version)
require.Equal(t, *msgID, payload.MsgID) require.Equal(t, *msgID, payload.MsgID)
require.Equal(t, payload.Payload.Labels, input) require.Equal(t, payload.Payload.Labels, input)
require.Equal(t, payload.Payload.UserEmail, email) require.Equal(t, payload.Payload.UserEmail, email)

View File

@ -90,9 +90,11 @@ receiver.router.post("/v1/webhook", async (req, res) => {
return res.status(400).send("Error: request body is missing"); return res.status(400).send("Error: request body is missing");
} }
const { title, body } = req.body; const { title_markdown, body_markdown } = req.body;
if (!title || !body) { if (!title_markdown || !body_markdown) {
return res.status(400).send('Error: missing fields: "title", or "body"'); return res
.status(400)
.send('Error: missing fields: "title_markdown", or "body_markdown"');
} }
const payload = req.body.payload; const payload = req.body.payload;
@ -118,11 +120,11 @@ receiver.router.post("/v1/webhook", async (req, res) => {
blocks: [ blocks: [
{ {
type: "header", type: "header",
text: { type: "plain_text", text: title }, text: { type: "mrkdwn", text: title_markdown },
}, },
{ {
type: "section", type: "section",
text: { type: "mrkdwn", text: body }, text: { type: "mrkdwn", text: body_markdown },
}, },
], ],
}; };

View File

@ -67,10 +67,10 @@ The process of setting up a Teams workflow consists of three key steps:
} }
} }
}, },
"title": { "title_markdown": {
"type": "string" "type": "string"
}, },
"body": { "body_markdown": {
"type": "string" "type": "string"
} }
} }
@ -108,11 +108,11 @@ The process of setting up a Teams workflow consists of three key steps:
}, },
{ {
"type": "TextBlock", "type": "TextBlock",
"text": "**@{replace(body('Parse_JSON')?['title'], '"', '\"')}**" "text": "**@{replace(body('Parse_JSON')?['title_markdown'], '"', '\"')}**"
}, },
{ {
"type": "TextBlock", "type": "TextBlock",
"text": "@{replace(body('Parse_JSON')?['body'], '"', '\"')}", "text": "@{replace(body('Parse_JSON')?['body_markdown'], '"', '\"')}",
"wrap": true "wrap": true
}, },
{ {