mirror of
https://github.com/mgrove36/ical-filter-proxy.git
synced 2026-03-03 01:47:07 +00:00
initial commit
This commit is contained in:
259
calendar.go
Normal file
259
calendar.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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"`
|
||||
Token string `yaml:"token"`
|
||||
FeedURL string `yaml:"feed_url"`
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if eventDescription == nil {
|
||||
slog.Debug("Event has no Description so cannot not match filter", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // if VEvent has no description it cannot match filter
|
||||
}
|
||||
if !filter.Match.Description.matchesString(eventDescription.Value) {
|
||||
slog.Debug("Event Description does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // event doesn't match
|
||||
}
|
||||
}
|
||||
|
||||
// Check Description filters against VEvent
|
||||
if filter.Match.Location.hasConditions() {
|
||||
eventLocation := event.GetProperty(ics.ComponentPropertyLocation)
|
||||
if eventLocation == nil {
|
||||
slog.Warn("Event has no Location so cannot match filter", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // if VEvent has no location it cannot match filter
|
||||
}
|
||||
if !filter.Match.Location.matchesString(eventLocation.Value) {
|
||||
slog.Debug("Event Location 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)
|
||||
}
|
||||
}
|
||||
|
||||
// EventMatchRules contains VEvent properties that user can match against
|
||||
type EventMatchRules struct {
|
||||
Summary StringMatchRule `yaml:"summary"`
|
||||
Description StringMatchRule `yaml:"description"`
|
||||
Location StringMatchRule `yaml:"location"`
|
||||
}
|
||||
|
||||
// StringMatchRule defines match rules for VEvent properties with string values
|
||||
type StringMatchRule struct {
|
||||
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.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 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 != "" {
|
||||
match, err := regexp.MatchString(data, smr.RegexMatch)
|
||||
if err != nil {
|
||||
slog.Warn("error processing regex rule", "value", smr.RegexMatch)
|
||||
return false // regex error is considered a failure to match
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
// StringTransformRule defines changes for VEvent properties with string values
|
||||
type StringTransformRule struct {
|
||||
Replace string `yaml:"replace"`
|
||||
Remove bool `yaml:"remove"`
|
||||
}
|
||||
Reference in New Issue
Block a user