feat: Optionale Kalender-Benachrichtigungen für Geburtstage
This commit is contained in:
@@ -133,7 +133,7 @@ func (d *Daemon) syncBirthdaysToCal(ctx context.Context, httpClient webdav.HTTPC
|
||||
matchedBev := false
|
||||
for _, v := range ev.Data.Children {
|
||||
for i, bev := range bevs {
|
||||
if icalMatchesBev(v, bev) {
|
||||
if icalMatchesBev(v, bev, d.notificationEnabled) {
|
||||
bevsInSync = append(bevsInSync, i)
|
||||
matchedBev = true
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func (d *Daemon) syncBirthdaysToCal(ctx context.Context, httpClient webdav.HTTPC
|
||||
if slices.Contains(bevsInSync, i) {
|
||||
continue
|
||||
}
|
||||
p, ic := v.generateICAL(calendarPath)
|
||||
p, ic := v.generateICAL(calendarPath, d.notificationEnabled, d.notificationTrigger)
|
||||
_, err := cl.PutCalendarObject(ctx, p, ic)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -193,7 +193,7 @@ func generateBirthdayEvents(birthdays []BirthdayContact) []birthdayEvent {
|
||||
return bb
|
||||
}
|
||||
|
||||
func icalMatchesBev(ic *ical.Component, bev birthdayEvent) bool {
|
||||
func icalMatchesBev(ic *ical.Component, bev birthdayEvent, notificationEnabled bool) bool {
|
||||
if ic.Props.Get(ical.PropSummary) == nil || ic.Props.Get(ical.PropSummary).Value != bev.Summary {
|
||||
return false
|
||||
}
|
||||
@@ -211,10 +211,20 @@ func icalMatchesBev(ic *ical.Component, bev birthdayEvent) bool {
|
||||
if dtEnd.Params.Get(ical.ParamValue) != string(ical.ValueDate) {
|
||||
return false
|
||||
}
|
||||
hasAlarm := false
|
||||
for _, child := range ic.Children {
|
||||
if child.Name == ical.CompAlarm {
|
||||
hasAlarm = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if notificationEnabled != hasAlarm {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (bev birthdayEvent) generateICAL(calendar string) (string, *ical.Calendar) {
|
||||
func (bev birthdayEvent) generateICAL(calendar string, notificationEnabled bool, notificationTrigger string) (string, *ical.Calendar) {
|
||||
id := uuid.New().String()
|
||||
cal := ical.NewCalendar()
|
||||
cal.Props.SetText(ical.PropProductID, ConstProductID)
|
||||
@@ -231,6 +241,16 @@ func (bev birthdayEvent) generateICAL(calendar string) (string, *ical.Calendar)
|
||||
end.Value = bev.DateTimeEnd
|
||||
event.Props.Set(start)
|
||||
event.Props.Set(end)
|
||||
if notificationEnabled {
|
||||
alarm := ical.NewComponent(ical.CompAlarm)
|
||||
alarm.Props.SetText(ical.PropAction, "DISPLAY")
|
||||
alarm.Props.SetText(ical.PropDescription, bev.Summary)
|
||||
trigger := ical.NewProp(ical.PropTrigger)
|
||||
trigger.Params.Set(ical.ParamValue, string(ical.ValueDuration))
|
||||
trigger.Value = notificationTrigger
|
||||
alarm.Props.Set(trigger)
|
||||
event.Children = append(event.Children, alarm)
|
||||
}
|
||||
cal.Children = append(cal.Children, event)
|
||||
return fmt.Sprintf("%s/%s.ics", calendar, id), cal
|
||||
}
|
||||
|
||||
@@ -26,15 +26,17 @@ var (
|
||||
)
|
||||
|
||||
type Daemon struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
mailcowClient mailcow.Client
|
||||
userTokens map[string]string
|
||||
userTokensLock *sync.RWMutex
|
||||
stateFilepath string
|
||||
stateUnsaved bool
|
||||
calendarName string
|
||||
oldCalendarName string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
mailcowClient mailcow.Client
|
||||
userTokens map[string]string
|
||||
userTokensLock *sync.RWMutex
|
||||
stateFilepath string
|
||||
stateUnsaved bool
|
||||
calendarName string
|
||||
oldCalendarName string
|
||||
notificationEnabled bool
|
||||
notificationTrigger string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -65,13 +67,29 @@ func run() error {
|
||||
if calendarName == "" {
|
||||
calendarName = "Birthdays"
|
||||
}
|
||||
notificationEnabled := strings.EqualFold(os.Getenv("NOTIFICATION_ENABLED"), "true")
|
||||
notificationTrigger := "PT8H"
|
||||
if notificationEnabled {
|
||||
notificationTime := os.Getenv("NOTIFICATION_TIME")
|
||||
if notificationTime == "" {
|
||||
notificationTime = "08:00"
|
||||
}
|
||||
trigger, err := parseNotificationTrigger(notificationTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid NOTIFICATION_TIME: %w", err)
|
||||
}
|
||||
notificationTrigger = trigger
|
||||
slog.Info("birthday notifications enabled", "time", notificationTime, "trigger", notificationTrigger)
|
||||
}
|
||||
d := &Daemon{
|
||||
userTokens: make(map[string]string),
|
||||
userTokensLock: &sync.RWMutex{},
|
||||
baseURL: mailcowBase,
|
||||
stateFilepath: os.Getenv("STATEFILE"),
|
||||
httpClient: &http.Client{Transport: buildTransport()},
|
||||
calendarName: calendarName,
|
||||
userTokens: make(map[string]string),
|
||||
userTokensLock: &sync.RWMutex{},
|
||||
baseURL: mailcowBase,
|
||||
stateFilepath: os.Getenv("STATEFILE"),
|
||||
httpClient: &http.Client{Transport: buildTransport()},
|
||||
calendarName: calendarName,
|
||||
notificationEnabled: notificationEnabled,
|
||||
notificationTrigger: notificationTrigger,
|
||||
}
|
||||
if len(d.stateFilepath) == 0 {
|
||||
d.stateFilepath = "state.json"
|
||||
|
||||
@@ -65,3 +65,32 @@ func sanitizeBirthday(input string) (uint16, uint16, uint16, error) {
|
||||
}
|
||||
return 0, 0, 0, fmt.Errorf("birthday prop format unknown: %s", input)
|
||||
}
|
||||
|
||||
// parseNotificationTrigger konvertiert eine Uhrzeit im Format "HH:MM" in eine
|
||||
// iCal-Duration (z.B. "PT8H", "PT9H30M"), die als VALARM-Trigger relativ zum
|
||||
// Start eines Ganztags-Events (Mitternacht) verwendet wird.
|
||||
func parseNotificationTrigger(timeStr string) (string, error) {
|
||||
parts := strings.Split(timeStr, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid time format: %s (expected HH:MM)", timeStr)
|
||||
}
|
||||
hours, err := strconv.Atoi(parts[0])
|
||||
if err != nil || hours < 0 || hours > 23 {
|
||||
return "", fmt.Errorf("invalid hours in time: %s", timeStr)
|
||||
}
|
||||
minutes, err := strconv.Atoi(parts[1])
|
||||
if err != nil || minutes < 0 || minutes > 59 {
|
||||
return "", fmt.Errorf("invalid minutes in time: %s", timeStr)
|
||||
}
|
||||
if hours == 0 && minutes == 0 {
|
||||
return "PT0S", nil
|
||||
}
|
||||
trigger := "PT"
|
||||
if hours > 0 {
|
||||
trigger += fmt.Sprintf("%dH", hours)
|
||||
}
|
||||
if minutes > 0 {
|
||||
trigger += fmt.Sprintf("%dM", minutes)
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user