blob: f9ae146d0b20382d1845436ceb4f77e1e8fe094a [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"
Serge Bazanski717aad42021-07-11 16:03:43 +00009 "strings"
Serge Bazanski8ef457f2021-07-11 14:42:38 +000010 "time"
11 _ "time/tzdata"
12
13 ics "github.com/arran4/golang-ical"
14 "github.com/golang/glog"
15)
16
17const (
Serge Bazanski11b276d2021-07-11 23:49:55 +000018 // EventsURL is the calendar from which we load public Hackerspace events.
19 EventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export"
Serge Bazanski8ef457f2021-07-11 14:42:38 +000020)
21
22// eventsBySooner sorts upcoming events so the one that happens the soonest
23// will be first in the list.
24type eventBySooner []*UpcomingEvent
25
26func (e eventBySooner) Len() int { return len(e) }
27func (e eventBySooner) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
28func (e eventBySooner) Less(i, j int) bool {
29 a, b := e[i], e[j]
30 if a.Start.Time == b.Start.Time {
31 if a.End.Time == b.End.Time {
32 return a.UID < b.UID
33 }
34 return a.End.Time.Before(b.End.Time)
35 }
36 return a.Start.Time.Before(b.Start.Time)
37}
38
39// parseUpcomingEvents generates a list of upcoming events from an open ICS/iCal file.
40func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error) {
41 cal, err := ics.ParseCalendar(data)
42 if err != nil {
43 return nil, fmt.Errorf("ParseCalendar(%q): %w", err)
44 }
45
46 var out []*UpcomingEvent
47 for _, event := range cal.Events() {
48 uidProp := event.GetProperty(ics.ComponentPropertyUniqueId)
49 if uidProp == nil || uidProp.Value == "" {
50 glog.Errorf("Event with no UID, ignoring: %+v", event)
51 continue
52 }
53 uid := uidProp.Value
54
55 summaryProp := event.GetProperty(ics.ComponentPropertySummary)
56 if summaryProp == nil || summaryProp.Value == "" {
57 glog.Errorf("Event %s has no summary, ignoring", uid)
58 }
59 summary := summaryProp.Value
60
Serge Bazanski717aad42021-07-11 16:03:43 +000061 var description string
62 descriptionProp := event.GetProperty(ics.ComponentPropertyDescription)
63 if descriptionProp != nil && descriptionProp.Value != "" {
64 // The ICS/iCal description has escaped newlines. Undo that.
65 description = strings.ReplaceAll(descriptionProp.Value, `\n`, "\n")
66 }
67
Serge Bazanski8ef457f2021-07-11 14:42:38 +000068 status := event.GetProperty(ics.ComponentPropertyStatus)
69 tentative := false
70 if status != nil {
71 if status.Value == string(ics.ObjectStatusCancelled) {
72 // NextCloud only has CONFIRMED, CANCELELD and TENTATIVE for
73 // events. We drop everything CANCELELD and keep things that are
74 // TENTATIVE.
75 continue
76 }
77 if status.Value == string(ics.ObjectStatusTentative) {
78 tentative = true
79 }
80 }
81
82 start, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtStart))
83 if err != nil {
84 glog.Errorf("Event %s has unparseable DTSTART, ignoring: %v", uid, err)
85 continue
86 }
87 end, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtEnd))
88 if err != nil {
89 glog.Errorf("Event %s has unparseable DTEND, ignoring: %v", uid, err)
90 continue
91 }
92
93 if (start.WholeDay && !end.WholeDay) || (!start.WholeDay && end.WholeDay) {
94 glog.Errorf("Event %s has whole-day inconsistencies, start: %s, end: %s, ignoring", uid, start, end)
95 }
96
97 u := &UpcomingEvent{
Serge Bazanski717aad42021-07-11 16:03:43 +000098 UID: uid,
99 Summary: summary,
100 Description: description,
101 Start: start,
102 End: end,
103 Tentative: tentative,
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000104 }
105 if u.Elapsed(now) {
106 continue
107 }
108
109 out = append(out, u)
110 }
111 sort.Sort(eventBySooner(out))
112 return out, nil
113}
114
115// GetUpcomingEvents returns all public Hackerspace events that are upcoming
116// relative to the given time 'now' as per the Warsaw Hackerspace public
117// calender (from owncloud.hackerspace.pl).
118func GetUpcomingEvents(ctx context.Context, now time.Time) ([]*UpcomingEvent, error) {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000119 r, err := http.NewRequestWithContext(ctx, "GET", EventsURL, nil)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000120 if err != nil {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000121 return nil, fmt.Errorf("NewRequest(%q): %w", EventsURL, err)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000122 }
123 res, err := http.DefaultClient.Do(r)
124 if err != nil {
Serge Bazanski11b276d2021-07-11 23:49:55 +0000125 return nil, fmt.Errorf("Do(%q): %w", EventsURL, err)
Serge Bazanski8ef457f2021-07-11 14:42:38 +0000126 }
127 defer res.Body.Close()
128 return parseUpcomingEvents(now, res.Body)
129}