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/load.go b/hswaw/site/calendar/load.go
new file mode 100644
index 0000000..5ea9198
--- /dev/null
+++ b/hswaw/site/calendar/load.go
@@ -0,0 +1,120 @@
+package calendar
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "sort"
+ "time"
+ _ "time/tzdata"
+
+ ics "github.com/arran4/golang-ical"
+ "github.com/golang/glog"
+)
+
+const (
+ // eventsURL is the calendar from which we load public Hackerspace events.
+ eventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export"
+)
+
+// eventsBySooner sorts upcoming events so the one that happens the soonest
+// will be first in the list.
+type eventBySooner []*UpcomingEvent
+
+func (e eventBySooner) Len() int { return len(e) }
+func (e eventBySooner) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
+func (e eventBySooner) Less(i, j int) bool {
+ a, b := e[i], e[j]
+ if a.Start.Time == b.Start.Time {
+ if a.End.Time == b.End.Time {
+ return a.UID < b.UID
+ }
+ return a.End.Time.Before(b.End.Time)
+ }
+ return a.Start.Time.Before(b.Start.Time)
+}
+
+// parseUpcomingEvents generates a list of upcoming events from an open ICS/iCal file.
+func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error) {
+ cal, err := ics.ParseCalendar(data)
+ if err != nil {
+ return nil, fmt.Errorf("ParseCalendar(%q): %w", err)
+ }
+
+ var out []*UpcomingEvent
+ for _, event := range cal.Events() {
+ uidProp := event.GetProperty(ics.ComponentPropertyUniqueId)
+ if uidProp == nil || uidProp.Value == "" {
+ glog.Errorf("Event with no UID, ignoring: %+v", event)
+ continue
+ }
+ uid := uidProp.Value
+
+ summaryProp := event.GetProperty(ics.ComponentPropertySummary)
+ if summaryProp == nil || summaryProp.Value == "" {
+ glog.Errorf("Event %s has no summary, ignoring", uid)
+ }
+ summary := summaryProp.Value
+
+ status := event.GetProperty(ics.ComponentPropertyStatus)
+ tentative := false
+ if status != nil {
+ if status.Value == string(ics.ObjectStatusCancelled) {
+ // NextCloud only has CONFIRMED, CANCELELD and TENTATIVE for
+ // events. We drop everything CANCELELD and keep things that are
+ // TENTATIVE.
+ continue
+ }
+ if status.Value == string(ics.ObjectStatusTentative) {
+ tentative = true
+ }
+ }
+
+ start, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtStart))
+ if err != nil {
+ glog.Errorf("Event %s has unparseable DTSTART, ignoring: %v", uid, err)
+ continue
+ }
+ end, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtEnd))
+ if err != nil {
+ glog.Errorf("Event %s has unparseable DTEND, ignoring: %v", uid, err)
+ continue
+ }
+
+ if (start.WholeDay && !end.WholeDay) || (!start.WholeDay && end.WholeDay) {
+ glog.Errorf("Event %s has whole-day inconsistencies, start: %s, end: %s, ignoring", uid, start, end)
+ }
+
+ u := &UpcomingEvent{
+ UID: uid,
+ Summary: summary,
+ Start: start,
+ End: end,
+ Tentative: tentative,
+ }
+ if u.Elapsed(now) {
+ continue
+ }
+
+ out = append(out, u)
+ }
+ sort.Sort(eventBySooner(out))
+ return out, nil
+}
+
+// GetUpcomingEvents returns all public Hackerspace events that are upcoming
+// relative to the given time 'now' as per the Warsaw Hackerspace public
+// calender (from owncloud.hackerspace.pl).
+func GetUpcomingEvents(ctx context.Context, now time.Time) ([]*UpcomingEvent, error) {
+ r, err := http.NewRequestWithContext(ctx, "GET", eventsURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("NewRequest(%q): %w", eventsURL, err)
+ }
+ res, err := http.DefaultClient.Do(r)
+ if err != nil {
+ return nil, fmt.Errorf("Do(%q): %w", eventsURL, err)
+ }
+ defer res.Body.Close()
+ return parseUpcomingEvents(now, res.Body)
+}