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/view/component/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" { throwNotFound(s, w, r, r.URL.Path) return } nc, err := setupClient(s, w, r) if err != nil { s.LogError(hws.HWSError{ Message: "Failed to get notification client", Error: err, Level: hws.ErrorERROR, StatusCode: http.StatusInternalServerError, }) return } ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{ OriginPatterns: []string{cfg.HWSAuth.TrustedHost}, }) if err != nil { s.LogError(hws.HWSError{ Message: "Failed to open websocket", Error: err, Level: hws.ErrorERROR, StatusCode: http.StatusInternalServerError, }) return } defer ws.CloseNow() ctx := ws.CloseRead(r.Context()) err = notifyLoop(ctx, nc, ws) if err != nil { s.LogError(hws.HWSError{ Message: "Notification error", Error: err, Level: hws.ErrorERROR, StatusCode: http.StatusInternalServerError, }) } }, ) } 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") } } } }