|
1 | 1 | package calendar
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "encoding/json" |
4 | 5 | "fmt"
|
| 6 | + "io" |
5 | 7 | "log"
|
6 | 8 | "net/http"
|
| 9 | + "net/url" |
7 | 10 | "sort"
|
| 11 | + "strings" |
8 | 12 | "time"
|
9 | 13 |
|
10 |
| - "github.com/emersion/go-ical" |
| 14 | + jcal "github.com/xHain-hackspace/go-jcal" |
11 | 15 | )
|
12 | 16 |
|
13 |
| -type IcalData struct { |
14 |
| - url string |
15 |
| - tz *time.Location |
16 |
| - parsed *ical.Calendar |
17 |
| - logger *log.Logger |
18 |
| -} |
19 |
| - |
20 | 17 | type Event struct {
|
21 |
| - UID string |
22 | 18 | Start time.Time
|
23 | 19 | End time.Time
|
24 | 20 | Summary string
|
25 |
| - Location string |
26 | 21 | Description string
|
27 | 22 | }
|
28 | 23 |
|
29 |
| -// Obtains an iCal from a given URL |
30 |
| -func ImportCalendar(url string, l *log.Logger) (IcalData, error) { |
31 |
| - data := IcalData{url: url, tz: time.Local, logger: l} |
32 |
| - |
33 |
| - // Download the ICS file |
34 |
| - resp, err := http.Get(url) |
35 |
| - if err != nil { |
36 |
| - return data, err |
37 |
| - } |
38 |
| - defer resp.Body.Close() |
39 |
| - |
40 |
| - // Parse the ICS file |
41 |
| - parser := ical.NewDecoder(resp.Body) |
42 |
| - parsedData, err := parser.Decode() |
43 |
| - if err != nil { |
44 |
| - return data, err |
45 |
| - } |
46 |
| - |
47 |
| - data.parsed = parsedData |
48 |
| - return data, nil |
| 24 | +type NextcloudCalendar struct { |
| 25 | + BaseUrl url.URL |
49 | 26 | }
|
50 | 27 |
|
51 |
| -// Assembles a list of events on a given date |
52 |
| -func (data IcalData) GetEventsOn(date time.Time) ([]Event, error) { |
53 |
| - |
54 |
| - allEvents := make(map[string]ical.Event) |
55 |
| - selectedEvents := make([]Event, 0) |
| 28 | +// NewNextcloudCalendar creates a new nextcloud calendar instance |
| 29 | +// |
| 30 | +// Parameters: |
| 31 | +// |
| 32 | +// baseUrlStr - the base url e.g. https://files.x-hain.de/remote.php/dav/public-calendars/Yi63cicwgDnjaBHR |
| 33 | +func NewNextcloudCalendar(baseUrlStr string) (NextcloudCalendar, error) { |
| 34 | + calendar := NextcloudCalendar{} |
56 | 35 |
|
57 |
| - // Prepare map of all events |
58 |
| - for _, event := range data.parsed.Events() { |
59 |
| - |
60 |
| - uid := event.Props.Get(ical.PropUID).Value |
61 |
| - allEvents[uid] = data.handleDuplicates(uid, event, allEvents) |
62 |
| - |
63 |
| - } |
| 36 | + baseUrlStr = strings.TrimSpace(baseUrlStr) |
64 | 37 |
|
65 |
| - for _, event := range allEvents { |
66 |
| - |
67 |
| - // Checks whether event happens on the given date |
68 |
| - if data.isSingleEventOnDate(event, date) || |
69 |
| - data.isRecurringEventOnDate(event, date) { |
70 |
| - |
71 |
| - convertedEvent, err := data.convertIcal(event, date) |
72 |
| - if err != nil { |
73 |
| - return nil, err |
74 |
| - } |
75 |
| - selectedEvents = append(selectedEvents, convertedEvent) |
76 |
| - } |
| 38 | + baseUrl, err := url.Parse(baseUrlStr) |
| 39 | + if err != nil { |
| 40 | + return calendar, err |
77 | 41 | }
|
| 42 | + calendar.BaseUrl = *baseUrl |
78 | 43 |
|
79 |
| - sortEvents(selectedEvents) |
80 |
| - |
81 |
| - return selectedEvents, nil |
| 44 | + return calendar, nil |
82 | 45 | }
|
83 | 46 |
|
84 |
| -// Checks whether the event is a single, standard event on the given date |
85 |
| -func (data IcalData) isSingleEventOnDate(event ical.Event, date time.Time) bool { |
86 |
| - |
87 |
| - start, err := event.DateTimeStart(data.tz) |
| 47 | +// Assembles a list of events on a given date |
| 48 | +func (c NextcloudCalendar) GetEventsOn(date time.Time) ([]Event, error) { |
| 49 | + events := make([]Event, 0) |
| 50 | + |
| 51 | + // build url params |
| 52 | + params := url.Values{} |
| 53 | + params.Add("accept", "jcal") |
| 54 | + params.Add("expand", "1") |
| 55 | + params.Add("export", "") |
| 56 | + |
| 57 | + // Get the first and last date of the day |
| 58 | + dayBegin := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local) |
| 59 | + dayEnd := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, time.Local) |
| 60 | + params.Add("start", fmt.Sprintf("%d", dayBegin.Unix())) |
| 61 | + params.Add("end", fmt.Sprintf("%d", dayEnd.Unix())) |
| 62 | + |
| 63 | + // add params to url |
| 64 | + requestUrl := c.BaseUrl |
| 65 | + requestUrl.RawQuery = params.Encode() |
| 66 | + |
| 67 | + // make request |
| 68 | + resp, err := http.Get(requestUrl.String()) |
88 | 69 | if err != nil {
|
89 |
| - data.logger.Printf("could not get start time: %s\n", err) |
90 |
| - return false |
| 70 | + return events, err |
91 | 71 | }
|
92 | 72 |
|
93 |
| - end, err := event.DateTimeEnd(data.tz) |
| 73 | + // read data |
| 74 | + respData, err := io.ReadAll(resp.Body) |
94 | 75 | if err != nil {
|
95 |
| - data.logger.Printf("could not get end time: %s\n", err) |
96 |
| - return false |
| 76 | + return events, err |
97 | 77 | }
|
| 78 | + resp.Body.Close() |
98 | 79 |
|
99 |
| - todayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, data.tz) |
100 |
| - todayEnd := todayStart.Add(24 * time.Hour) |
101 |
| - |
102 |
| - isRegularEvent := (start.After(todayStart) || start.Equal(todayStart)) && start.Before(todayEnd) |
103 |
| - isSpanningEvent := start.Before(todayStart) && end.After(todayEnd) |
104 |
| - |
105 |
| - return isRegularEvent || isSpanningEvent |
106 |
| -} |
107 |
| - |
108 |
| -// Checks whether an recurring event happens on a given date |
109 |
| -func (data IcalData) isRecurringEventOnDate(event ical.Event, date time.Time) bool { |
110 |
| - recurrenceSet, err := event.RecurrenceSet(data.tz) |
111 |
| - if err != nil { |
112 |
| - data.logger.Printf("could not get recurrence set: %s\n", err) |
113 |
| - return false |
114 |
| - } |
115 |
| - if recurrenceSet == nil { |
116 |
| - return false |
| 80 | + // parse jcal |
| 81 | + var jcalObj jcal.JCalObject |
| 82 | + if err := json.Unmarshal(respData, &jcalObj); err != nil { |
| 83 | + log.Fatalf("Error parsing jCal JSON: %v", err) |
117 | 84 | }
|
118 | 85 |
|
119 |
| - todayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, data.tz) |
120 |
| - |
121 |
| - // Obtain the next recurrence date after todayStart. |
122 |
| - nextRecurrence := recurrenceSet.After(todayStart, true) |
123 |
| - |
124 |
| - // Use components of nextRecurrence to create a new date with a specific time set to midnight. |
125 |
| - recurrenceDate := time.Date(nextRecurrence.Year(), nextRecurrence.Month(), nextRecurrence.Day(), 0, 0, 0, 0, data.tz) |
126 |
| - |
127 |
| - return recurrenceDate.Equal(todayStart) |
128 |
| -} |
129 |
| - |
130 |
| -// Check for a duplicate and returns the right event |
131 |
| -func (data IcalData) handleDuplicates(uid string, event ical.Event, allEvents map[string]ical.Event) ical.Event { |
132 |
| - |
133 |
| - newCandidate := event |
134 |
| - |
135 |
| - if _, ok := allEvents[uid]; ok { |
136 |
| - |
137 |
| - existingCandidate := allEvents[uid] |
138 |
| - |
139 |
| - existingCandidateCreatedProp := allEvents[uid].Props.Get(ical.PropCreated) |
140 |
| - if existingCandidateCreatedProp == nil { |
141 |
| - return existingCandidate |
142 |
| - } |
143 |
| - existingCandidateCreatedTime, err := existingCandidateCreatedProp.DateTime(data.tz) |
144 |
| - if err != nil { |
145 |
| - return existingCandidate |
146 |
| - } |
147 |
| - |
148 |
| - newCandidateCreatedProp := event.Props.Get(ical.PropCreated) |
149 |
| - if newCandidateCreatedProp == nil { |
150 |
| - return existingCandidate |
151 |
| - } |
152 |
| - |
153 |
| - newCandidateCreatedTime, err := newCandidateCreatedProp.DateTime(data.tz) |
154 |
| - if err != nil { |
155 |
| - return existingCandidate |
156 |
| - } |
157 |
| - |
158 |
| - // If there is a duplicate select the one that was created later |
159 |
| - if existingCandidateCreatedTime.After(newCandidateCreatedTime) { |
160 |
| - return existingCandidate |
161 |
| - } |
| 86 | + // convert from jcal |
| 87 | + for _, jEvent := range jcalObj.Events { |
| 88 | + events = append(events, fromJcal(jEvent)) |
162 | 89 | }
|
163 | 90 |
|
164 |
| - return newCandidate |
165 |
| -} |
166 |
| - |
167 |
| -// Sorts events by start time |
168 |
| -func sortEvents(events []Event) { |
| 91 | + // sort events by start time |
169 | 92 | sort.SliceStable(events, func(i, j int) bool {
|
170 | 93 | return events[i].Start.Before(events[j].Start)
|
171 | 94 | })
|
| 95 | + return events, nil |
172 | 96 | }
|
173 | 97 |
|
174 |
| -// Builds the object that is used to represent an event |
175 |
| -func (data IcalData) convertIcal(icalEvent ical.Event, date time.Time) (Event, error) { |
176 |
| - event := Event{} |
177 |
| - |
178 |
| - // Handle UID |
179 |
| - uidProp := icalEvent.Props.Get(ical.PropUID) |
180 |
| - if uidProp != nil { |
181 |
| - event.UID = uidProp.Value |
182 |
| - } else { |
183 |
| - return event, fmt.Errorf("UID is missing for event %s", icalEvent.Name) |
184 |
| - } |
185 |
| - |
186 |
| - // Handle DTSTART |
187 |
| - startProp := icalEvent.Props.Get(ical.PropDateTimeStart) |
188 |
| - if startProp == nil { |
189 |
| - return event, fmt.Errorf("DTSTART is missing for event %s", icalEvent.Name) |
| 98 | +func fromJcal(jEvent jcal.Event) Event { |
| 99 | + return Event{ |
| 100 | + Summary: jEvent.Summary, |
| 101 | + Description: jEvent.Description, |
| 102 | + Start: jEvent.DtStart, |
| 103 | + End: jEvent.DtEnd, |
190 | 104 | }
|
191 |
| - eventStart, err := startProp.DateTime(data.tz) |
192 |
| - if err != nil { |
193 |
| - return event, err |
194 |
| - } |
195 |
| - event.Start = time.Date(date.Year(), date.Month(), date.Day(), eventStart.Hour(), eventStart.Minute(), 0, 0, date.Location()) |
196 |
| - |
197 |
| - // Handle DTEND |
198 |
| - endProp := icalEvent.Props.Get(ical.PropDateTimeEnd) |
199 |
| - if endProp != nil { |
200 |
| - eventEnd, err := endProp.DateTime(data.tz) |
201 |
| - if err != nil { |
202 |
| - return event, err |
203 |
| - } |
204 |
| - // Calculate the difference in days and adjust Event.End |
205 |
| - daysDiff := int(eventEnd.Sub(eventStart).Hours() / 24) |
206 |
| - event.End = time.Date(date.Year(), date.Month(), date.Day()+daysDiff, eventEnd.Hour(), eventEnd.Minute(), 0, 0, date.Location()) |
207 |
| - } |
208 |
| - |
209 |
| - // Handle SUMMARY |
210 |
| - summaryProp := icalEvent.Props.Get(ical.PropSummary) |
211 |
| - if summaryProp != nil { |
212 |
| - event.Summary = summaryProp.Value |
213 |
| - } |
214 |
| - |
215 |
| - // Handle LOCATION |
216 |
| - locationProp := icalEvent.Props.Get(ical.PropLocation) |
217 |
| - if locationProp != nil { |
218 |
| - event.Location = locationProp.Value |
219 |
| - } |
220 |
| - |
221 |
| - // Handle DESCRIPTION |
222 |
| - descriptionProp := icalEvent.Props.Get(ical.PropDescription) |
223 |
| - if descriptionProp != nil { |
224 |
| - event.Description = descriptionProp.Value |
225 |
| - } else { |
226 |
| - event.Description = "" |
227 |
| - } |
228 |
| - |
229 |
| - return event, nil |
230 | 105 | }
|
0 commit comments