//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 ( "htwkalender/model" "htwkalender/service/functions" clock "htwkalender/service/functions/time" "htwkalender/service/names" "time" "github.com/jordic/goics" _ "time/tzdata" ) // IcalModel local type for EmitICal function type IcalModel struct { Events model.Events Mapping map[string]model.FeedCollection } // EmitICal implements the interface for goics func (icalModel IcalModel) EmitICal() goics.Componenter { internalClock := clock.RealClock{} c := generateIcalEmit(icalModel, internalClock) return c } func generateIcalEmit(icalModel IcalModel, internalClock clock.Clock) *goics.Component { europeTime, _ := time.LoadLocation("Europe/Berlin") c := goics.NewComponent() c.SetType("VCALENDAR") // PRODID is required by the standard c.AddProperty("PRODID", "-//HTWK Kalender//htwkalender.de//DE") c.AddProperty("VERSION", "2.0") c.AddProperty("CALSCALE", "GREGORIAN") c.AddProperty("TZID", "EUROPE/BERLIN") c.AddProperty("X-WR-CALNAME", "HTWK Kalender") c.AddProperty("X-WR-TIMEZONE", "EUROPE/BERLIN") //add v time zone icalModel.vtimezone(c) for _, event := range icalModel.Events { mapEntry, mappingFound := icalModel.Mapping[event.UUID] s := goics.NewComponent() s.SetType("VEVENT") s.AddProperty(goics.FormatDateTime("DTSTAMP", internalClock.Now().Local().In(europeTime))) // create a unique id for the event by hashing the event start, end, course and name var eventHash = functions.HashString(event.Start.String() + event.End.String() + event.Course + event.Name + event.Rooms) s.AddProperty("UID", eventHash+"@htwkalender.de") s.AddProperty(goics.FormatDateTime("DTEND", event.End.Time().Local().In(europeTime))) s.AddProperty(goics.FormatDateTime("DTSTART", event.Start.Time().Local().In(europeTime))) if mappingFound { addPropertyIfNotEmpty(s, "SUMMARY", replaceNameIfUserDefined(&event, mapEntry)) addAlarmIfSpecified(s, event, mapEntry, internalClock) } else { addPropertyIfNotEmpty(s, "SUMMARY", event.Name) } addPropertyIfNotEmpty(s, "DESCRIPTION", generateDescription(event)) addPropertyIfNotEmpty(s, "LOCATION", event.Rooms) c.AddComponent(s) } return c } func (icalModel IcalModel) vtimezone(c *goics.Component) { tz := goics.NewComponent() tz.SetType("VTIMEZONE") tz.AddProperty("TZID", "EUROPE/BERLIN") //add standard time icalModel.standard(tz) //add daylight time icalModel.daylight(tz) c.AddComponent(tz) } func (icalModel IcalModel) standard(tz *goics.Component) { st := NewHtwkalenderComponent() st.SetType("STANDARD") st.AddProperty("DTSTART", "19701025T030000") st.AddProperty("RRULE", "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU") st.AddProperty("TZOFFSETFROM", "+0200") st.AddProperty("TZOFFSETTO", "+0100") st.AddProperty("TZNAME", "CET") tz.AddComponent(st) } // create an override for goics component function Write // to add the RRULE property func (icalModel IcalModel) daylight(tz *goics.Component) { dt := NewHtwkalenderComponent() dt.SetType("DAYLIGHT") dt.AddProperty("DTSTART", "19700329T020000") dt.AddProperty("TZOFFSETFROM", "+0100") dt.AddProperty("TZOFFSETTO", "+0200") dt.AddProperty("TZNAME", "CEST") dt.AddProperty("RRULE", "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU") tz.AddComponent(dt) } // if reminder is specified in the configuration for this event, an alarm will be added to the event func addAlarmIfSpecified(s *goics.Component, event model.Event, mapping model.FeedCollection, clock clock.Clock) { // if event.Start > now // then add alarm if event.Start.Time().Local().After(clock.Now().Local()) && mapping.Reminder { a := goics.NewComponent() a.SetType("VALARM") a.AddProperty("TRIGGER", "-P0DT0H15M0S") a.AddProperty("ACTION", "DISPLAY") a.AddProperty("DESCRIPTION", "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms) s.AddComponent(a) } } // 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 } // AddPropertyIfNotEmpty adds a property to the component if the value is not empty // or contains only whitespaces func addPropertyIfNotEmpty(component *goics.Component, key string, value string) { if !functions.OnlyWhitespace(value) { component.AddProperty(key, value) } } func generateDescription(event model.Event) string { var description string if !functions.OnlyWhitespace(event.Notes) { description += "Notizen: " + event.Notes + "\n" } if !functions.OnlyWhitespace(event.Prof) { description += "Prof: " + event.Prof + "\n" } if !functions.OnlyWhitespace(event.Course) { description += "Gruppe: " + event.Course + "\n" } if !functions.OnlyWhitespace(event.EventType) { description += "Typ: " + event.EventType + event.Compulsory + "\n" } return description }