//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format. //Copyright (C) 2024 HTWKalender support@htwkalender.de //This program is free software: you can redistribute it and/or modify //it under the terms of the GNU Affero General Public License as published by //the Free Software Foundation, either version 3 of the License, or //(at your option) any later version. //This program is distributed in the hope that it will be useful, //but WITHOUT ANY WARRANTY; without even the implied warranty of //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //GNU Affero General Public License for more details. //You should have received a copy of the GNU Affero General Public License //along with this program. If not, see . package ical import ( ics "github.com/arran4/golang-ical" "htwkalender/ical/model" "htwkalender/ical/service/functions" clock "htwkalender/ical/service/functions/time" "htwkalender/ical/service/names" "time" _ "time/tzdata" ) func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollection, userAgent string) *ics.Calendar { cal := ics.NewCalendarFor("HTWK Kalender") setDefaultIcalParams(cal) europeTime, _ := time.LoadLocation("Europe/Berlin") internalClock := clock.RealClock{} for _, event := range events { mapEntry, mappingFound := mapping[event.UUID] var eventHash = functions.HashString(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms) icalEvent := ics.NewEvent(eventHash + "@htwkalender.de") icalEvent.SetDtStampTime(internalClock.Now().Local().In(europeTime)) icalEvent.SetStartAt(time.Time(event.Start).Local().In(europeTime)) icalEvent.SetEndAt(time.Time(event.End).Local().In(europeTime)) if mappingFound { addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry)) addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock) } else { addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name) } addUserAgentSpecificDescription(icalEvent, event, userAgent) addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms) cal.AddVEvent(icalEvent) } return cal } func setDefaultIcalParams(cal *ics.Calendar) { cal.SetMethod(ics.MethodPublish) cal.SetProductId("-//HTWK Kalender//htwkalender.de//DE") cal.SetTzid("Europe/Berlin") cal.SetXWRCalName("HTWK Kalender") cal.SetXWRTimezone("Europe/Berlin") cal.SetVersion("2.0") cal.SetCalscale("GREGORIAN") vTimeZone := ics.NewTimezone("Europe/Berlin") vTimeZone.Components = []ics.Component{ &ics.Daylight{ ComponentBase: ics.ComponentBase{ Properties: []ics.IANAProperty{ { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyDtstart), Value: "19700329T020000", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyRrule), Value: "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzname), Value: "CEST", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzoffsetfrom), Value: "+0100", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzoffsetto), Value: "+0200", }, }, }, }, }, &ics.Standard{ ComponentBase: ics.ComponentBase{ Properties: []ics.IANAProperty{ { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyDtstart), Value: "19701025T030000", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyRrule), Value: "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzname), Value: "CET", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzoffsetfrom), Value: "+0200", }, }, { BaseProperty: ics.BaseProperty{ IANAToken: string(ics.PropertyTzoffsetto), Value: "+0100", }, }, }, }, }, } cal.AddVTimezone(vTimeZone) } // AddPropertyIfNotEmpty adds a property to the component if the value is not empty // or contains only whitespaces func addPropertyIfNotEmpty(component *ics.VEvent, key ics.ComponentProperty, value string) { if !functions.OnlyWhitespace(value) { component.AddProperty(key, value) } } // if reminder is specified in the configuration for this event, an alarm will be added to the event func addAlarmIfSpecified(icalEvent *ics.VEvent, event model.Event, mapping model.FeedCollection, clock clock.Clock) { // if event.Start > now // then add alarm if time.Time(event.Start).Local().After(clock.Now().Local()) && mapping.Reminder { alarm := icalEvent.AddAlarm() alarm.AddProperty(ics.ComponentPropertyTrigger, "-P0DT0H15M0S") alarm.AddProperty(ics.ComponentPropertyAction, "DISPLAY") alarm.AddProperty(ics.ComponentPropertyDescription, "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms) } } // replaceNameIfUserDefined replaces the name of the event with the user defined name if it is not empty // all contained template strings will be replaced with the corresponding values from the event func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection) string { if !functions.OnlyWhitespace(mapping.UserDefinedName) { return names.ReplaceTemplateSubStrings(mapping.UserDefinedName, *event) } return event.Name }