feat:#49 added new ical lib for description formatting

This commit is contained in:
Elmar Kresse
2024-09-24 16:39:23 +02:00
parent 7b70499171
commit c00990dd8e
9 changed files with 299 additions and 122 deletions

View File

@ -50,7 +50,7 @@ EXPOSE 8090
ENTRYPOINT ["./main", "serve"] ENTRYPOINT ["./main", "serve"]
FROM golang:1.21.6 AS dev FROM golang:1.23 AS dev
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /htwkalender-data-manager WORKDIR /htwkalender-data-manager

View File

@ -24,6 +24,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // 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/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 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect

View File

@ -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/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 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 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-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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 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/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 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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= 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/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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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/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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 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/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.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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -49,7 +49,7 @@ EXPOSE 8091
ENTRYPOINT ["./main"] ENTRYPOINT ["./main"]
FROM golang:1.21.6 AS dev FROM golang:1.23 AS dev
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /htwkalender-ical WORKDIR /htwkalender-ical

View File

@ -62,7 +62,12 @@ func main() {
DataManagerURL: "http://" + host + ":8090", 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 // Add routes to the app instance for the data-manager ical service
service.AddFeedRoutes(app) service.AddFeedRoutes(app)

View File

@ -17,9 +17,7 @@
package ical package ical
import ( import (
"bytes"
"fmt" "fmt"
"github.com/jordic/goics"
"htwkalender/ical/model" "htwkalender/ical/model"
"htwkalender/ical/service/connector" "htwkalender/ical/service/connector"
htwkalenderGrpc "htwkalender/ical/service/connector/grpc" htwkalenderGrpc "htwkalender/ical/service/connector/grpc"
@ -31,7 +29,7 @@ const expirationTime = 5 * time.Minute
var FeedDeletedError = fmt.Errorf("feed deleted") 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 var events model.Events
modules := map[string]model.FeedCollection{} modules := map[string]model.FeedCollection{}
@ -68,9 +66,8 @@ func Feed(app model.AppType, token string) (string, error) {
} }
} }
b := bytes.Buffer{} cal := GenerateIcalFeed(events, modules, userAgent)
goics.NewICalEncode(&b).Encode(IcalModel{Events: events, Mapping: modules}) icalFeed := &model.FeedModel{Content: cal.Serialize(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))}
icalFeed := &model.FeedModel{Content: b.String(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))}
return icalFeed.Content, nil return icalFeed.Content, nil
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
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 + "<br>")
}
if !functions.OnlyWhitespace(event.Course) {
htmlDescription.WriteString("Gruppen: " + event.Course + "<br>")
}
if !functions.OnlyWhitespace(event.EventType) {
htmlDescription.WriteString("Typ: " + event.EventType + event.Compulsory + "<br>")
}
// Add the HTML link to the room map.
htmlDescription.WriteString(`Link: <a href="https://map.htwk-leipzig.de/room/` + event.Rooms + `">HTWK-Karte</a>`)
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("<a href=\"https://map.htwk-leipzig.de/room/" + room + "\">HTWK-Karte</a>\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},
}
}

View File

@ -17,121 +17,153 @@
package ical package ical
import ( import (
ics "github.com/arran4/golang-ical"
"htwkalender/ical/model" "htwkalender/ical/model"
"htwkalender/ical/service/functions" "htwkalender/ical/service/functions"
clock "htwkalender/ical/service/functions/time" clock "htwkalender/ical/service/functions/time"
"htwkalender/ical/service/names" "htwkalender/ical/service/names"
"time" "time"
"github.com/jordic/goics"
_ "time/tzdata" _ "time/tzdata"
) )
// IcalModel local type for EmitICal function func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollection, userAgent string) *ics.Calendar {
type IcalModel struct {
Events model.Events
Mapping map[string]model.FeedCollection
}
// EmitICal implements the interface for goics cal := ics.NewCalendarFor("HTWK Kalender")
func (icalModel IcalModel) EmitICal() goics.Componenter { cal.SetMethod(ics.MethodPublish)
internalClock := clock.RealClock{} cal.SetProductId("-//HTWK Kalender//htwkalender.de//DE")
c := generateIcalEmit(icalModel, internalClock) cal.SetTzid("Europe/Berlin")
return c 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") europeTime, _ := time.LoadLocation("Europe/Berlin")
c := goics.NewComponent() internalClock := clock.RealClock{}
c.SetType("VCALENDAR")
// PRODID is required by the standard
c.AddProperty("PRODID", "-//HTWK Kalender//htwkalender.de//DE")
c.AddProperty("VERSION", "2.0") for _, event := range events {
c.AddProperty("CALSCALE", "GREGORIAN") mapEntry, mappingFound := mapping[event.UUID]
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(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms) 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") icalEvent := ics.NewEvent(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.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 { if mappingFound {
addPropertyIfNotEmpty(s, "SUMMARY", replaceNameIfUserDefined(&event, mapEntry)) addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry))
addAlarmIfSpecified(s, event, mapEntry, internalClock) addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock)
} else { } else {
addPropertyIfNotEmpty(s, "SUMMARY", event.Name) addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name)
} }
addPropertyIfNotEmpty(s, "DESCRIPTION", generateDescription(event)) generateUserAgentSpecificDescription(icalEvent, event, userAgent)
addPropertyIfNotEmpty(s, "LOCATION", event.Rooms) addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms)
c.AddComponent(s)
cal.AddVEvent(icalEvent)
} }
return c return cal
} }
func (icalModel IcalModel) vtimezone(c *goics.Component) { // AddPropertyIfNotEmpty adds a property to the component if the value is not empty
tz := goics.NewComponent() // or contains only whitespaces
tz.SetType("VTIMEZONE") func addPropertyIfNotEmpty(component *ics.VEvent, key ics.ComponentProperty, value string) {
tz.AddProperty("TZID", "EUROPE/BERLIN") if !functions.OnlyWhitespace(value) {
//add standard time component.AddProperty(key, value)
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 // 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 // if event.Start > now
// then add alarm // then add alarm
if time.Time(event.Start).Local().After(clock.Now().Local()) && mapping.Reminder { if time.Time(event.Start).Local().After(clock.Now().Local()) && mapping.Reminder {
a := goics.NewComponent() alarm := icalEvent.AddAlarm()
a.SetType("VALARM") alarm.AddProperty(ics.ComponentPropertyTrigger, "-P0DT0H15M0S")
a.AddProperty("TRIGGER", "-P0DT0H15M0S") alarm.AddProperty(ics.ComponentPropertyAction, "DISPLAY")
a.AddProperty("ACTION", "DISPLAY") alarm.AddProperty(ics.ComponentPropertyDescription, "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms)
a.AddProperty("DESCRIPTION", "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms)
s.AddComponent(a)
} }
} }
@ -144,30 +176,3 @@ func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection)
return event.Name 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
}

View File

@ -34,7 +34,13 @@ func AddFeedRoutes(app model.AppType) {
app.Fiber.Get("/api/feed", func(c fiber.Ctx) error { app.Fiber.Get("/api/feed", func(c fiber.Ctx) error {
token := c.Query("token") 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) { if errors.Is(err, ical.FeedDeletedError) {
return c.SendStatus(fiber.StatusGone) return c.SendStatus(fiber.StatusGone)