From c00990dd8ee690c1777f3aeabe848047d4f40f68 Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Tue, 24 Sep 2024 16:39:23 +0200 Subject: [PATCH] feat:#49 added new ical lib for description formatting --- services/data-manager/Dockerfile | 2 +- services/go.mod | 1 + services/go.sum | 10 + services/ical/Dockerfile | 2 +- services/ical/main.go | 7 +- services/ical/service/ical/ical.go | 9 +- .../service/ical/icalDescriptionGenerator.go | 153 ++++++++++++ .../ical/service/ical/icalFileGeneration.go | 229 +++++++++--------- services/ical/service/routes.go | 8 +- 9 files changed, 299 insertions(+), 122 deletions(-) create mode 100644 services/ical/service/ical/icalDescriptionGenerator.go diff --git a/services/data-manager/Dockerfile b/services/data-manager/Dockerfile index 9e52f5d..0799b2d 100644 --- a/services/data-manager/Dockerfile +++ b/services/data-manager/Dockerfile @@ -50,7 +50,7 @@ EXPOSE 8090 ENTRYPOINT ["./main", "serve"] -FROM golang:1.21.6 AS dev +FROM golang:1.23 AS dev # Set the Current Working Directory inside the container WORKDIR /htwkalender-data-manager diff --git a/services/go.mod b/services/go.mod index 0b5f256..94733a1 100644 --- a/services/go.mod +++ b/services/go.mod @@ -24,6 +24,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/arran4/golang-ical v0.3.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect diff --git a/services/go.sum b/services/go.sum index 66c6218..6e16631 100644 --- a/services/go.sum +++ b/services/go.sum @@ -73,6 +73,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk= +github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -139,6 +141,7 @@ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnTh github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -267,6 +270,8 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -293,6 +298,7 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -330,6 +336,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -491,9 +498,12 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/services/ical/Dockerfile b/services/ical/Dockerfile index 19d231c..446d796 100644 --- a/services/ical/Dockerfile +++ b/services/ical/Dockerfile @@ -49,7 +49,7 @@ EXPOSE 8091 ENTRYPOINT ["./main"] -FROM golang:1.21.6 AS dev +FROM golang:1.23 AS dev # Set the Current Working Directory inside the container WORKDIR /htwkalender-ical diff --git a/services/ical/main.go b/services/ical/main.go index 1769786..0b8b19f 100644 --- a/services/ical/main.go +++ b/services/ical/main.go @@ -62,7 +62,12 @@ func main() { DataManagerURL: "http://" + host + ":8090", } - fiberApp.Use(logger.New()) + fiberApp.Use(logger.New( + logger.Config{ + Format: "[${time}] ${status} - ${latency} ${method} ${path} ${error}\n", + TimeFormat: "02-01-2006 15:04:05", + }, + )) // Add routes to the app instance for the data-manager ical service service.AddFeedRoutes(app) diff --git a/services/ical/service/ical/ical.go b/services/ical/service/ical/ical.go index ba8e7f2..6a331d5 100644 --- a/services/ical/service/ical/ical.go +++ b/services/ical/service/ical/ical.go @@ -17,9 +17,7 @@ package ical import ( - "bytes" "fmt" - "github.com/jordic/goics" "htwkalender/ical/model" "htwkalender/ical/service/connector" htwkalenderGrpc "htwkalender/ical/service/connector/grpc" @@ -31,7 +29,7 @@ const expirationTime = 5 * time.Minute var FeedDeletedError = fmt.Errorf("feed deleted") -func Feed(app model.AppType, token string) (string, error) { +func Feed(app model.AppType, token string, userAgent string) (string, error) { var events model.Events modules := map[string]model.FeedCollection{} @@ -68,9 +66,8 @@ func Feed(app model.AppType, token string) (string, error) { } } - b := bytes.Buffer{} - goics.NewICalEncode(&b).Encode(IcalModel{Events: events, Mapping: modules}) - icalFeed := &model.FeedModel{Content: b.String(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))} + cal := GenerateIcalFeed(events, modules, userAgent) + icalFeed := &model.FeedModel{Content: cal.Serialize(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))} return icalFeed.Content, nil } diff --git a/services/ical/service/ical/icalDescriptionGenerator.go b/services/ical/service/ical/icalDescriptionGenerator.go new file mode 100644 index 0000000..d88559c --- /dev/null +++ b/services/ical/service/ical/icalDescriptionGenerator.go @@ -0,0 +1,153 @@ +//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" + "net/url" + "strings" + _ "time/tzdata" +) + +// Generates user-agent specific description and adds it to the calendar event. +func generateUserAgentSpecificDescription(vEvent *ics.VEvent, event model.Event, userAgent string) { + // Generate description and ALTREP (alternative representation) based on user agent. + description, altrep := generateDescription(event, userAgent) + + if isThunderbird(userAgent) && altrep != "" { + // Thunderbird-specific handling: Add DESCRIPTION with ALTREP attribute. + // create property altrep + altrepParam := WithAltRep(altrep) + + vEvent.AddProperty(ics.ComponentPropertyDescription, description, altrepParam) + AddProperty(ics.ComponentPropertyDescription, description, altrepParam, vEvent) + } else { + // Default handling: Add plain DESCRIPTION property for other user agents. + vEvent.AddProperty("DESCRIPTION", description) + } + +} + +func AddProperty(property ics.ComponentProperty, value string, prop ics.PropertyParameter, cb *ics.VEvent) { + baseProperty := &ics.BaseProperty{ + IANAToken: string(property), + Value: value, + ICalParameters: map[string][]string{}, + } + + customProperty := NewCustomProperty(*baseProperty) + + r := ics.IANAProperty{ + BaseProperty: customProperty, + } + k, v := prop.KeyValue() + r.ICalParameters[k] = v + cb.Properties = append(cb.Properties, r) +} + +// Generates description based on the event details and user agent. +func generateDescription(event model.Event, userAgent string) (string, string) { + plainDescription := buildPlainTextDescription(event) + + if isThunderbird(userAgent) { + // Thunderbird-specific: Generate HTML and ALTREP format for Thunderbird users. + htmlDescription := generateThunderbirdHTMLDescription(event) + altrep := `"data:text/html,` + url.PathEscape(htmlDescription) + `"` + return plainDescription, altrep + } + + // Google Calendar-specific handling. + if isGoogleCalendar(userAgent) { + plainDescription += generateGoogleCalendarDescription(event.Rooms) + } + + // Default: Return plain text description without ALTREP for non-Thunderbird cases. + return plainDescription, "" +} + +// Builds the plain text description from the event details. +func buildPlainTextDescription(event model.Event) string { + var description string + + if !functions.OnlyWhitespace(event.Prof) { + description += "Profs: " + event.Prof + "\n" + } + if !functions.OnlyWhitespace(event.Course) { + description += "Gruppen: " + event.Course + "\n" + } + if !functions.OnlyWhitespace(event.EventType) { + description += "Typ: " + event.EventType + event.Compulsory + "\n" + } + if !functions.OnlyWhitespace(event.Notes) { + description += "Notizen: " + event.Notes + "\n" + } + + return description +} + +// Helper function to generate HTML description for Thunderbird's ALTREP. +func generateThunderbirdHTMLDescription(event model.Event) string { + var htmlDescription strings.Builder + + // HTML-encoded description similar to plain text. + if !functions.OnlyWhitespace(event.Prof) { + htmlDescription.WriteString("Profs: " + event.Prof + "
") + } + if !functions.OnlyWhitespace(event.Course) { + htmlDescription.WriteString("Gruppen: " + event.Course + "
") + } + if !functions.OnlyWhitespace(event.EventType) { + htmlDescription.WriteString("Typ: " + event.EventType + event.Compulsory + "
") + } + + // Add the HTML link to the room map. + htmlDescription.WriteString(`Link: HTWK-Karte`) + + return htmlDescription.String() +} + +// Generates a room description with links for Google Calendar. +func generateGoogleCalendarDescription(rooms string) string { + var description strings.Builder + roomList := strings.Split(rooms, " ") + description.WriteString("Orte: \n ") + + for _, room := range roomList { + description.WriteString("HTWK-Karte\n") + } + + return description.String() +} + +// Checks if the user agent is Thunderbird. +func isThunderbird(userAgent string) bool { + return strings.Contains(userAgent, "Thunderbird") +} + +// Checks if the user agent is Google Calendar. +func isGoogleCalendar(userAgent string) bool { + return strings.Contains(userAgent, "Google-Calendar-Importer") +} + +func WithAltRep(altRepUrl string) ics.PropertyParameter { + return &ics.KeyValues{ + Key: string(ics.ParameterAltrep), + Value: []string{altRepUrl}, + } +} diff --git a/services/ical/service/ical/icalFileGeneration.go b/services/ical/service/ical/icalFileGeneration.go index 9af0c60..fed15df 100644 --- a/services/ical/service/ical/icalFileGeneration.go +++ b/services/ical/service/ical/icalFileGeneration.go @@ -17,121 +17,153 @@ 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" - "github.com/jordic/goics" _ "time/tzdata" ) -// IcalModel local type for EmitICal function -type IcalModel struct { - Events model.Events - Mapping map[string]model.FeedCollection -} +func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollection, userAgent string) *ics.Calendar { -// EmitICal implements the interface for goics -func (icalModel IcalModel) EmitICal() goics.Componenter { - internalClock := clock.RealClock{} - c := generateIcalEmit(icalModel, internalClock) - return c -} + cal := ics.NewCalendarFor("HTWK Kalender") + 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) -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") + internalClock := clock.RealClock{} - 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 events { + mapEntry, mappingFound := mapping[event.UUID] - 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(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms) - s.AddProperty("UID", eventHash+"@htwkalender.de") - s.AddProperty(goics.FormatDateTime("DTEND", time.Time(event.End).Local().In(europeTime))) - s.AddProperty(goics.FormatDateTime("DTSTART", time.Time(event.Start).Local().In(europeTime))) + 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(s, "SUMMARY", replaceNameIfUserDefined(&event, mapEntry)) - addAlarmIfSpecified(s, event, mapEntry, internalClock) + addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry)) + addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock) } else { - addPropertyIfNotEmpty(s, "SUMMARY", event.Name) + addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name) } - addPropertyIfNotEmpty(s, "DESCRIPTION", generateDescription(event)) - addPropertyIfNotEmpty(s, "LOCATION", event.Rooms) - c.AddComponent(s) + generateUserAgentSpecificDescription(icalEvent, event, userAgent) + addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms) + + cal.AddVEvent(icalEvent) + } - return c + return cal } -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) +// 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(s *goics.Component, event model.Event, mapping model.FeedCollection, clock clock.Clock) { +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 { - 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) + 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) } } @@ -144,30 +176,3 @@ func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection) 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.Prof) { - description += "Profs: " + event.Prof + "\n" - } - if !functions.OnlyWhitespace(event.Course) { - description += "Gruppen: " + event.Course + "\n" - } - if !functions.OnlyWhitespace(event.EventType) { - description += "Typ: " + event.EventType + event.Compulsory + "\n" - } - if !functions.OnlyWhitespace(event.Notes) { - description += "Notizen: " + event.Notes + "\n" - } - - return description -} diff --git a/services/ical/service/routes.go b/services/ical/service/routes.go index c92a0f7..09ffc9e 100644 --- a/services/ical/service/routes.go +++ b/services/ical/service/routes.go @@ -34,7 +34,13 @@ func AddFeedRoutes(app model.AppType) { app.Fiber.Get("/api/feed", func(c fiber.Ctx) error { token := c.Query("token") - results, err := ical.Feed(app, token) + + // get request userAgent and check if it is Thunderbird + userAgent := c.Get("User-Agent") + + slog.Info("User-Agent", "userAgent", userAgent) + + results, err := ical.Feed(app, token, userAgent) if errors.Is(err, ical.FeedDeletedError) { return c.SendStatus(fiber.StatusGone)