package calendar import ( "context" "fmt" "io" "net/http" "sort" "strings" "time" _ "time/tzdata" ics "github.com/arran4/golang-ical" "github.com/golang/glog" rrule "github.com/teambition/rrule-go" ) 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 var description string descriptionProp := event.GetProperty(ics.ComponentPropertyDescription) if descriptionProp != nil && descriptionProp.Value != "" { // The ICS/iCal description has escaped newlines. Undo that. description = strings.ReplaceAll(descriptionProp.Value, `\n`, "\n") } 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) } rruleS := event.GetProperty(ics.ComponentPropertyRrule) if rruleS != nil { rrule, err := rrule.StrToRRule(rruleS.Value) if err != nil { glog.Errorf("Event %s has unparseable RRULE, ignoring: %v", uid, err) continue } rrule.DTStart(start.Time) duration := end.Time.Sub(start.Time) if start.WholeDay { duration = time.Hour * 24 } next := rrule.After(now, true) if next.IsZero() { continue } u := &UpcomingEvent{ UID: uid, Summary: summary, Description: description, Start: &EventTime{ Time: next, WholeDay: start.WholeDay, }, End: &EventTime{ Time: next.Add(duration), WholeDay: start.WholeDay, }, Tentative: tentative, } out = append(out, u) continue } u := &UpcomingEvent{ UID: uid, Summary: summary, Description: description, 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) }