mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
347 lines
11 KiB
Go
347 lines
11 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"cdr.dev/slog"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
"github.com/coder/coder/v2/coderd/pubsub"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/wsjson"
|
|
"github.com/coder/websocket"
|
|
)
|
|
|
|
// 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{
|
|
ID: notif.ID,
|
|
UserID: notif.UserID,
|
|
TemplateID: notif.TemplateID,
|
|
Targets: notif.Targets,
|
|
Title: notif.Title,
|
|
Content: notif.Content,
|
|
Icon: notif.Icon,
|
|
Actions: func() []codersdk.InboxNotificationAction {
|
|
var actionsList []codersdk.InboxNotificationAction
|
|
err := json.Unmarshal([]byte(notif.Actions), &actionsList)
|
|
if err != nil {
|
|
logger.Error(ctx, "unmarshal inbox notification actions", slog.Error(err))
|
|
}
|
|
return actionsList
|
|
}(),
|
|
ReadAt: func() *time.Time {
|
|
if !notif.ReadAt.Valid {
|
|
return nil
|
|
}
|
|
return ¬if.ReadAt.Time
|
|
}(),
|
|
CreatedAt: notif.CreatedAt,
|
|
}
|
|
}
|
|
|
|
// watchInboxNotifications watches for new inbox notifications and sends them to the client.
|
|
// The client can specify a list of target IDs to filter the notifications.
|
|
// @Summary Watch for new inbox notifications
|
|
// @ID watch-for-new-inbox-notifications
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Notifications
|
|
// @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"
|
|
// @Success 200 {object} codersdk.GetInboxNotificationResponse
|
|
// @Router /notifications/inbox/watch [get]
|
|
func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) {
|
|
p := httpapi.NewQueryParamParser()
|
|
vals := r.URL.Query()
|
|
|
|
var (
|
|
ctx = r.Context()
|
|
apikey = httpmw.APIKey(r)
|
|
|
|
targets = p.UUIDs(vals, []uuid.UUID{}, "targets")
|
|
templates = p.UUIDs(vals, []uuid.UUID{}, "templates")
|
|
readStatus = p.String(vals, "all", "read_status")
|
|
)
|
|
p.ErrorExcessParams(vals)
|
|
if len(p.Errors) > 0 {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Query parameters have invalid values.",
|
|
Validations: p.Errors,
|
|
})
|
|
return
|
|
}
|
|
|
|
if !slices.Contains([]string{
|
|
string(database.InboxNotificationReadStatusAll),
|
|
string(database.InboxNotificationReadStatusRead),
|
|
string(database.InboxNotificationReadStatusUnread),
|
|
}, readStatus) {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "starting_before query parameter should be any of 'all', 'read', 'unread'.",
|
|
})
|
|
return
|
|
}
|
|
|
|
notificationCh := make(chan codersdk.InboxNotification, 10)
|
|
|
|
closeInboxNotificationsSubscriber, err := api.Pubsub.SubscribeWithErr(pubsub.InboxNotificationForOwnerEventChannel(apikey.UserID),
|
|
pubsub.HandleInboxNotificationEvent(
|
|
func(ctx context.Context, payload pubsub.InboxNotificationEvent, err error) {
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "inbox notification event", slog.Error(err))
|
|
return
|
|
}
|
|
|
|
// HandleInboxNotificationEvent cb receives all the inbox notifications - without any filters excepted the user_id.
|
|
// Based on query parameters defined above and filters defined by the client - we then filter out the
|
|
// notifications we do not want to forward and discard it.
|
|
|
|
// filter out notifications that don't match the targets
|
|
if len(targets) > 0 {
|
|
for _, target := range targets {
|
|
if isFound := slices.Contains(payload.InboxNotification.Targets, target); !isFound {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// filter out notifications that don't match the templates
|
|
if len(templates) > 0 {
|
|
if isFound := slices.Contains(templates, payload.InboxNotification.TemplateID); !isFound {
|
|
return
|
|
}
|
|
}
|
|
|
|
// filter out notifications that don't match the read status
|
|
if readStatus != "" {
|
|
if readStatus == string(database.InboxNotificationReadStatusRead) {
|
|
if payload.InboxNotification.ReadAt == nil {
|
|
return
|
|
}
|
|
} else if readStatus == string(database.InboxNotificationReadStatusUnread) {
|
|
if payload.InboxNotification.ReadAt != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// keep a safe guard in case of latency to push notifications through websocket
|
|
select {
|
|
case notificationCh <- payload.InboxNotification:
|
|
default:
|
|
api.Logger.Error(ctx, "failed to push consumed notification into websocket handler, check latency")
|
|
}
|
|
},
|
|
))
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "subscribe to inbox notification event", slog.Error(err))
|
|
return
|
|
}
|
|
defer closeInboxNotificationsSubscriber()
|
|
|
|
conn, err := websocket.Accept(rw, r, nil)
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to upgrade connection to websocket.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
go httpapi.Heartbeat(ctx, conn)
|
|
defer conn.Close(websocket.StatusNormalClosure, "connection closed")
|
|
|
|
encoder := wsjson.NewEncoder[codersdk.GetInboxNotificationResponse](conn, websocket.MessageText)
|
|
defer encoder.Close(websocket.StatusNormalClosure)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case notif := <-notificationCh:
|
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to count unread inbox notifications", slog.Error(err))
|
|
return
|
|
}
|
|
if err := encoder.Encode(codersdk.GetInboxNotificationResponse{
|
|
Notification: notif,
|
|
UnreadCount: int(unreadCount),
|
|
}); err != nil {
|
|
api.Logger.Error(ctx, "encode notification", slog.Error(err))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// listInboxNotifications lists the notifications for the user.
|
|
// @Summary List inbox notifications
|
|
// @ID list-inbox-notifications
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Notifications
|
|
// @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"
|
|
// @Success 200 {object} codersdk.ListInboxNotificationsResponse
|
|
// @Router /notifications/inbox [get]
|
|
func (api *API) listInboxNotifications(rw http.ResponseWriter, r *http.Request) {
|
|
p := httpapi.NewQueryParamParser()
|
|
vals := r.URL.Query()
|
|
|
|
var (
|
|
ctx = r.Context()
|
|
apikey = httpmw.APIKey(r)
|
|
|
|
targets = p.UUIDs(vals, nil, "targets")
|
|
templates = p.UUIDs(vals, nil, "templates")
|
|
readStatus = p.String(vals, "all", "read_status")
|
|
startingBefore = p.UUID(vals, uuid.Nil, "starting_before")
|
|
)
|
|
p.ErrorExcessParams(vals)
|
|
if len(p.Errors) > 0 {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Query parameters have invalid values.",
|
|
Validations: p.Errors,
|
|
})
|
|
return
|
|
}
|
|
|
|
if !slices.Contains([]string{
|
|
string(database.InboxNotificationReadStatusAll),
|
|
string(database.InboxNotificationReadStatusRead),
|
|
string(database.InboxNotificationReadStatusUnread),
|
|
}, readStatus) {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "starting_before query parameter should be any of 'all', 'read', 'unread'.",
|
|
})
|
|
return
|
|
}
|
|
|
|
createdBefore := dbtime.Now()
|
|
if startingBefore != uuid.Nil {
|
|
lastNotif, err := api.Database.GetInboxNotificationByID(ctx, startingBefore)
|
|
if err == nil {
|
|
createdBefore = lastNotif.CreatedAt
|
|
}
|
|
}
|
|
|
|
notifs, err := api.Database.GetFilteredInboxNotificationsByUserID(ctx, database.GetFilteredInboxNotificationsByUserIDParams{
|
|
UserID: apikey.UserID,
|
|
Templates: templates,
|
|
Targets: targets,
|
|
ReadStatus: database.InboxNotificationReadStatus(readStatus),
|
|
CreatedAtOpt: createdBefore,
|
|
})
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to get filtered inbox notifications", slog.Error(err))
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to get filtered inbox notifications.",
|
|
})
|
|
return
|
|
}
|
|
|
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to count unread inbox notifications", slog.Error(err))
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to count unread inbox notifications.",
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ListInboxNotificationsResponse{
|
|
Notifications: func() []codersdk.InboxNotification {
|
|
notificationsList := make([]codersdk.InboxNotification, 0, len(notifs))
|
|
for _, notification := range notifs {
|
|
notificationsList = append(notificationsList, convertInboxNotificationResponse(ctx, api.Logger, notification))
|
|
}
|
|
return notificationsList
|
|
}(),
|
|
UnreadCount: int(unreadCount),
|
|
})
|
|
}
|
|
|
|
// updateInboxNotificationReadStatus changes the read status of a notification.
|
|
// @Summary Update read status of a notification
|
|
// @ID update-read-status-of-a-notification
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Notifications
|
|
// @Param id path string true "id of the notification"
|
|
// @Success 200 {object} codersdk.Response
|
|
// @Router /notifications/inbox/{id}/read-status [put]
|
|
func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
apikey = httpmw.APIKey(r)
|
|
)
|
|
|
|
notificationID, ok := httpmw.ParseUUIDParam(rw, r, "id")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body codersdk.UpdateInboxNotificationReadStatusRequest
|
|
if !httpapi.Read(ctx, rw, r, &body) {
|
|
return
|
|
}
|
|
|
|
err := api.Database.UpdateInboxNotificationReadStatus(ctx, database.UpdateInboxNotificationReadStatusParams{
|
|
ID: notificationID,
|
|
ReadAt: func() sql.NullTime {
|
|
if body.IsRead {
|
|
return sql.NullTime{
|
|
Time: dbtime.Now(),
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
return sql.NullTime{}
|
|
}(),
|
|
})
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to update inbox notification read status", slog.Error(err))
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to update inbox notification read status.",
|
|
})
|
|
return
|
|
}
|
|
|
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to call count unread inbox notifications", slog.Error(err))
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to call count unread inbox notifications.",
|
|
})
|
|
return
|
|
}
|
|
|
|
updatedNotification, err := api.Database.GetInboxNotificationByID(ctx, notificationID)
|
|
if err != nil {
|
|
api.Logger.Error(ctx, "failed to get notification by id", slog.Error(err))
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to get notification by id.",
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateInboxNotificationReadStatusResponse{
|
|
Notification: convertInboxNotificationResponse(ctx, api.Logger, updatedNotification),
|
|
UnreadCount: int(unreadCount),
|
|
})
|
|
}
|