-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcalendar.go
310 lines (267 loc) · 9.14 KB
/
calendar.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package main
import (
"bytes"
"io"
"log/slog"
"net/http"
"regexp"
"strings"
ics "github.com/arran4/golang-ical"
)
// All structs defined in this file are used to unmarshall yaml configuration and
// provide helper functions that are used to fetch and filter events
// CalendarConfig definition
type CalendarConfig struct {
Name string `yaml:"name"`
PublishName string `yaml:"publish_name"`
Public bool `yaml:"public"`
Token string `yaml:"token"`
TokenFile string `yaml:"token_file"`
FeedURL string `yaml:"feed_url"`
FeedURLFile string `yaml:"feed_url_file"`
Filters []Filter `yaml:"filters"`
}
// Downloads iCal feed from the URL and applies filtering rules
func (calendarConfig CalendarConfig) fetch() ([]byte, error) {
// get the iCal feed
slog.Debug("Fetching iCal feed", "url", calendarConfig.FeedURL)
resp, err := http.Get(calendarConfig.FeedURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
feedData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// parse calendar
cal, err := ics.ParseCalendar(strings.NewReader(string(feedData)))
if err != nil {
return nil, err
}
if calendarConfig.PublishName != "" {
cal.SetName(calendarConfig.PublishName)
}
// process filters
if len(calendarConfig.Filters) > 0 {
slog.Debug("Processing filters", "calendar", calendarConfig.Name)
for _, event := range cal.Events() {
if !calendarConfig.ProcessEvent(event) {
cal.RemoveEvent(event.Id())
}
}
slog.Debug("Filter processing completed", "calendar", calendarConfig.Name)
} else {
slog.Debug("No filters to evaluate", "calendar", calendarConfig.Name)
}
// serialize output
var buf bytes.Buffer
err = cal.SerializeTo(&buf)
if err != nil {
return nil, err
}
// return
return buf.Bytes(), nil
}
// Evaluate the filters for a calendar against a given VEvent and
// perform any transformations directly to the VEvent (pointer)
// This function returns false if an event should be deleted
func (calendarConfig CalendarConfig) ProcessEvent(event *ics.VEvent) bool {
// Get the Summary (the "title" of the event)
// In case we cannot parse the event summary it should get dropped
summary := event.GetProperty(ics.ComponentPropertySummary) // summary only for logging
if summary == nil {
return false
}
// Iterate through the Filter rules
for id, filter := range calendarConfig.Filters {
// Does the filter match the event?
if filter.matchesEvent(*event) {
slog.Debug("Filter match found", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
// The event should get dropped if RemoveEvent is set
if filter.RemoveEvent {
slog.Debug("Event to be removed, no more rules will be processed", "action", "DELETE", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
return false
}
// Apply transformation rules to event
filter.transformEvent(event)
// Check if we should stop processing rules
if filter.Stop {
slog.Debug("Stop option is set, no more rules will be processed", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
return true
}
}
}
// Keep event by default if all Filter rules are processed
slog.Debug("Rule processing complete, event will be kept", "rule_id", nil, "event_summary", summary.Value)
return true
}
// Filter definition
type Filter struct {
Description string `yaml:"description"`
RemoveEvent bool `yaml:"remove"`
Stop bool `yaml:"stop"`
Match EventMatchRules `yaml:"match"`
Transform EventTransformRules `yaml:"transform"`
}
// Returns true if a VEvent matches the Filter conditions
func (filter Filter) matchesEvent(event ics.VEvent) bool {
// If an event property is not defined golang-ical returns a nil pointer
// Get event Summary - only used for debug logging
eventSummary := event.GetProperty(ics.ComponentPropertySummary)
if eventSummary == nil {
slog.Warn("Unable to process event summary. Event will be dropped")
return false // never match if VEvent has no summary
}
// Check Summary filters against VEvent
if filter.Match.Summary.hasConditions() {
if !filter.Match.Summary.matchesString(eventSummary.Value) {
return false
}
}
// Check Description filters against VEvent
if filter.Match.Description.hasConditions() {
eventDescription := event.GetProperty(ics.ComponentPropertyDescription)
var eventDescriptionValue string
if eventDescription == nil {
eventDescriptionValue = ""
} else {
eventDescriptionValue = eventDescription.Value
}
if !filter.Match.Description.matchesString(eventDescriptionValue) {
slog.Debug("Event Description does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
return false // event doesn't match
}
}
// Check Location filters against VEvent
if filter.Match.Location.hasConditions() {
eventLocation := event.GetProperty(ics.ComponentPropertyLocation)
var eventLocationValue string
if eventLocation == nil {
eventLocationValue = ""
} else {
eventLocationValue = eventLocation.Value
}
if !filter.Match.Location.matchesString(eventLocationValue) {
slog.Debug("Event Location does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
return false // event doesn't match
}
}
// Check Url filters against VEvent
if filter.Match.Url.hasConditions() {
eventUrl := event.GetProperty(ics.ComponentPropertyUrl)
var eventUrlValue string
if eventUrl == nil {
eventUrlValue = ""
} else {
eventUrlValue = eventUrl.Value
}
if !filter.Match.Url.matchesString(eventUrlValue) {
slog.Debug("Event URL does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
return false // event doesn't match
}
}
// VEvent must match if we get here
slog.Debug("Event matches filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
return true
}
// Applies filter transformations to a VEvent pointer
func (filter Filter) transformEvent(event *ics.VEvent) {
// Summary transformations
if filter.Transform.Summary.Remove {
event.SetSummary("")
} else if filter.Transform.Summary.Replace != "" {
event.SetSummary(filter.Transform.Summary.Replace)
}
// Description transformations
if filter.Transform.Description.Remove {
event.SetDescription("")
} else if filter.Transform.Description.Replace != "" {
event.SetDescription(filter.Transform.Description.Replace)
}
// Location transformations
if filter.Transform.Location.Remove {
event.SetLocation("")
} else if filter.Transform.Location.Replace != "" {
event.SetLocation(filter.Transform.Location.Replace)
}
// URL transformations
if filter.Transform.Url.Remove {
event.SetURL("")
} else if filter.Transform.Url.Replace != "" {
event.SetURL(filter.Transform.Url.Replace)
}
}
// EventMatchRules contains VEvent properties that user can match against
type EventMatchRules struct {
Summary StringMatchRule `yaml:"summary"`
Description StringMatchRule `yaml:"description"`
Location StringMatchRule `yaml:"location"`
Url StringMatchRule `yaml:"url"`
}
// StringMatchRule defines match rules for VEvent properties with string values
type StringMatchRule struct {
Null bool `yaml:"empty"`
Contains string `yaml:"contains"`
Prefix string `yaml:"prefix"`
Suffix string `yaml:"suffix"`
RegexMatch string `yaml:"regex"`
}
// Returns true if StringMatchRule has any conditions
func (smr StringMatchRule) hasConditions() bool {
return smr.Null ||
smr.Contains != "" ||
smr.Prefix != "" ||
smr.Suffix != "" ||
smr.RegexMatch != ""
}
// Returns true if a given string (data) matches ALL StringMatchRule conditions
func (smr StringMatchRule) matchesString(data string) bool {
// check null if set and don't process further - this condition can only be met on its own
if smr.Null {
return data == ""
}
// check contains if set
if smr.Contains != "" {
if data == "" || !strings.Contains(data, smr.Contains) {
return false
}
}
// check prefix if set
if smr.Prefix != "" {
if data == "" || !strings.HasPrefix(data, smr.Prefix) {
return false
}
}
// check suffix if set
if smr.Suffix != "" {
if data == "" || !strings.HasSuffix(data, smr.Suffix) {
return false
}
}
// check regex match if set
if smr.RegexMatch != "" {
re, err := regexp.Compile(smr.RegexMatch)
if err != nil {
slog.Warn("error processing regex rule", "value", smr.RegexMatch)
return false // regex error is considered a failure to match
}
match := re.MatchString(data)
if !match {
return false // regex didn't match
}
}
return true
}
// EventTransformRules contains VEvent properties that user can modify
type EventTransformRules struct {
Summary StringTransformRule `yaml:"summary"`
Description StringTransformRule `yaml:"description"`
Location StringTransformRule `yaml:"location"`
Url StringTransformRule `yaml:"url"`
}
// StringTransformRule defines changes for VEvent properties with string values
type StringTransformRule struct {
Replace string `yaml:"replace"`
Remove bool `yaml:"remove"`
}