2026-10-18 17:40:41 +02:00
|
|
|
package queue
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Allowed values for quiet_mode (public config value).
|
2026-10-18 17:40:41 +02:00
|
|
|
const (
|
|
|
|
|
QuietModeDiscard = "discard"
|
|
|
|
|
QuietModeToast = "toast"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// clockTime is a time of day at minute resolution (local time, 24 h).
|
2026-10-18 17:40:41 +02:00
|
|
|
type clockTime struct{ hour, minute int }
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// minutes returns minutes since midnight.
|
2026-10-18 17:40:41 +02:00
|
|
|
func (c clockTime) minutes() int { return c.hour*60 + c.minute }
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// String renders the time as "HH:MM".
|
2026-10-18 17:40:41 +02:00
|
|
|
func (c clockTime) String() string { return fmt.Sprintf("%02d:%02d", c.hour, c.minute) }
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// QuietHours is a quiet window in local time, for example "18:00-08:00"
|
|
|
|
|
// (spanning midnight). The zero value (Start == End) is disabled; silence
|
|
|
|
|
// around the clock is mute, not quiet hours.
|
2026-10-18 17:40:41 +02:00
|
|
|
type QuietHours struct {
|
|
|
|
|
Start, End clockTime
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// String renders the window as "HH:MM-HH:MM" (zero value: "").
|
2026-10-18 17:40:41 +02:00
|
|
|
func (q QuietHours) String() string {
|
|
|
|
|
if q.Start == q.End {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return q.Start.String() + "-" + q.End.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// ParseQuietHours parses a quiet window "HH:MM-HH:MM" (local time, 24 h;
|
|
|
|
|
// hours with one or two digits). Windows across midnight (Start > End) are
|
|
|
|
|
// allowed; Start == End disables the window. The empty string is the
|
|
|
|
|
// disabled window (no error).
|
2026-10-18 17:40:41 +02:00
|
|
|
func ParseQuietHours(s string) (QuietHours, error) {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return QuietHours{}, nil
|
|
|
|
|
}
|
|
|
|
|
startS, endS, ok := strings.Cut(s, "-")
|
|
|
|
|
if !ok {
|
2026-10-24 11:15:35 +02:00
|
|
|
return QuietHours{}, fmt.Errorf("quiet_hours: %q is not HH:MM-HH:MM", s)
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
start, err := parseClock(startS)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return QuietHours{}, fmt.Errorf("quiet_hours: %v", err)
|
|
|
|
|
}
|
|
|
|
|
end, err := parseClock(endS)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return QuietHours{}, fmt.Errorf("quiet_hours: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return QuietHours{Start: start, End: end}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// parseClock parses "H[H]:MM" into its parts.
|
2026-10-18 17:40:41 +02:00
|
|
|
func parseClock(s string) (clockTime, error) {
|
|
|
|
|
hourS, minuteS, ok := strings.Cut(s, ":")
|
|
|
|
|
if !ok || len(hourS) < 1 || len(hourS) > 2 || len(minuteS) != 2 {
|
2026-10-24 11:15:35 +02:00
|
|
|
return clockTime{}, fmt.Errorf("%q is not HH:MM", s)
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
hour, err := strconv.Atoi(hourS)
|
|
|
|
|
if err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
return clockTime{}, fmt.Errorf("%q is not HH:MM", s)
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
minute, err := strconv.Atoi(minuteS)
|
|
|
|
|
if err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
return clockTime{}, fmt.Errorf("%q is not HH:MM", s)
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
2026-10-24 11:15:35 +02:00
|
|
|
return clockTime{}, fmt.Errorf("%q is not a valid time of day (24 h)", s)
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
return clockTime{hour: hour, minute: minute}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Active reports whether the local time t lies inside the window. Day
|
|
|
|
|
// windows (Start < End) run from Start up to but excluding End; night
|
|
|
|
|
// windows (Start > End) span midnight.
|
2026-10-18 17:40:41 +02:00
|
|
|
func (q QuietHours) Active(t time.Time) bool {
|
|
|
|
|
if q.Start == q.End {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
m := t.Hour()*60 + t.Minute()
|
|
|
|
|
s, e := q.Start.minutes(), q.End.minutes()
|
|
|
|
|
if s < e {
|
|
|
|
|
return m >= s && m < e
|
|
|
|
|
}
|
|
|
|
|
return m >= s || m < e
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Until returns the local end of the currently active window; the zero time
|
|
|
|
|
// when no window is active. Basis for GET /status.
|
2026-10-18 17:40:41 +02:00
|
|
|
func (q QuietHours) Until(t time.Time) time.Time {
|
|
|
|
|
if !q.Active(t) {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
end := time.Date(t.Year(), t.Month(), t.Day(), q.End.hour, q.End.minute, 0, 0, t.Location())
|
|
|
|
|
if q.Start.minutes() > q.End.minutes() && t.Hour()*60+t.Minute() >= q.Start.minutes() {
|
2026-10-24 11:15:35 +02:00
|
|
|
end = end.AddDate(0, 0, 1) // night window: the end is tomorrow
|
2026-10-18 17:40:41 +02:00
|
|
|
}
|
|
|
|
|
return end
|
|
|
|
|
}
|