From 8bd876085205731aee3299f9e3060236277d3396 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Sat, 20 Jan 2024 00:24:27 +0000 Subject: [PATCH] Restructured code a bit --- cmd/calendar-bot/calendar-bot.go | 2 +- internal/message/message.go | 2 +- pkg/calendar/calendar.go | 183 +++++++++++++++++-------------- pkg/calendar/calendar_test.go | 30 ++--- 4 files changed, 117 insertions(+), 100 deletions(-) diff --git a/cmd/calendar-bot/calendar-bot.go b/cmd/calendar-bot/calendar-bot.go index b286c66..029be4a 100644 --- a/cmd/calendar-bot/calendar-bot.go +++ b/cmd/calendar-bot/calendar-bot.go @@ -96,7 +96,7 @@ func main() { infoLog.Printf("Scheduling notifications for %s", notifyTime.Format("15:04")) s.Every(1).Day().At(notifyTime).Do(func() { infoLog.Println("Start Notification") - cal, err := calendar.NewCalendar(conf.Calendar, infoLog) + cal, err := calendar.ImportCalendar(conf.Calendar, infoLog) if err != nil { errLog.Printf("Could not read calendar info from %s\n", conf.Calendar) } diff --git a/internal/message/message.go b/internal/message/message.go index 512a944..d95186f 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -29,7 +29,7 @@ type TemplatedMessage struct { txtTemplate *template.Template } -func NewTemplatedMessage(htmlTemplate, txtTemplate string, events []calendar.EventData, tz *time.Location) (TemplatedMessage, error) { +func NewTemplatedMessage(htmlTemplate, txtTemplate string, events []calendar.Event, tz *time.Location) (TemplatedMessage, error) { msg := TemplatedMessage{} for _, evt := range events { event := Event{ diff --git a/pkg/calendar/calendar.go b/pkg/calendar/calendar.go index 813827b..5288ed2 100644 --- a/pkg/calendar/calendar.go +++ b/pkg/calendar/calendar.go @@ -8,17 +8,16 @@ import ( "time" "github.com/emersion/go-ical" - // "github.com/teambition/rrule-go" ) -type Calendar struct { - url string - tz *time.Location - *ical.Calendar - *log.Logger +type IcalData struct { + url string + tz *time.Location + parsed *ical.Calendar + logger *log.Logger } -type EventData struct { +type Event struct { UID string Start time.Time End time.Time @@ -27,146 +26,164 @@ type EventData struct { Description string } -func NewCalendar(url string, l *log.Logger) (Calendar, error) { - calendar := Calendar{url: url, tz: time.Local} +// Obtains an iCal from a given URL +func ImportCalendar(url string, l *log.Logger) (IcalData, error) { + data := IcalData{url: url, tz: time.Local, logger: l} + // Download the ICS file resp, err := http.Get(url) if err != nil { - return calendar, err + return data, err } defer resp.Body.Close() + // Parse the ICS file parser := ical.NewDecoder(resp.Body) - - cal, err := parser.Decode() + parsedData, err := parser.Decode() if err != nil { - return calendar, err + return data, err } - calendar.Calendar = cal - calendar.Logger = l - return calendar, nil + + data.parsed = parsedData + return data, nil } -func (cal Calendar) GetEventsOn(date time.Time) ([]EventData, error) { - events := make([]ical.Event, 0) - todayStart := GetDateWithoutTime(date) +// Assebles a list of all events on a given date +func (data IcalData) GetEventsOn(date time.Time) ([]Event, error) { + events := make([]Event, 0) + todayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) todayEnd := todayStart.Add(24 * time.Hour) - for _, event := range cal.Events() { - start, err := event.DateTimeStart(cal.tz) - if err != nil { - return []EventData{}, err - } - end, err := event.DateTimeEnd(cal.tz) + + for _, e := range data.parsed.Events() { + + start, err := e.DateTimeStart(data.tz) if err != nil { - return []EventData{}, err - } - // regular event - if (start.After(todayStart) || start.Local() == todayStart.Local()) && start.Before(todayEnd) || (start.Before(todayStart) && end.After(todayEnd)) { - events = append(events, event) - continue + return nil, err } - // recurring event - reccurenceSet, err := event.RecurrenceSet(cal.tz) + + end, err := e.DateTimeEnd(data.tz) if err != nil { - cal.Printf("could not get recurrence set: %s\n", err) - continue - } - if reccurenceSet == nil { - // no recurrence - continue + return nil, err } - if GetDateWithoutTime(reccurenceSet.After(todayStart, true)).Local() == GetDateWithoutTime(date).Local() { + + // Checks whether event happens on the given date + if data.isSingleEventOnDate(start, end, todayStart, todayEnd) || + data.isRecurringEventOnDate(e, date) { + + event, err := data.convertIcal(e, date) + if err != nil { + return nil, err + } events = append(events, event) } } - // check for doubles via uid - uids := make(map[string]struct{}) - dedupedEvents := make([]ical.Event, 0) - for _, e := range events { - uid := e.Props.Get(ical.PropUID).Value - if _, ok := uids[uid]; !ok { - dedupedEvents = append(dedupedEvents, e) - uids[uid] = struct{}{} - } + // Post-processing + removeDuplicates(&events) + sortEvents(events) + + return events, nil +} + +// Checks whether the event is a single, standard event on the given date +func (data IcalData) isSingleEventOnDate(start, end, todayStart, todayEnd time.Time) bool { + isRegularEvent := (start.After(todayStart) || start.Equal(todayStart)) && start.Before(todayEnd) + isSpanningEvent := start.Before(todayStart) && end.After(todayEnd) + return isRegularEvent || isSpanningEvent +} + +// Checks whether an recurring event happens on a given date +func (data IcalData) isRecurringEventOnDate(event ical.Event, date time.Time) bool { + recurrenceSet, err := event.RecurrenceSet(data.tz) + if err != nil { + data.logger.Printf("could not get recurrence set: %s\n", err) + return false + } + if recurrenceSet == nil { + return false } - // Convert ical.Events to EventData - eventDatas := make([]EventData, 0) - for _, event := range dedupedEvents { - eventData, err := cal.ConvertToEventData(event, date) - if err != nil { - return nil, err + todayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) + recurrenceDate := time.Date(recurrenceSet.After(todayStart, true).Year(), recurrenceSet.After(todayStart, true).Month(), recurrenceSet.After(todayStart, true).Day(), 0, 0, 0, 0, time.Local) + return recurrenceDate.Equal(todayStart) +} + +// Removes duplicate events from the list +func removeDuplicates(events *[]Event) { + uids := make(map[string]struct{}) + dedupedEvents := make([]Event, 0) + + for _, event := range *events { + uid := event.UID + if _, exists := uids[uid]; !exists { + dedupedEvents = append(dedupedEvents, event) + uids[uid] = struct{}{} } - eventDatas = append(eventDatas, eventData) } + *events = dedupedEvents +} - // sort events - sort.SliceStable(eventDatas, func(i, j int) bool { - start1 := eventDatas[i].Start - start2 := eventDatas[j].Start - return start1.Before(start2) +// Sorts events by start time +func sortEvents(events []Event) { + sort.SliceStable(events, func(i, j int) bool { + return events[i].Start.Before(events[j].Start) }) - - return eventDatas, nil } -func GetDateWithoutTime(date time.Time) time.Time { - return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) -} -func (cal Calendar) ConvertToEventData(icalEvent ical.Event, d time.Time) (EventData, error) { - eventData := EventData{} +// Builds the object that is used to represent an event +func (data IcalData) convertIcal(icalEvent ical.Event, d time.Time) (Event, error) { + event := Event{} // Handle UID uidProp := icalEvent.Props.Get(ical.PropUID) if uidProp != nil { - eventData.UID = uidProp.Value + event.UID = uidProp.Value } else { - return eventData, fmt.Errorf("UID is missing for event %s", icalEvent.Name) + return event, fmt.Errorf("UID is missing for event %s", icalEvent.Name) } // Handle DTSTART startProp := icalEvent.Props.Get(ical.PropDateTimeStart) if startProp == nil { - return eventData, fmt.Errorf("DTSTART is missing for event %s", icalEvent.Name) + return event, fmt.Errorf("DTSTART is missing for event %s", icalEvent.Name) } - eventStart, err := startProp.DateTime(cal.tz) + eventStart, err := startProp.DateTime(data.tz) if err != nil { - return eventData, err + return event, err } - eventData.Start = time.Date(d.Year(), d.Month(), d.Day(), eventStart.Hour(), eventStart.Minute(), 0, 0, d.Location()) + event.Start = time.Date(d.Year(), d.Month(), d.Day(), eventStart.Hour(), eventStart.Minute(), 0, 0, d.Location()) // Handle DTEND endProp := icalEvent.Props.Get(ical.PropDateTimeEnd) if endProp != nil { - eventEnd, err := endProp.DateTime(cal.tz) + eventEnd, err := endProp.DateTime(data.tz) if err != nil { - return eventData, err + return event, err } - // Calculate the difference in days and adjust eventData.End + // Calculate the difference in days and adjust Event.End daysDiff := int(eventEnd.Sub(eventStart).Hours() / 24) - eventData.End = time.Date(d.Year(), d.Month(), d.Day()+daysDiff, eventEnd.Hour(), eventEnd.Minute(), 0, 0, d.Location()) + event.End = time.Date(d.Year(), d.Month(), d.Day()+daysDiff, eventEnd.Hour(), eventEnd.Minute(), 0, 0, d.Location()) } // Handle SUMMARY summaryProp := icalEvent.Props.Get(ical.PropSummary) if summaryProp != nil { - eventData.Summary = summaryProp.Value + event.Summary = summaryProp.Value } // Handle LOCATION locationProp := icalEvent.Props.Get(ical.PropLocation) if locationProp != nil { - eventData.Location = locationProp.Value + event.Location = locationProp.Value } // Handle DESCRIPTION descriptionProp := icalEvent.Props.Get(ical.PropDescription) if descriptionProp != nil { - eventData.Description = descriptionProp.Value + event.Description = descriptionProp.Value } else { - eventData.Description = "" + event.Description = "" } - return eventData, nil + return event, nil } diff --git a/pkg/calendar/calendar_test.go b/pkg/calendar/calendar_test.go index 6bbf567..e7791d1 100644 --- a/pkg/calendar/calendar_test.go +++ b/pkg/calendar/calendar_test.go @@ -14,8 +14,8 @@ const ( xHainDump_1 = "testData.txt" ) -func NewWantedCalendarEvent(uid string, summary string, start time.Time, end time.Time) EventData { - return EventData{ +func NewWantedCalendarEvent(uid string, summary string, start time.Time, end time.Time) Event { + return Event{ UID: uid, Summary: summary, Start: start, @@ -36,7 +36,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { name string testDataFile io.ReadCloser args args - want []EventData + want []Event wantErr bool }{ { @@ -46,7 +46,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 2, 23, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "dae1e4eb-7213-4620-bc9f-1bdb8a023af9", "Workshop - Learn PCB design with KiCad", @@ -62,7 +62,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 3, 2, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "66adcfc4-6827-45a2-a5b4-655923d5dd62", "How to use the latest AIs in your daily workflow - for... everything?", @@ -78,7 +78,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 3, 21, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "5fb7f276-54d6-4c30-a993-92cfe962e41b", "Gespräch unter Bäumen (mit Elisa Filevich)", @@ -94,7 +94,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 2, 24, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "1ec26b84-60e1-437d-a455-db6404dff879", "Drones' night", @@ -110,7 +110,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 2, 27, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "c9158eec-083a-4798-9860-99c4a83cce0f", "offener Montag", @@ -126,7 +126,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 2, 8, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "3591c731-0e27-4902-9ae0-8748d46841f3", "XMPP-Meetup", @@ -148,7 +148,7 @@ func TestCalendar_GetEventsOn(t *testing.T) { date: time.Date(2023, 1, 22, 0, 0, 0, 0, time.Local), }, wantErr: false, - want: []EventData{ + want: []Event{ NewWantedCalendarEvent( "290b69b7-aaaf-47d4-88d1-d42366e36163", "Kindernachmittag", @@ -173,11 +173,11 @@ func TestCalendar_GetEventsOn(t *testing.T) { t.Fatalf("could not open test data file: %s", tt.testDataFile) } - cal := Calendar{ - url: "file", - tz: time.Local, - Logger: log.New(os.Stdout, "[TEST] ", log.Ldate|log.Ltime|log.Lmsgprefix|log.Lshortfile), - Calendar: calendar, + cal := IcalData{ + url: "file", + tz: time.Local, + logger: log.New(os.Stdout, "[TEST] ", log.Ldate|log.Ltime|log.Lmsgprefix|log.Lshortfile), + parsed: calendar, } got, err := cal.GetEventsOn(tt.args.date) if (err != nil) != tt.wantErr {