diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 868657683c9c8..fe6aacf84d5dd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1693,6 +1693,13 @@ const docTemplate = `{ "description": "Filter notifications by read status. Possible values: read, unread, all", "name": "read_status", "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "ID of the last notification from the current page. Notifications returned will be older than the associated one", + "name": "starting_before", + "in": "query" } ], "responses": { @@ -1757,6 +1764,16 @@ const docTemplate = `{ "description": "Filter notifications by read status. Possible values: read, unread, all", "name": "read_status", "in": "query" + }, + { + "enum": [ + "plaintext", + "markdown" + ], + "type": "string", + "description": "Define the output format for notifications title and body.", + "name": "format", + "in": "query" } ], "responses": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a82fd53d6b24f..7a399a0e044b4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1474,6 +1474,13 @@ "description": "Filter notifications by read status. Possible values: read, unread, all", "name": "read_status", "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "ID of the last notification from the current page. Notifications returned will be older than the associated one", + "name": "starting_before", + "in": "query" } ], "responses": { @@ -1532,6 +1539,13 @@ "description": "Filter notifications by read status. Possible values: read, unread, all", "name": "read_status", "in": "query" + }, + { + "enum": ["plaintext", "markdown"], + "type": "string", + "description": "Define the output format for notifications title and body.", + "name": "format", + "in": "query" } ], "responses": { diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 23e1c8479a76b..37ae8905c7d24 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -17,11 +17,17 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/pubsub" + markdown "github.com/coder/coder/v2/coderd/render" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/websocket" ) +const ( + notificationFormatMarkdown = "markdown" + notificationFormatPlaintext = "plaintext" +) + // convertInboxNotificationResponse works as a util function to transform a database.InboxNotification to codersdk.InboxNotification func convertInboxNotificationResponse(ctx context.Context, logger slog.Logger, notif database.InboxNotification) codersdk.InboxNotification { return codersdk.InboxNotification{ @@ -60,6 +66,7 @@ func convertInboxNotificationResponse(ctx context.Context, logger slog.Logger, n // @Param targets query string false "Comma-separated list of target IDs to filter notifications" // @Param templates query string false "Comma-separated list of template IDs to filter notifications" // @Param read_status query string false "Filter notifications by read status. Possible values: read, unread, all" +// @Param format query string false "Define the output format for notifications title and body." enums(plaintext,markdown) // @Success 200 {object} codersdk.GetInboxNotificationResponse // @Router /notifications/inbox/watch [get] func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) { @@ -73,6 +80,7 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) targets = p.UUIDs(vals, []uuid.UUID{}, "targets") templates = p.UUIDs(vals, []uuid.UUID{}, "templates") readStatus = p.String(vals, "all", "read_status") + format = p.String(vals, notificationFormatMarkdown, "format") ) p.ErrorExcessParams(vals) if len(p.Errors) > 0 { @@ -176,6 +184,23 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) api.Logger.Error(ctx, "failed to count unread inbox notifications", slog.Error(err)) return } + + // By default, notifications are stored as markdown + // We can change the format based on parameter if required + if format == notificationFormatPlaintext { + notif.Title, err = markdown.PlaintextFromMarkdown(notif.Title) + if err != nil { + api.Logger.Error(ctx, "failed to convert notification title to plain text", slog.Error(err)) + return + } + + notif.Content, err = markdown.PlaintextFromMarkdown(notif.Content) + if err != nil { + api.Logger.Error(ctx, "failed to convert notification content to plain text", slog.Error(err)) + return + } + } + if err := encoder.Encode(codersdk.GetInboxNotificationResponse{ Notification: notif, UnreadCount: int(unreadCount), @@ -196,6 +221,7 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) // @Param targets query string false "Comma-separated list of target IDs to filter notifications" // @Param templates query string false "Comma-separated list of template IDs to filter notifications" // @Param read_status query string false "Filter notifications by read status. Possible values: read, unread, all" +// @Param starting_before query string false "ID of the last notification from the current page. Notifications returned will be older than the associated one" format(uuid) // @Success 200 {object} codersdk.ListInboxNotificationsResponse // @Router /notifications/inbox [get] func (api *API) listInboxNotifications(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index ef095ed72988c..ed0696195cb60 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -137,6 +137,62 @@ func TestInboxNotification_Watch(t *testing.T) { require.Equal(t, memberClient.ID, notif.Notification.UserID) }) + t.Run("OK - change format", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + logger := testutil.Logger(t) + + db, ps := dbtestutil.NewDB(t) + + firstClient, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Pubsub: ps, + Database: db, + }) + firstUser := coderdtest.CreateFirstUser(t, firstClient) + member, memberClient := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin()) + + u, err := member.URL.Parse("/api/v2/notifications/inbox/watch?format=plaintext") + require.NoError(t, err) + + // nolint:bodyclose + wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPHeader: http.Header{ + "Coder-Session-Token": []string{member.SessionToken()}, + }, + }) + if err != nil { + if resp.StatusCode != http.StatusSwitchingProtocols { + err = codersdk.ReadBodyAsError(resp) + } + require.NoError(t, err) + } + defer wsConn.Close(websocket.StatusNormalClosure, "done") + + inboxHandler := dispatch.NewInboxHandler(logger, db, ps) + dispatchFunc, err := inboxHandler.Dispatcher(types.MessagePayload{ + UserID: memberClient.ID.String(), + NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(), + }, "# Notification Title", "This is the __content__.", nil) + require.NoError(t, err) + + _, err = dispatchFunc(ctx, uuid.New()) + require.NoError(t, err) + + _, message, err := wsConn.Read(ctx) + require.NoError(t, err) + + var notif codersdk.GetInboxNotificationResponse + err = json.Unmarshal(message, ¬if) + require.NoError(t, err) + + require.Equal(t, 1, notif.UnreadCount) + require.Equal(t, memberClient.ID, notif.Notification.UserID) + + require.Equal(t, "Notification Title", notif.Notification.Title) + require.Equal(t, "This is the content.", notif.Notification.Content) + }) + t.Run("OK - filters on templates", func(t *testing.T) { t.Parallel() diff --git a/coderd/notifications/dispatch/inbox.go b/coderd/notifications/dispatch/inbox.go index 9383e89afec3e..63e21acb56b80 100644 --- a/coderd/notifications/dispatch/inbox.go +++ b/coderd/notifications/dispatch/inbox.go @@ -16,7 +16,6 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/notifications/types" coderdpubsub "github.com/coder/coder/v2/coderd/pubsub" - markdown "github.com/coder/coder/v2/coderd/render" "github.com/coder/coder/v2/codersdk" ) @@ -36,17 +35,7 @@ func NewInboxHandler(log slog.Logger, store InboxStore, ps pubsub.Pubsub) *Inbox } func (s *InboxHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string, _ template.FuncMap) (DeliveryFunc, error) { - subject, err := markdown.PlaintextFromMarkdown(titleTmpl) - if err != nil { - return nil, xerrors.Errorf("render subject: %w", err) - } - - htmlBody, err := markdown.PlaintextFromMarkdown(bodyTmpl) - if err != nil { - return nil, xerrors.Errorf("render html body: %w", err) - } - - return s.dispatch(payload, subject, htmlBody), nil + return s.dispatch(payload, titleTmpl, bodyTmpl), nil } func (s *InboxHandler) dispatch(payload types.MessagePayload, title, body string) DeliveryFunc { diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index 67b61bccb6302..09890d3b17864 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -61,11 +61,12 @@ curl -X GET http://coder-server:8080/api/v2/notifications/inbox \ ### Parameters -| Name | In | Type | Required | Description | -|---------------|-------|--------|----------|-------------------------------------------------------------------------| -| `targets` | query | string | false | Comma-separated list of target IDs to filter notifications | -| `templates` | query | string | false | Comma-separated list of template IDs to filter notifications | -| `read_status` | query | string | false | Filter notifications by read status. Possible values: read, unread, all | +| Name | In | Type | Required | Description | +|-------------------|-------|--------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `targets` | query | string | false | Comma-separated list of target IDs to filter notifications | +| `templates` | query | string | false | Comma-separated list of template IDs to filter notifications | +| `read_status` | query | string | false | Filter notifications by read status. Possible values: read, unread, all | +| `starting_before` | query | string(uuid) | false | ID of the last notification from the current page. Notifications returned will be older than the associated one | ### Example responses @@ -146,6 +147,14 @@ curl -X GET http://coder-server:8080/api/v2/notifications/inbox/watch \ | `targets` | query | string | false | Comma-separated list of target IDs to filter notifications | | `templates` | query | string | false | Comma-separated list of template IDs to filter notifications | | `read_status` | query | string | false | Filter notifications by read status. Possible values: read, unread, all | +| `format` | query | string | false | Define the output format for notifications title and body. | + +#### Enumerated Values + +| Parameter | Value | +|-----------|-------------| +| `format` | `plaintext` | +| `format` | `markdown` | ### Example responses