Files
oslstats/internal/handlers/notifswebsocket.go
2026-02-09 19:30:47 +11:00

112 lines
2.7 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/notify"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/throw"
"git.haelnorr.com/h/oslstats/internal/view/popup"
"github.com/coder/websocket"
"github.com/pkg/errors"
)
func NotificationWS(
s *hws.Server,
cfg *config.Config,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "websocket" {
throw.NotFound(s, w, r, r.URL.Path)
return
}
nc, err := setupClient(s, w, r)
if err != nil {
logError(s, "Failed to get notification client", errors.Wrap(err, "setupClient"))
return
}
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: []string{cfg.HWSAuth.TrustedHost},
})
if err != nil {
logError(s, "Failed to open websocket", errors.Wrap(err, "websocket.Accept"))
return
}
ctx := ws.CloseRead(r.Context())
err = notifyLoop(ctx, nc, ws)
if err != nil {
logError(s, "Notification error", errors.Wrap(err, "notifyLoop"))
}
err = ws.CloseNow()
if err != nil {
logError(s, "Error closing websocket", errors.Wrap(err, "ws.CloseNow"))
}
},
)
}
func setupClient(s *hws.Server, w http.ResponseWriter, r *http.Request) (*hws.Client, error) {
user := db.CurrentUser(r.Context())
altID := ""
if user != nil {
altID = strconv.Itoa(user.ID)
}
subCookie, err := r.Cookie("ws_sub_id")
subID := ""
if err == nil {
subID = subCookie.Value
}
nc, err := s.GetClient(subID, altID)
if err != nil {
return nil, errors.Wrap(err, "s.GetClient")
}
cookies.SetCookie(w, "ws_sub_id", "/", nc.ID(), 0)
return nc, nil
}
func notifyLoop(ctx context.Context, c *hws.Client, ws *websocket.Conn) error {
notifs, stop := c.Listen()
defer close(stop)
count := 0
for {
select {
case <-ctx.Done():
return nil
case nt, ok := <-notifs:
count++
if !ok {
return nil
}
w, err := ws.Writer(ctx, websocket.MessageText)
if err != nil {
return errors.Wrap(err, "ws.Writer")
}
switch nt.Level {
case hws.LevelShutdown:
err = popup.Toast(nt, count, 30000).Render(ctx, w)
case notify.LevelWarn:
err = popup.Toast(nt, count, 10000).Render(ctx, w)
case notify.LevelError:
// Parse error code and stacktrace from Details field
code, stacktrace := parseErrorDetails(nt.Details)
err = popup.ErrorModalWS(code, stacktrace, nt, count).Render(ctx, w)
default:
err = popup.Toast(nt, count, 6000).Render(ctx, w)
}
if err != nil {
return errors.Wrap(err, "popup.Toast")
}
err = w.Close()
if err != nil {
return errors.Wrap(err, "w.Close")
}
}
}
}