mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-07-16 09:38:49 +02:00
feat:#49 added new ical lib for description formatting
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
153
services/ical/service/ical/icalDescriptionGenerator.go
Normal file
153
services/ical/service/ical/icalDescriptionGenerator.go
Normal 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},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user