blob: 5b36b9cbb7c455dfc8d3cb45c2e93107c0df49ab [file] [log] [blame]
Serge Bazanski8ef457f2021-07-11 14:42:38 +00001package calendar
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "sort"
9 "time"
10 _ "time/tzdata"
11
12 ics "github.com/arran4/golang-ical"
13 "github.com/golang/glog"
14)
15
16const (
Serge Bazanski11b276d2021-07-11 23:49:55 +000017 // EventsURL is the calendar from which we load public Hackerspace events.
18 EventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export"
Serge Bazanski8ef457f2021-07-11 14:42:38 +000019)
20
21// eventsBySooner sorts upcoming events so the one that happens the soonest
22// will be first in the list.
23type eventBySooner []*UpcomingEvent
24
25func (e eventBySooner) Len() int { return len(e) }
26func (e eventBySooner) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
27func (e eventBySooner) Less(i, j int) bool {
28 a, b := e[i], e[j]
29 if a.Start.Time == b.Start.Time {
30 if a.End.Time == b.End.Time {
31 return a.UID < b.UID
32 }
33 return a.End.Time.Before(b.End.Time)
34 }
35 return a.Start.Time.Before(b.Start.Time)
36}
37
38// parseUpcomingEvents generates a list of upcoming events from an open ICS/iCal file.
39func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error) {
40 cal, err := ics.ParseCalendar(data)
41 if err != nil {
42 return nil, fmt.Errorf("ParseCalendar(%q): %w", err)
43 }
44
45 var out []*UpcomingEvent
46 for _, event := range cal.Events() {
47 uidProp := event.GetProperty(ics.ComponentPropertyUniqueId)
48 if uidProp == nil || uidProp.Value == "" {
49 glog.Errorf("Event with no UID, ignoring: %+v", event)
50 continue
51 }
52 uid := uidProp.Value
53
54 summaryProp := event.GetProperty(ics.ComponentPropertySummary)
55 if summaryProp == nil || summaryProp.Value == "" {
56 glog.Errorf("Event %s has no summary, ignoring", uid)
57 }
58 summary := summaryProp.Value
59
60 status := event.GetProperty(ics.ComponentPropertyStatus)
61 tentative := false
62 if status != nil {
63 if status.Value == string(ics.ObjectStatusCancelled) {
64 // NextCloud only has CONFIRMED, CANCELELD and TENTATIVE for
65 // events. We drop everything CANCELELD and keep things that are
66 // TENTATIVE.
67 continue
68 }
69 if status.Value == string(ics.ObjectStatusTentative) {
70 tentative = true
71 }
72 }
73
74 start, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtStart))
75 if err != nil {
76 glog.Errorf("Event %s has unparseable DTSTART, ignoring: %v", uid, err)
77 continue
78 }
79 end, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtEnd))
80 if err != nil {
81 glog.Errorf("Event %s has unparseable DTEND, ignoring: %v", uid, err)
82 continue
83 }
84
85 if (start.WholeDay && !end.WholeDay) || (!start.WholeDay && end.WholeDay) {
86 glog.Errorf("Event %s has whole-day inconsistencies, start: %s, end: %s, ignoring", uid, start, end)
87 }
88
89 u := &UpcomingEvent{
90 UID: uid,
91 Summary: summary,
92 Start: start,
93 End: end,
94 Tentative: tentative,
95 }
96 if u.Elapsed(now) {
97 continue
98 }
99
100 out = append(out, u)
101 }
102 sort.Sort(eventBySooner(out))
103 return out, nil
104}
105
106// GetUpcomingEvents returns all public Hackerspace events that are upcoming
107// relative to the given time 'now' as per the Warsaw Hackerspace public
108// calender (from owncloud.hackerspace.pl).
109func GetUpcomingEvents(ctx context.Context, now time.Time) ([]*UpcomingEvent, error) {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000110 r, err := http.NewRequestWithContext(ctx, "GET", EventsURL, nil)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000111 if err != nil {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000112 return nil, fmt.Errorf("NewRequest(%q): %w", EventsURL, err)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000113 }
114 res, err := http.DefaultClient.Do(r)
115 if err != nil {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000116 return nil, fmt.Errorf("Do(%q): %w", EventsURL, err)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000117 }
118 defer res.Body.Close()
119 return parseUpcomingEvents(now, res.Body)
120}