hswaw/site: add calendar/event library
This will let us populate upcoming events server-side on the website (or
serve this data in a format that can be more easily consumed by JS).
Change-Id: I0f6b5bf9831f4d07acebb4eb77a7d88b63fe8e46
diff --git a/hswaw/site/calendar/time.go b/hswaw/site/calendar/time.go
new file mode 100644
index 0000000..f742a67
--- /dev/null
+++ b/hswaw/site/calendar/time.go
@@ -0,0 +1,73 @@
+package calendar
+
+import (
+ "fmt"
+ "time"
+
+ ics "github.com/arran4/golang-ical"
+)
+
+// EventTime is a timestamp for calendar events. It either represents a real
+// point-in time or a calender day, if it's a whole-day event.
+type EventTime struct {
+ // Time is a timestamp in the timezone originally defined for this event if
+ // WholeDay is true. Otherwise, it's a UTC time from which a year, month
+ // and day can be extracted and treated as the indication of a 'calendar
+ // day' in an unknown timezone.
+ Time time.Time
+ // WholeDay is true if this EventTime represents an entire calendar day.
+ WholeDay bool
+}
+
+func (e *EventTime) String() string {
+ if e.WholeDay {
+ return fmt.Sprintf("%s (whole day)", e.Time.Format("2006/01/02"))
+ } else {
+ return e.Time.String()
+ }
+}
+
+// parseICSTime attempts to parse a given ICS DT{START,END} object into an
+// EventTime, trying to figure out if the given object represents a timestamp
+// or a whole-day event.
+func parseICSTime(p *ics.IANAProperty) (*EventTime, error) {
+ // If this is has a VALUE of DATE, then this is a whole-day time.
+ // Otherwise, it's an actual timestamp.
+ valueList, ok := p.ICalParameters[string(ics.ParameterValue)]
+ if ok {
+ if len(valueList) != 1 || valueList[0] != "DATE" {
+ return nil, fmt.Errorf("unsupported time type: %v", valueList)
+ }
+ ts, err := time.Parse("20060102", p.Value)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse date %q: %w", p.Value, err)
+ }
+ return &EventTime{
+ Time: ts,
+ WholeDay: true,
+ }, nil
+ }
+ // You would expect that nextcloud would emit VALUE == DATE-TIME for
+ // timestamps, but that just doesn't seem to be the case. Maye I should
+ // read the ICS standard...
+
+ tzidList, ok := p.ICalParameters[string(ics.ParameterTzid)]
+ if !ok || len(tzidList) != 1 {
+ return nil, fmt.Errorf("TZID missing")
+ }
+ tzid := tzidList[0]
+ location, err := time.LoadLocation(tzid)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse TZID %q: %w", tzid, err)
+ }
+
+ ts, err := time.ParseInLocation("20060102T150405", p.Value, location)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse time %q: %w", p.Value, err)
+ }
+
+ return &EventTime{
+ Time: ts,
+ WholeDay: false,
+ }, nil
+}