teg/backend/notification_preferences.go
2026-06-15 20:59:44 +02:00

219 lines
5.7 KiB
Go

// backend/notification_preferences.go
package main
import (
"context"
"errors"
"net/http"
"strings"
"github.com/jackc/pgx/v5"
)
type notificationPreferencesResponse struct {
SoundEnabled bool `json:"soundEnabled"`
SoundName string `json:"soundName"`
EmailEnabled bool `json:"emailEnabled"`
DeviceUpdates bool `json:"deviceUpdates"`
DeviceJournals bool `json:"deviceJournals"`
ChatMessages bool `json:"chatMessages"`
ChannelMessages bool `json:"channelMessages"`
DesktopEnabled bool `json:"desktopEnabled"`
PushChatMessages bool `json:"pushChatMessages"`
PushChannelMessages bool `json:"pushChannelMessages"`
}
func defaultNotificationPreferences() notificationPreferencesResponse {
return notificationPreferencesResponse{
SoundEnabled: false,
SoundName: "default",
EmailEnabled: false,
DeviceUpdates: true,
DeviceJournals: true,
ChatMessages: true,
ChannelMessages: true,
DesktopEnabled: false,
PushChatMessages: true,
PushChannelMessages: true,
}
}
// shouldPushNotification entscheidet anhand der granularen Push-Einstellungen,
// ob für den jeweiligen Benachrichtigungstyp ein Web-Push gesendet werden soll.
// Es deckt aktuell die Nachrichtentypen ab, für die der Web-Push-Payload korrekt
// aufgebaut werden kann (Chat- und Channel-Nachrichten).
func shouldPushNotification(
preferences notificationPreferencesResponse,
notificationType string,
) bool {
switch notificationType {
case "channel.message":
return preferences.PushChannelMessages
case "chat.message":
return preferences.PushChatMessages
default:
return false
}
}
func normalizeNotificationSoundName(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
switch value {
case "default", "chime", "soft", "alert", "success":
return value
default:
return "default"
}
}
func (s *Server) handleGetNotificationPreferences(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
settings, err := s.getNotificationPreferences(r.Context(), user.ID)
if errors.Is(err, pgx.ErrNoRows) {
writeJSON(w, http.StatusOK, map[string]any{
"settings": defaultNotificationPreferences(),
})
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen konnten nicht geladen werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"settings": settings,
})
}
func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var input notificationPreferencesResponse
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
input.SoundName = normalizeNotificationSoundName(input.SoundName)
_, err := s.db.Exec(
r.Context(),
`
INSERT INTO notification_preferences (
user_id,
sound_enabled,
sound_name,
email_enabled,
device_updates,
device_journals,
chat_messages,
channel_messages,
desktop_enabled,
push_chat_messages,
push_channel_messages
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (user_id)
DO UPDATE SET
sound_enabled = EXCLUDED.sound_enabled,
sound_name = EXCLUDED.sound_name,
email_enabled = EXCLUDED.email_enabled,
device_updates = EXCLUDED.device_updates,
device_journals = EXCLUDED.device_journals,
chat_messages = EXCLUDED.chat_messages,
channel_messages = EXCLUDED.channel_messages,
desktop_enabled = EXCLUDED.desktop_enabled,
push_chat_messages = EXCLUDED.push_chat_messages,
push_channel_messages = EXCLUDED.push_channel_messages,
updated_at = now()
`,
user.ID,
input.SoundEnabled,
input.SoundName,
input.EmailEnabled,
input.DeviceUpdates,
input.DeviceJournals,
input.ChatMessages,
input.ChannelMessages,
input.DesktopEnabled,
input.PushChatMessages,
input.PushChannelMessages,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen konnten nicht gespeichert werden")
return
}
settings, err := s.getNotificationPreferences(r.Context(), user.ID)
if err != nil {
writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen wurden gespeichert, konnten aber nicht neu geladen werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"settings": settings,
})
}
func (s *Server) getNotificationPreferences(ctx context.Context, userID string) (notificationPreferencesResponse, error) {
settings := defaultNotificationPreferences()
err := s.db.QueryRow(
ctx,
`
SELECT
sound_enabled,
COALESCE(sound_name, 'default'),
email_enabled,
device_updates,
device_journals,
chat_messages,
channel_messages,
desktop_enabled,
push_chat_messages,
push_channel_messages
FROM notification_preferences
WHERE user_id = $1
LIMIT 1
`,
userID,
).Scan(
&settings.SoundEnabled,
&settings.SoundName,
&settings.EmailEnabled,
&settings.DeviceUpdates,
&settings.DeviceJournals,
&settings.ChatMessages,
&settings.ChannelMessages,
&settings.DesktopEnabled,
&settings.PushChatMessages,
&settings.PushChannelMessages,
)
settings.SoundName = normalizeNotificationSoundName(settings.SoundName)
return settings, err
}
func (s *Server) getNotificationPreferencesOrDefault(ctx context.Context, userID string) (notificationPreferencesResponse, error) {
settings, err := s.getNotificationPreferences(ctx, userID)
if errors.Is(err, pgx.ErrNoRows) {
return defaultNotificationPreferences(), nil
}
return settings, err
}