diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 586cb73..e0a74d8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -35,33 +35,60 @@ lint-frontend:
- npm i
- npm run lint-no-fix
-lint-backend:
+lint-data-manager:
stage: lint
image: golangci/golangci-lint:latest
rules:
- changes:
- - backend/**/*
+ - services/data-manager/**/*
script:
- - cd backend
+ - cd services/data-manager
+ - go mod download
+ - golangci-lint --version
+ - golangci-lint run -v --skip-dirs=migrations --timeout=5m
+
+lint-ical:
+ stage: lint
+ image: golangci/golangci-lint:latest
+ rules:
+ - changes:
+ - services/ical/**/*
+ script:
+ - cd services/ical
- go mod download
- golangci-lint --version
- golangci-lint run -v --skip-dirs=migrations --timeout=5m
-build-backend:
+build-data-manager:
image: golang:alpine
stage: build
rules:
- changes:
- - backend/**/*
+ - services/data-manager/**/*
script:
- - cd backend
+ - cd services/data-manager
- go build -o htwkalender
artifacts:
paths:
- - backend/htwkalender
- - backend/go.sum
- - backend/go.mod
+ - data-manager/htwkalender
+ - data-manager/go.sum
+ - data-manager/go.mod
+
+build-ical:
+ image: golang:alpine
+ stage: build
+ rules:
+ - changes:
+ - services/ical/**/*
+ script:
+ - cd services/ical
+ - go build -o htwkalender-ical
+ artifacts:
+ paths:
+ - data-manager/htwkalender-ical
+ - data-manager/go.sum
+ - data-manager/go.mod
sonarqube-check-backend:
stage: sonarqube-check
@@ -99,17 +126,29 @@ build-frontend:
paths:
- frontend/build
-test-backend:
+test-data-manager:
image: golang:alpine
stage: test
rules:
- changes:
- - backend/**/*
+ - services/data-manager/**/*
script:
- - cd backend
+ - cd services/data-manager
- go test -v ./...
dependencies:
- - build-backend
+ - build-data-manager
+
+test-ical:
+ image: golang:alpine
+ stage: test
+ rules:
+ - changes:
+ - services/ical/**/*
+ script:
+ - cd services/ical
+ - go test -v ./...
+ dependencies:
+ - build-ical
test-frontend:
image: node:lts
@@ -124,7 +163,7 @@ test-frontend:
dependencies:
- lint-frontend
-build-backend-image:
+build-data-manager-image:
stage: oci-build
image: docker:latest
services:
@@ -132,7 +171,7 @@ build-backend-image:
tags:
- image
variables:
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-backend
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-data-manager
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
@@ -140,12 +179,35 @@ build-backend-image:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- - docker build --pull -t $IMAGE_TAG -f ./backend/Dockerfile --target prod ./backend
+ - docker build --pull -t $IMAGE_TAG -f ./services/data-manager/Dockerfile --target prod ./services/data-manager
- docker push $IMAGE_TAG
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "development"
changes:
- - backend/**/*
+ - services/data-manager/**/*
+
+build-ical-image:
+ stage: oci-build
+ image: docker:latest
+ services:
+ - docker:dind
+ tags:
+ - image
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-ical
+ DOCKER_HOST: tcp://docker:2376
+ DOCKER_TLS_CERTDIR: "/certs"
+ DOCKER_TLS_VERIFY: 1
+ DOCKER_CERT_PATH: "/certs/client"
+ before_script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ script:
+ - docker build --pull -t $IMAGE_TAG -f ./services/ical/Dockerfile --target prod ./services/ical
+ - docker push $IMAGE_TAG
+ rules:
+ - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "development"
+ changes:
+ - services/ical/**/*
build-frontend-image:
stage: oci-build
diff --git a/backend/backend.iml b/backend/backend.iml
deleted file mode 100644
index 49df094..0000000
--- a/backend/backend.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/service/addCalDavRoutes.go b/backend/service/addCalDavRoutes.go
deleted file mode 100644
index 8f91f50..0000000
--- a/backend/service/addCalDavRoutes.go
+++ /dev/null
@@ -1,198 +0,0 @@
-//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 service
-
-import (
- "github.com/labstack/echo/v5"
- "github.com/pocketbase/pocketbase"
- "github.com/pocketbase/pocketbase/apis"
- "github.com/pocketbase/pocketbase/core"
- "htwkalender/service/db"
- "htwkalender/service/ical"
- "io"
- "net/http"
-)
-
-func addFeedRoutes(app *pocketbase.PocketBase) {
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: http.MethodPost,
- Path: "/api/createFeed",
- Handler: func(c echo.Context) error {
- requestBody, _ := io.ReadAll(c.Request().Body)
- result, err := ical.CreateIndividualFeed(requestBody, app)
- if err != nil {
- return c.JSON(http.StatusInternalServerError, "Failed to create individual feed")
- }
- return c.JSON(http.StatusOK, result)
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: http.MethodGet,
- Path: "/api/feed",
- Handler: func(c echo.Context) error {
- token := c.QueryParam("token")
- result, err := ical.Feed(app, token)
- if err != nil {
- return c.JSON(http.StatusInternalServerError, "Failed to get feed")
- }
-
- c.Response().Header().Set("Content-type", "text/calendar")
- c.Response().Header().Set("charset", "utf-8")
- c.Response().Header().Set("Content-Disposition", "inline")
- c.Response().Header().Set("filename", "calendar.ics")
-
- _, err = c.Response().Write([]byte(result))
-
- if err != nil {
- return err
- }
- return nil
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: http.MethodDelete,
- Path: "/api/feed",
- Handler: func(c echo.Context) error {
- token := c.QueryParam("token")
- err := db.DeleteFeed(app.Dao(), token)
- if err != nil {
- return c.JSON(http.StatusNotFound, err)
- } else {
- return c.JSON(http.StatusOK, "Feed deleted")
- }
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: http.MethodPut,
- Path: "/api/feed",
- Handler: func(c echo.Context) error {
- token := c.QueryParam("token")
- return c.JSON(http.StatusOK, "token: "+token)
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- // Route for Thunderbird to get calendar server information
- // Response is a 200 OK without additional content
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: "PROPFIND",
- Path: "/",
- Handler: func(c echo.Context) error {
- return c.JSON(http.StatusOK, "")
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- // Route for Thunderbird to get calendar server information
- // Response is a 200 OK without additional content
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: "PROPFIND",
- Path: "/.well-known/caldav",
- Handler: func(c echo.Context) error {
- return c.JSON(http.StatusOK, "")
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: "PROPFIND",
- Path: "/api/feed",
- Handler: func(c echo.Context) error {
- return c.JSON(http.StatusOK, "")
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-
- app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
- _, err := e.Router.AddRoute(echo.Route{
- Method: http.MethodHead,
- Path: "/api/feed",
- Handler: func(c echo.Context) error {
- return c.JSON(http.StatusOK, "")
- },
- Middlewares: []echo.MiddlewareFunc{
- apis.ActivityLogger(app),
- },
- })
- if err != nil {
- return err
- }
- return nil
- })
-}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index c22cc99..26fdc84 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -15,13 +15,20 @@
#along with this program. If not, see .
services:
- htwkalender-backend:
- image: DOCKER_REGISTRY_REPO-backend # DOCKER_REGISTRY_REPO will be replaced by CI
- command: "--http=0.0.0.0:8090 --dir=/htwkalender/data/pb_data"
+ htwkalender-data-manager:
+ image: DOCKER_REGISTRY_REPO-data-manager # DOCKER_REGISTRY_REPO will be replaced by CI
+ command: "--http=0.0.0.0:8090 --dir=/htwkalender-data-manager/data/pb_data"
pull_policy: always
restart: always
volumes:
- - pb_data:/htwkalender/data
+ - pb_data:/htwkalender-data-manager/data
+ networks:
+ - "net"
+
+ htwkalender-ical:
+ image: DOCKER_REGISTRY_REPO-ical # DOCKER_REGISTRY_REPO will be replaced by CI
+ pull_policy: always
+ restart: always
networks:
- "net"
@@ -30,7 +37,8 @@ services:
pull_policy: always
restart: always
depends_on:
- - htwkalender-backend
+ - htwkalender-data-manager
+ - htwkalender-ical
networks:
- "net"
@@ -42,8 +50,9 @@ services:
- ./dev_htwkalender_de.pem:/opt/bitnami/nginx/conf/dev_htwkalender_de.pem
- ./dev_htwkalender_de.key.pem:/opt/bitnami/nginx/conf/dev_htwkalender_de.key.pem
depends_on:
- - htwkalender-backend
+ - htwkalender-data-manager
- htwkalender-frontend
+ - htwkalender-ical
ports:
- "443:443"
- "80:80"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index f33790e..8358f31 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -15,13 +15,20 @@
#along with this program. If not, see .
services:
- htwkalender-backend:
- image: DOCKER_REGISTRY_REPO-backend # DOCKER_REGISTRY_REPO will be replaced by CI
- command: "--http=0.0.0.0:8090 --dir=/htwkalender/data/pb_data"
+ htwkalender-data-manager:
+ image: DOCKER_REGISTRY_REPO-data-manager # DOCKER_REGISTRY_REPO will be replaced by CI
+ command: "--http=0.0.0.0:8090 --dir=/htwkalender-data-manager/data/pb_data"
pull_policy: always
restart: always
volumes:
- - pb_data:/htwkalender/data
+ - pb_data:/htwkalender-data-manager/data
+ networks:
+ - "net"
+
+ htwkalender-ical:
+ image: DOCKER_REGISTRY_REPO-ical # DOCKER_REGISTRY_REPO will be replaced by CI
+ pull_policy: always
+ restart: always
networks:
- "net"
@@ -30,7 +37,7 @@ services:
pull_policy: always
restart: always
depends_on:
- - htwkalender-backend
+ - htwkalender-data-manager
networks:
- "net"
@@ -44,7 +51,7 @@ services:
- ./cal.htwk-leipzig.de.pem:/opt/bitnami/nginx/conf/cal.htwk-leipzig.de.pem
- ./cal.htwk-leipzig.de.key.pem:/opt/bitnami/nginx/conf/cal.htwk-leipzig.de.key.pem
depends_on:
- - htwkalender-backend
+ - htwkalender-data-manager
- htwkalender-frontend
ports:
- "443:443"
diff --git a/docker-compose.yml b/docker-compose.yml
index 9eb3608..c6a28f6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,17 +15,24 @@
#along with this program. If not, see .
services:
- htwkalender-backend:
+ htwkalender-data-manager:
build:
dockerfile: Dockerfile
- context: ./backend
+ context: services/data-manager
target: dev # prod
- command: "--http=0.0.0.0:8090 --dir=/htwkalender/data/pb_data"
- ports:
- - "8090:8090"
+ command: "--http=0.0.0.0:8090 --dir=/htwkalender-data-manager/data/pb_data"
+ #ports:
+ # - "8090:8090"
volumes:
- - pb_data:/htwkalender/data # for production with volume
- # - ./backend:/htwkalender/data # for development with bind mount from project directory
+ - pb_data:/htwkalender-data-manager/data # for production with volume
+ # - ./data-manager:/htwkalender/data # for development with bind mount from project directory
+
+ htwkalender-ical:
+ build:
+ dockerfile: Dockerfile
+ context: services/ical
+ target: dev # prod
+
htwkalender-frontend:
build:
@@ -44,7 +51,7 @@ services:
volumes:
- ./reverseproxy.local.conf:/opt/bitnami/nginx/conf/nginx.conf
depends_on:
- - htwkalender-backend
+ - htwkalender-data-manager
- htwkalender-frontend
ports:
- "80:80"
diff --git a/frontend/src/api/createFeed.ts b/frontend/src/api/createFeed.ts
index 7b8fcc4..81a897c 100644
--- a/frontend/src/api/createFeed.ts
+++ b/frontend/src/api/createFeed.ts
@@ -18,7 +18,7 @@ import { Module } from "../model/module.ts";
export async function createIndividualFeed(modules: Module[]): Promise {
try {
- const response = await fetch("/api/createFeed", {
+ const response = await fetch("/api/feed", {
method: "POST",
headers: {
"Content-Type": "application/json",
diff --git a/frontend/src/api/fetchModule.ts b/frontend/src/api/fetchModule.ts
index f6cfb27..0238933 100644
--- a/frontend/src/api/fetchModule.ts
+++ b/frontend/src/api/fetchModule.ts
@@ -17,7 +17,7 @@
import { Module } from "../model/module";
export async function fetchModule(module: Module): Promise {
- // request to the backend on /api/module with query parameters name as the module name
+ // request to the data-manager on /api/module with query parameters name as the module name
const request = new Request("/api/module?uuid=" + module.uuid);
return await fetch(request)
diff --git a/frontend/src/api/requestFreeRooms.ts b/frontend/src/api/requestFreeRooms.ts
index 4a33d62..5d467ec 100644
--- a/frontend/src/api/requestFreeRooms.ts
+++ b/frontend/src/api/requestFreeRooms.ts
@@ -14,7 +14,7 @@
//You should have received a copy of the GNU Affero General Public License
//along with this program. If not, see .
-// load free rooms as a list of strings form the backend
+// load free rooms as a list of strings form the data-manager
export async function requestFreeRooms(
from: string,
to: string,
diff --git a/reverseproxy.conf b/reverseproxy.conf
index 7ced37c..c01c32c 100644
--- a/reverseproxy.conf
+++ b/reverseproxy.conf
@@ -119,13 +119,14 @@ http {
server_name cal.htwk-leipzig.de;
location /api/feed {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-ical:8091;
client_max_body_size 2m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
- limit_req zone=feed burst=10 nodelay;
+ limit_req zone=$limit_zone burst=10 nodelay;
+ limit_req_status 429;
}
location / {
@@ -140,13 +141,19 @@ http {
server_name htwkalender.de;
location /api/feed {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-ical:8091;
client_max_body_size 2m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
+
+ # Apply rate limiting
+ if ($request_method = POST) {
+ limit_req zone=createFeed burst=10 nodelay;
+ }
limit_req zone=feed burst=10 nodelay;
+ limit_req_status 429;
}
location / {
@@ -174,8 +181,19 @@ http {
ssl_certificate cal.htwk-leipzig.de.pem;
ssl_certificate_key cal.htwk-leipzig.de.key.pem;
+ location /api/feed {
+ proxy_pass http://htwkalender-ical:8091;
+ client_max_body_size 2m;
+ proxy_connect_timeout 600s;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ send_timeout 600s;
+ limit_req zone=$limit_zone burst=10 nodelay;
+ limit_req_status 429;
+ }
+
location /api {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -185,7 +203,7 @@ http {
# Cache only specific URI
location /api/modules {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -203,7 +221,7 @@ http {
}
location /api/events/types {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -221,7 +239,7 @@ http {
}
location /api/rooms {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -239,7 +257,7 @@ http {
}
location /api/schedule {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -257,7 +275,7 @@ http {
}
location /api/courses {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -274,30 +292,8 @@ http {
limit_req zone=modules burst=5 nodelay;
}
- location /api/feed {
- proxy_pass http://htwkalender-backend:8090;
- client_max_body_size 2m;
- proxy_connect_timeout 600s;
- proxy_read_timeout 600s;
- proxy_send_timeout 600s;
- send_timeout 600s;
- limit_req zone=feed burst=10 nodelay;
- }
-
- location /api/createFeed {
- limit_req zone=createFeed nodelay;
- # return limit request error
- limit_req_status 429;
- proxy_pass http://htwkalender-backend:8090;
- client_max_body_size 2m;
- proxy_connect_timeout 600s;
- proxy_read_timeout 600s;
- proxy_send_timeout 600s;
- send_timeout 600s;
- }
-
location /_ {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
# if user is not 0 in admin list, return 404
if ($admin) {
return 404 "Not Found";
diff --git a/reverseproxy.dev.conf b/reverseproxy.dev.conf
index 7a99919..7a6bf6b 100644
--- a/reverseproxy.dev.conf
+++ b/reverseproxy.dev.conf
@@ -108,9 +108,21 @@ http {
1 $binary_remote_addr;
}
+ # Different rate limits for different request methods
+ map $request_method $limit_feed {
+ POST ''; # Create feed is limited to 1 request per minute
+ default $binary_remote_addr; # All other requests are limited to 20 requests per minute
+ }
+
+
+ map $request_method $limit_createFeed {
+ POST $binary_remote_addr; # Create feed is limited to 1 request per minute
+ default ''; # All other requests are limited to 20 requests per minute
+ }
+
# Limit the number of requests per IP
- limit_req_zone $limit_key zone=feed:20m rate=20r/m;
- limit_req_zone $limit_key zone=createFeed:10m rate=1r/m;
+ limit_req_zone $limit_feed zone=feed:20m rate=20r/m;
+ limit_req_zone $limit_createFeed zone=createFeed:10m rate=1r/m;
limit_req_zone $limit_key zone=modules:10m rate=30r/m;
server {
@@ -130,17 +142,19 @@ http {
ssl_certificate_key dev_htwkalender_de.key.pem;
location /api/feed {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-ical:8091;
client_max_body_size 2m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
limit_req zone=feed burst=10 nodelay;
+ limit_req zone=createFeed burst=10 nodelay;
+ limit_req_status 429;
}
location /api {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -150,7 +164,7 @@ http {
# Cache only specific URI
location /api/modules {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -168,7 +182,7 @@ http {
}
location /api/rooms {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -186,7 +200,7 @@ http {
}
location /api/schedule {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -204,7 +218,7 @@ http {
}
location /api/courses {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -221,20 +235,8 @@ http {
limit_req zone=modules burst=5 nodelay;
}
- location /api/createFeed {
- limit_req zone=createFeed nodelay;
- # return limit request error
- limit_req_status 429;
- proxy_pass http://htwkalender-backend:8090;
- client_max_body_size 2m;
- proxy_connect_timeout 600s;
- proxy_read_timeout 600s;
- proxy_send_timeout 600s;
- send_timeout 600s;
- }
-
location /_ {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
# if user is not 0 in admin list, return 404
if ($admin) {
return 404 "Not Found";
diff --git a/reverseproxy.local.conf b/reverseproxy.local.conf
index 9f41876..16c5eba 100644
--- a/reverseproxy.local.conf
+++ b/reverseproxy.local.conf
@@ -56,8 +56,17 @@ http {
listen [::]:80;
http2 on;
+ location /api/feed {
+ proxy_pass http://htwkalender-ical:8091;
+ client_max_body_size 20m;
+ proxy_connect_timeout 600s;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ send_timeout 600s;
+ }
+
location /api {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
@@ -66,7 +75,7 @@ http {
}
location /_ {
- proxy_pass http://htwkalender-backend:8090;
+ proxy_pass http://htwkalender-data-manager:8090;
# Increase upload file size
client_max_body_size 100m;
}
diff --git a/backend/.gitignore b/services/data-manager/.gitignore
similarity index 100%
rename from backend/.gitignore
rename to services/data-manager/.gitignore
diff --git a/backend/Dockerfile b/services/data-manager/Dockerfile
similarity index 82%
rename from backend/Dockerfile
rename to services/data-manager/Dockerfile
index 931dbeb..35dd5c8 100644
--- a/backend/Dockerfile
+++ b/services/data-manager/Dockerfile
@@ -24,12 +24,12 @@ COPY . ./
# download needed modules
RUN apk add --no-cache --update go gcc g++ && \
go mod download && \
- CGO_ENABLED=1 GOOS=linux go build -o /htwkalender
+ CGO_ENABLED=1 GOOS=linux go build -o /htwkalender-data-manager
# production stage
FROM alpine:latest AS prod
-WORKDIR /htwkalender
+WORKDIR /htwkalender-data-manager
ARG USER=ical
RUN adduser -Ds /bin/sh $USER && \
@@ -39,18 +39,18 @@ USER $USER
RUN mkdir -p data
# copies executable from build container
-COPY --chown=$USER:$USER --from=build /htwkalender ./
+COPY --chown=$USER:$USER --from=build /htwkalender-data-manager ./
# Expose port 8090 to the outside world
EXPOSE 8090
-ENTRYPOINT ["./htwkalender", "serve"]
+ENTRYPOINT ["./htwkalender-data-manager", "serve"]
FROM golang:1.21.6 AS dev
# Set the Current Working Directory inside the container
-WORKDIR /htwkalender
+WORKDIR /htwkalender-data-manager
# Copy go mod and sum files
COPY go.mod go.sum ./
@@ -61,10 +61,10 @@ COPY *.go ./
COPY . .
# Build the Go app
-RUN CGO_ENABLED=1 GOOS=linux go build -o /htwkalender
+RUN CGO_ENABLED=1 GOOS=linux go build -o /htwkalender-data-manager
# Expose port 8090 to the outside world
EXPOSE 8090
# Entry point
-ENTRYPOINT ["./htwkalender", "serve"]
\ No newline at end of file
+ENTRYPOINT ["./htwkalender-data-manager", "serve"]
\ No newline at end of file
diff --git a/backend/LICENSE b/services/data-manager/LICENSE
similarity index 100%
rename from backend/LICENSE
rename to services/data-manager/LICENSE
diff --git a/backend/README.md b/services/data-manager/README.md
similarity index 100%
rename from backend/README.md
rename to services/data-manager/README.md
diff --git a/backend/go.mod b/services/data-manager/go.mod
similarity index 100%
rename from backend/go.mod
rename to services/data-manager/go.mod
diff --git a/backend/go.sum b/services/data-manager/go.sum
similarity index 100%
rename from backend/go.sum
rename to services/data-manager/go.sum
diff --git a/backend/main.go b/services/data-manager/main.go
similarity index 100%
rename from backend/main.go
rename to services/data-manager/main.go
diff --git a/backend/migrations/1695150679_collections_snapshot.go b/services/data-manager/migrations/1695150679_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1695150679_collections_snapshot.go
rename to services/data-manager/migrations/1695150679_collections_snapshot.go
diff --git a/backend/migrations/1695151278_add_admin_account.go b/services/data-manager/migrations/1695151278_add_admin_account.go
similarity index 100%
rename from backend/migrations/1695151278_add_admin_account.go
rename to services/data-manager/migrations/1695151278_add_admin_account.go
diff --git a/backend/migrations/1697532023_collections_snapshot.go b/services/data-manager/migrations/1697532023_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1697532023_collections_snapshot.go
rename to services/data-manager/migrations/1697532023_collections_snapshot.go
diff --git a/backend/migrations/1697570688_collections_snapshot.go b/services/data-manager/migrations/1697570688_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1697570688_collections_snapshot.go
rename to services/data-manager/migrations/1697570688_collections_snapshot.go
diff --git a/backend/migrations/1698017941_updated_events.go b/services/data-manager/migrations/1698017941_updated_events.go
similarity index 100%
rename from backend/migrations/1698017941_updated_events.go
rename to services/data-manager/migrations/1698017941_updated_events.go
diff --git a/backend/migrations/1698770845_updated_events.go b/services/data-manager/migrations/1698770845_updated_events.go
similarity index 100%
rename from backend/migrations/1698770845_updated_events.go
rename to services/data-manager/migrations/1698770845_updated_events.go
diff --git a/backend/migrations/1698770863_updated_events.go b/services/data-manager/migrations/1698770863_updated_events.go
similarity index 100%
rename from backend/migrations/1698770863_updated_events.go
rename to services/data-manager/migrations/1698770863_updated_events.go
diff --git a/backend/migrations/1698770891_collections_snapshot.go b/services/data-manager/migrations/1698770891_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1698770891_collections_snapshot.go
rename to services/data-manager/migrations/1698770891_collections_snapshot.go
diff --git a/backend/migrations/1700512738_updated_feeds.go b/services/data-manager/migrations/1700512738_updated_feeds.go
similarity index 100%
rename from backend/migrations/1700512738_updated_feeds.go
rename to services/data-manager/migrations/1700512738_updated_feeds.go
diff --git a/backend/migrations/1700512916_collections_snapshot.go b/services/data-manager/migrations/1700512916_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1700512916_collections_snapshot.go
rename to services/data-manager/migrations/1700512916_collections_snapshot.go
diff --git a/backend/migrations/1706827339_collections_snapshot.go b/services/data-manager/migrations/1706827339_collections_snapshot.go
similarity index 100%
rename from backend/migrations/1706827339_collections_snapshot.go
rename to services/data-manager/migrations/1706827339_collections_snapshot.go
diff --git a/backend/migrations/1706827586_updated_groups.go b/services/data-manager/migrations/1706827586_updated_groups.go
similarity index 100%
rename from backend/migrations/1706827586_updated_groups.go
rename to services/data-manager/migrations/1706827586_updated_groups.go
diff --git a/backend/model/eventModel.go b/services/data-manager/model/eventModel.go
similarity index 100%
rename from backend/model/eventModel.go
rename to services/data-manager/model/eventModel.go
diff --git a/backend/model/eventModel_test.go b/services/data-manager/model/eventModel_test.go
similarity index 100%
rename from backend/model/eventModel_test.go
rename to services/data-manager/model/eventModel_test.go
diff --git a/backend/model/feedModel.go b/services/data-manager/model/feedModel.go
similarity index 100%
rename from backend/model/feedModel.go
rename to services/data-manager/model/feedModel.go
diff --git a/backend/model/feedModel_test.go b/services/data-manager/model/feedModel_test.go
similarity index 100%
rename from backend/model/feedModel_test.go
rename to services/data-manager/model/feedModel_test.go
diff --git a/backend/model/icalModel.go b/services/data-manager/model/icalModel.go
similarity index 100%
rename from backend/model/icalModel.go
rename to services/data-manager/model/icalModel.go
diff --git a/backend/model/moduleModel.go b/services/data-manager/model/moduleModel.go
similarity index 100%
rename from backend/model/moduleModel.go
rename to services/data-manager/model/moduleModel.go
diff --git a/backend/model/moduleModel_test.go b/services/data-manager/model/moduleModel_test.go
similarity index 100%
rename from backend/model/moduleModel_test.go
rename to services/data-manager/model/moduleModel_test.go
diff --git a/backend/model/seminarGroup.go b/services/data-manager/model/seminarGroup.go
similarity index 100%
rename from backend/model/seminarGroup.go
rename to services/data-manager/model/seminarGroup.go
diff --git a/backend/model/seminarGroupXMLStruct.go b/services/data-manager/model/seminarGroupXMLStruct.go
similarity index 100%
rename from backend/model/seminarGroupXMLStruct.go
rename to services/data-manager/model/seminarGroupXMLStruct.go
diff --git a/backend/model/sportFetcherModel.go b/services/data-manager/model/sportFetcherModel.go
similarity index 100%
rename from backend/model/sportFetcherModel.go
rename to services/data-manager/model/sportFetcherModel.go
diff --git a/backend/openapi.yml b/services/data-manager/openapi.yml
similarity index 100%
rename from backend/openapi.yml
rename to services/data-manager/openapi.yml
diff --git a/backend/pb_schema.json b/services/data-manager/pb_schema.json
similarity index 100%
rename from backend/pb_schema.json
rename to services/data-manager/pb_schema.json
diff --git a/services/data-manager/service/addCalDavRoutes.go b/services/data-manager/service/addCalDavRoutes.go
new file mode 100644
index 0000000..9aae82f
--- /dev/null
+++ b/services/data-manager/service/addCalDavRoutes.go
@@ -0,0 +1,78 @@
+//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 service
+
+import (
+ "github.com/labstack/echo/v5"
+ "github.com/pocketbase/pocketbase"
+ "github.com/pocketbase/pocketbase/apis"
+ "github.com/pocketbase/pocketbase/core"
+ "htwkalender/service/db"
+ "htwkalender/service/ical"
+ "io"
+ "log/slog"
+ "net/http"
+)
+
+func addFeedRoutes(app *pocketbase.PocketBase) {
+ app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
+ _, err := e.Router.AddRoute(echo.Route{
+ Method: http.MethodPost,
+ Path: "/api/feed",
+ Handler: func(c echo.Context) error {
+ requestBody, _ := io.ReadAll(c.Request().Body)
+ result, err := ical.CreateIndividualFeed(requestBody, app)
+ if err != nil {
+ slog.Error("Failed to create individual feed", "error", err)
+ return c.JSON(http.StatusInternalServerError, "Failed to create individual feed")
+ }
+ return c.JSON(http.StatusOK, result)
+ },
+ Middlewares: []echo.MiddlewareFunc{
+ apis.ActivityLogger(app),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+
+ app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
+ _, err := e.Router.AddRoute(echo.Route{
+ Method: http.MethodDelete,
+ Path: "/api/feed",
+ Handler: func(c echo.Context) error {
+ token := c.QueryParam("token")
+ err := db.DeleteFeed(app.Dao(), token)
+ if err != nil {
+ return c.JSON(http.StatusNotFound, err)
+ } else {
+ return c.JSON(http.StatusOK, "Feed deleted")
+ }
+ },
+ Middlewares: []echo.MiddlewareFunc{
+ apis.ActivityLogger(app),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+
+}
diff --git a/backend/service/addRoute.go b/services/data-manager/service/addRoute.go
similarity index 100%
rename from backend/service/addRoute.go
rename to services/data-manager/service/addRoute.go
diff --git a/backend/service/addSchedule.go b/services/data-manager/service/addSchedule.go
similarity index 100%
rename from backend/service/addSchedule.go
rename to services/data-manager/service/addSchedule.go
diff --git a/backend/service/course/courseFunctions.go b/services/data-manager/service/course/courseFunctions.go
similarity index 100%
rename from backend/service/course/courseFunctions.go
rename to services/data-manager/service/course/courseFunctions.go
diff --git a/backend/service/date/dateFormat.go b/services/data-manager/service/date/dateFormat.go
similarity index 100%
rename from backend/service/date/dateFormat.go
rename to services/data-manager/service/date/dateFormat.go
diff --git a/backend/service/date/dateFormat_test.go b/services/data-manager/service/date/dateFormat_test.go
similarity index 100%
rename from backend/service/date/dateFormat_test.go
rename to services/data-manager/service/date/dateFormat_test.go
diff --git a/backend/service/db/dbEvents.go b/services/data-manager/service/db/dbEvents.go
similarity index 100%
rename from backend/service/db/dbEvents.go
rename to services/data-manager/service/db/dbEvents.go
diff --git a/backend/service/db/dbEvents_test.go b/services/data-manager/service/db/dbEvents_test.go
similarity index 100%
rename from backend/service/db/dbEvents_test.go
rename to services/data-manager/service/db/dbEvents_test.go
diff --git a/backend/service/db/dbFeeds.go b/services/data-manager/service/db/dbFeeds.go
similarity index 100%
rename from backend/service/db/dbFeeds.go
rename to services/data-manager/service/db/dbFeeds.go
diff --git a/backend/service/db/dbFeeds_test.go b/services/data-manager/service/db/dbFeeds_test.go
similarity index 100%
rename from backend/service/db/dbFeeds_test.go
rename to services/data-manager/service/db/dbFeeds_test.go
diff --git a/backend/service/db/dbFunctions.go b/services/data-manager/service/db/dbFunctions.go
similarity index 100%
rename from backend/service/db/dbFunctions.go
rename to services/data-manager/service/db/dbFunctions.go
diff --git a/backend/service/db/dbGroups.go b/services/data-manager/service/db/dbGroups.go
similarity index 100%
rename from backend/service/db/dbGroups.go
rename to services/data-manager/service/db/dbGroups.go
diff --git a/backend/service/db/dbRooms.go b/services/data-manager/service/db/dbRooms.go
similarity index 100%
rename from backend/service/db/dbRooms.go
rename to services/data-manager/service/db/dbRooms.go
diff --git a/backend/service/db/mockData/data.db b/services/data-manager/service/db/mockData/data.db
similarity index 100%
rename from backend/service/db/mockData/data.db
rename to services/data-manager/service/db/mockData/data.db
diff --git a/backend/service/db/mockData/logs.db b/services/data-manager/service/db/mockData/logs.db
similarity index 100%
rename from backend/service/db/mockData/logs.db
rename to services/data-manager/service/db/mockData/logs.db
diff --git a/backend/service/events/courseService.go b/services/data-manager/service/events/courseService.go
similarity index 100%
rename from backend/service/events/courseService.go
rename to services/data-manager/service/events/courseService.go
diff --git a/backend/service/events/courseService_test.go b/services/data-manager/service/events/courseService_test.go
similarity index 100%
rename from backend/service/events/courseService_test.go
rename to services/data-manager/service/events/courseService_test.go
diff --git a/backend/service/events/eventService.go b/services/data-manager/service/events/eventService.go
similarity index 100%
rename from backend/service/events/eventService.go
rename to services/data-manager/service/events/eventService.go
diff --git a/backend/service/events/eventService_test.go b/services/data-manager/service/events/eventService_test.go
similarity index 100%
rename from backend/service/events/eventService_test.go
rename to services/data-manager/service/events/eventService_test.go
diff --git a/backend/service/feed/feedFunctions.go b/services/data-manager/service/feed/feedFunctions.go
similarity index 100%
rename from backend/service/feed/feedFunctions.go
rename to services/data-manager/service/feed/feedFunctions.go
diff --git a/backend/service/feed/feedFunctions_test.go b/services/data-manager/service/feed/feedFunctions_test.go
similarity index 100%
rename from backend/service/feed/feedFunctions_test.go
rename to services/data-manager/service/feed/feedFunctions_test.go
diff --git a/backend/service/feed/mockData/data.db b/services/data-manager/service/feed/mockData/data.db
similarity index 100%
rename from backend/service/feed/mockData/data.db
rename to services/data-manager/service/feed/mockData/data.db
diff --git a/backend/service/feed/mockData/logs.db b/services/data-manager/service/feed/mockData/logs.db
similarity index 100%
rename from backend/service/feed/mockData/logs.db
rename to services/data-manager/service/feed/mockData/logs.db
diff --git a/backend/service/fetch/htmlDownloader.go b/services/data-manager/service/fetch/htmlDownloader.go
similarity index 100%
rename from backend/service/fetch/htmlDownloader.go
rename to services/data-manager/service/fetch/htmlDownloader.go
diff --git a/backend/service/fetch/sport/sportFetcher.go b/services/data-manager/service/fetch/sport/sportFetcher.go
similarity index 100%
rename from backend/service/fetch/sport/sportFetcher.go
rename to services/data-manager/service/fetch/sport/sportFetcher.go
diff --git a/backend/service/fetch/sport/sportFetcher_test.go b/services/data-manager/service/fetch/sport/sportFetcher_test.go
similarity index 100%
rename from backend/service/fetch/sport/sportFetcher_test.go
rename to services/data-manager/service/fetch/sport/sportFetcher_test.go
diff --git a/backend/service/fetch/v1/fetchSeminarEventService.go b/services/data-manager/service/fetch/v1/fetchSeminarEventService.go
similarity index 100%
rename from backend/service/fetch/v1/fetchSeminarEventService.go
rename to services/data-manager/service/fetch/v1/fetchSeminarEventService.go
diff --git a/backend/service/fetch/v1/fetchSeminarEventService_test.go b/services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go
similarity index 100%
rename from backend/service/fetch/v1/fetchSeminarEventService_test.go
rename to services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go
diff --git a/backend/service/fetch/v1/fetchSeminarGroupService.go b/services/data-manager/service/fetch/v1/fetchSeminarGroupService.go
similarity index 100%
rename from backend/service/fetch/v1/fetchSeminarGroupService.go
rename to services/data-manager/service/fetch/v1/fetchSeminarGroupService.go
diff --git a/backend/service/fetch/v1/fetchSeminarGroupService_test.go b/services/data-manager/service/fetch/v1/fetchSeminarGroupService_test.go
similarity index 100%
rename from backend/service/fetch/v1/fetchSeminarGroupService_test.go
rename to services/data-manager/service/fetch/v1/fetchSeminarGroupService_test.go
diff --git a/backend/service/fetch/v1/htmlParsingFunctions.go b/services/data-manager/service/fetch/v1/htmlParsingFunctions.go
similarity index 100%
rename from backend/service/fetch/v1/htmlParsingFunctions.go
rename to services/data-manager/service/fetch/v1/htmlParsingFunctions.go
diff --git a/backend/service/fetch/v2/eventParser.go b/services/data-manager/service/fetch/v2/eventParser.go
similarity index 100%
rename from backend/service/fetch/v2/eventParser.go
rename to services/data-manager/service/fetch/v2/eventParser.go
diff --git a/backend/service/fetch/v2/fetcher.go b/services/data-manager/service/fetch/v2/fetcher.go
similarity index 100%
rename from backend/service/fetch/v2/fetcher.go
rename to services/data-manager/service/fetch/v2/fetcher.go
diff --git a/backend/service/fetch/v2/fetcher_test.go b/services/data-manager/service/fetch/v2/fetcher_test.go
similarity index 100%
rename from backend/service/fetch/v2/fetcher_test.go
rename to services/data-manager/service/fetch/v2/fetcher_test.go
diff --git a/backend/service/fetch/v2/htmlParsingFunctions.go b/services/data-manager/service/fetch/v2/htmlParsingFunctions.go
similarity index 100%
rename from backend/service/fetch/v2/htmlParsingFunctions.go
rename to services/data-manager/service/fetch/v2/htmlParsingFunctions.go
diff --git a/backend/service/functions/semester.go b/services/data-manager/service/functions/semester.go
similarity index 100%
rename from backend/service/functions/semester.go
rename to services/data-manager/service/functions/semester.go
diff --git a/backend/service/functions/semester_test.go b/services/data-manager/service/functions/semester_test.go
similarity index 100%
rename from backend/service/functions/semester_test.go
rename to services/data-manager/service/functions/semester_test.go
diff --git a/backend/service/functions/string.go b/services/data-manager/service/functions/string.go
similarity index 100%
rename from backend/service/functions/string.go
rename to services/data-manager/service/functions/string.go
diff --git a/backend/service/functions/string_test.go b/services/data-manager/service/functions/string_test.go
similarity index 100%
rename from backend/service/functions/string_test.go
rename to services/data-manager/service/functions/string_test.go
diff --git a/backend/service/functions/time/mockClock.go b/services/data-manager/service/functions/time/mockClock.go
similarity index 100%
rename from backend/service/functions/time/mockClock.go
rename to services/data-manager/service/functions/time/mockClock.go
diff --git a/backend/service/functions/time/parse.go b/services/data-manager/service/functions/time/parse.go
similarity index 100%
rename from backend/service/functions/time/parse.go
rename to services/data-manager/service/functions/time/parse.go
diff --git a/backend/service/functions/time/realClock.go b/services/data-manager/service/functions/time/realClock.go
similarity index 100%
rename from backend/service/functions/time/realClock.go
rename to services/data-manager/service/functions/time/realClock.go
diff --git a/backend/service/functions/time/time.go b/services/data-manager/service/functions/time/time.go
similarity index 100%
rename from backend/service/functions/time/time.go
rename to services/data-manager/service/functions/time/time.go
diff --git a/backend/service/ical/ical.go b/services/data-manager/service/ical/ical.go
similarity index 57%
rename from backend/service/ical/ical.go
rename to services/data-manager/service/ical/ical.go
index d1cea64..ec604fc 100644
--- a/backend/service/ical/ical.go
+++ b/services/data-manager/service/ical/ical.go
@@ -17,60 +17,13 @@
package ical
import (
- "bytes"
"encoding/json"
- "htwkalender/model"
- "htwkalender/service/db"
- "htwkalender/service/feed"
- "time"
-
- "github.com/jordic/goics"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
+ "htwkalender/model"
+ "htwkalender/service/db"
)
-const expirationTime = 5 * time.Minute
-
-func Feed(app *pocketbase.PocketBase, token string) (string, error) {
- var result string
-
- icalFeed, err := db.FindFeedByToken(token, app)
- if icalFeed == nil && err != nil {
- return "", err
- }
-
- modules := make(map[string]model.FeedCollection)
- var modulesArray []model.FeedCollection
- _ = json.Unmarshal([]byte(icalFeed.Modules), &modulesArray)
-
- for _, module := range modulesArray {
- modules[module.UUID] = module
- }
-
- newFeed, createFeedErr := createFeedForToken(app, modules)
- if createFeedErr != nil {
- return "", createFeedErr
- }
- result = newFeed.Content
-
- return result, nil
-}
-
-func createFeedForToken(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) (*model.FeedModel, error) {
- events, err := db.GetPlanForModules(app, modules)
- if err != nil {
- return nil, apis.NewNotFoundError("Could not fetch events", err)
- }
-
- // Combine Events
- events = feed.CombineEventsInFeed(events)
-
- b := bytes.Buffer{}
- goics.NewICalEncode(&b).Encode(IcalModel{Events: events, Mapping: modules})
- icalFeed := &model.FeedModel{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)}
- return icalFeed, nil
-}
-
func CreateIndividualFeed(requestBody []byte, app *pocketbase.PocketBase) (string, error) {
var modules []model.FeedCollection
diff --git a/backend/service/ical/icalFileGeneration.go b/services/data-manager/service/ical/icalFileGeneration.go
similarity index 100%
rename from backend/service/ical/icalFileGeneration.go
rename to services/data-manager/service/ical/icalFileGeneration.go
diff --git a/backend/service/ical/icalFileGeneration_test.go b/services/data-manager/service/ical/icalFileGeneration_test.go
similarity index 100%
rename from backend/service/ical/icalFileGeneration_test.go
rename to services/data-manager/service/ical/icalFileGeneration_test.go
diff --git a/backend/service/ical/icsComponenter.go b/services/data-manager/service/ical/icsComponenter.go
similarity index 100%
rename from backend/service/ical/icsComponenter.go
rename to services/data-manager/service/ical/icsComponenter.go
diff --git a/backend/service/names/userDefinedNameTemplates.go b/services/data-manager/service/names/userDefinedNameTemplates.go
similarity index 100%
rename from backend/service/names/userDefinedNameTemplates.go
rename to services/data-manager/service/names/userDefinedNameTemplates.go
diff --git a/backend/service/names/userDefinedNameTemplates_test.go b/services/data-manager/service/names/userDefinedNameTemplates_test.go
similarity index 100%
rename from backend/service/names/userDefinedNameTemplates_test.go
rename to services/data-manager/service/names/userDefinedNameTemplates_test.go
diff --git a/backend/service/room/roomService.go b/services/data-manager/service/room/roomService.go
similarity index 100%
rename from backend/service/room/roomService.go
rename to services/data-manager/service/room/roomService.go
diff --git a/backend/service/room/roomService_test.go b/services/data-manager/service/room/roomService_test.go
similarity index 100%
rename from backend/service/room/roomService_test.go
rename to services/data-manager/service/room/roomService_test.go
diff --git a/backend/sonar-project.properties b/services/data-manager/sonar-project.properties
similarity index 100%
rename from backend/sonar-project.properties
rename to services/data-manager/sonar-project.properties
diff --git a/services/ical/Dockerfile b/services/ical/Dockerfile
new file mode 100644
index 0000000..2bc4c3b
--- /dev/null
+++ b/services/ical/Dockerfile
@@ -0,0 +1,70 @@
+#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 .
+
+# build stage
+FROM golang:alpine AS build
+
+WORKDIR /app
+
+# Copy the source from the current directory to the Working Directory inside the container
+COPY . ./
+# download needed modules
+RUN apk add --no-cache --update go gcc g++ && \
+ go mod download && \
+ CGO_ENABLED=1 GOOS=linux go build -o /htwkalender-ical
+
+# production stage
+FROM alpine:latest AS prod
+
+WORKDIR /htwkalender-ical
+
+ARG USER=ical
+RUN adduser -Ds /bin/sh $USER && \
+ chown $USER:$USER ./
+
+USER $USER
+RUN mkdir -p data
+
+# copies executable from build container
+COPY --chown=$USER:$USER --from=build /htwkalender-ical ./
+
+# Expose port 8091 to the outside world
+EXPOSE 8091
+
+ENTRYPOINT ["./htwkalender-ical"]
+
+
+FROM golang:1.21.6 AS dev
+
+# Set the Current Working Directory inside the container
+WORKDIR /htwkalender-ical
+
+# Copy go mod and sum files
+COPY go.mod go.sum ./
+RUN go mod download
+
+# Copy the source from the current directory to the Working Directory inside the container
+COPY *.go ./
+COPY . .
+
+# Build the Go app
+RUN CGO_ENABLED=1 GOOS=linux go build -o /htwkalender-ical
+
+# Expose port 8091 to the outside world
+EXPOSE 8091
+
+# Entry point
+ENTRYPOINT ["./htwkalender-ical"]
\ No newline at end of file
diff --git a/services/ical/go.mod b/services/ical/go.mod
new file mode 100644
index 0000000..c087dbe
--- /dev/null
+++ b/services/ical/go.mod
@@ -0,0 +1,90 @@
+module htwkalender-ical
+
+go 1.21.6
+
+require (
+ github.com/gofiber/fiber/v3 v3.0.0-beta.2
+ github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0
+ github.com/pocketbase/pocketbase v0.22.12
+)
+
+require (
+ github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.27.13 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.13 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 // indirect
+ github.com/aws/smithy-go v1.20.2 // indirect
+ github.com/disintegration/imaging v1.6.2 // indirect
+ github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/fatih/color v1.16.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/ganigeorgiev/fexpr v0.4.0 // indirect
+ github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.4 // indirect
+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/klauspost/compress v1.17.8 // indirect
+ github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
+ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/pocketbase/dbx v1.10.1 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/cobra v1.8.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.54.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ gocloud.dev v0.37.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
+ golang.org/x/image v0.16.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/oauth2 v0.20.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/term v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
+ google.golang.org/api v0.180.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240509183442-62759503f434 // indirect
+ google.golang.org/grpc v1.63.2 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
+ modernc.org/libc v1.50.5 // indirect
+ modernc.org/mathutil v1.6.0 // indirect
+ modernc.org/memory v1.8.0 // indirect
+ modernc.org/sqlite v1.29.9 // indirect
+ modernc.org/strutil v1.2.0 // indirect
+ modernc.org/token v1.1.0 // indirect
+)
diff --git a/services/ical/go.sum b/services/ical/go.sum
new file mode 100644
index 0000000..e4d76bd
--- /dev/null
+++ b/services/ical/go.sum
@@ -0,0 +1,384 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw=
+cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
+cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
+cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
+cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
+cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
+cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
+cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
+github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
+github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+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=
+github.com/aws/aws-sdk-go v1.51.11 h1:El5VypsMIz7sFwAAj/j06JX9UGs4KAbAIEaZ57bNY4s=
+github.com/aws/aws-sdk-go v1.51.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
+github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
+github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
+github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17 h1:9b1Os1s11mF5qTIKLgSsyPG810di2+ySSLIIt9bwe9I=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17/go.mod h1:9Wp7tDOMhv0+sb/FTRAkbHNQ7abYDnoJRzm5AAtCnTc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2 h1:rq2hglTQM3yHZvOPVMtNvLS5x6hijx7JvRDgKiTNDGQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
+github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
+github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+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=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
+github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
+github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/ganigeorgiev/fexpr v0.4.0 h1:ojitI+VMNZX/odeNL1x3RzTTE8qAIVvnSSYPNAnQFDI=
+github.com/ganigeorgiev/fexpr v0.4.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
+github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
+github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gofiber/fiber/v3 v3.0.0-beta.2 h1:mVVgt8PTaHGup3NGl/+7U7nEoZaXJ5OComV4E+HpAao=
+github.com/gofiber/fiber/v3 v3.0.0-beta.2/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM=
+github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
+github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
+github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
+github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0 h1:p+k2RozdR141dIkAbOuZafkZjrcjT/YvwYYH7qCSG+c=
+github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0/go.mod h1:YHaw6sOIeFRob8Y9q/blEAMfVcLpeE9+vdhrwyEMxoI=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
+github.com/klauspost/compress v1.17.8/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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
+github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
+github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
+github.com/pocketbase/pocketbase v0.22.12 h1:2wWcs2yoyA3OkbW9cxFShemzchOh3eHFr1e8QvLy5is=
+github.com/pocketbase/pocketbase v0.22.12/go.mod h1:yY/3IGi1tUbcI6yGVFspAyKi/IDHCntdYk4IAP32UF0=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+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.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=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.54.0 h1:cCL+ZZR3z3HPLMVfEYVUMtJqVaui0+gu7Lx63unHwS0=
+github.com/valyala/fasthttp v1.54.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
+gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
+golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
+google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s=
+google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U=
+google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240509183442-62759503f434 h1:umK/Ey0QEzurTNlsV3R+MfxHAb78HCEX/IkuR+zH4WQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240509183442-62759503f434/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
+google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/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.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=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+modernc.org/cc/v4 v4.21.0 h1:D/gLKtcztomvWbsbvBKo3leKQv+86f+DdqEZBBXhnag=
+modernc.org/cc/v4 v4.21.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.17.3 h1:t2CQci84jnxKw3GGnHvjGKjiNZeZqyQx/023spkk4hU=
+modernc.org/ccgo/v4 v4.17.3/go.mod h1:1FCbAtWYJoKuc+AviS+dH+vGNtYmFJqBeRWjmnDWsIg=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
+modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
+modernc.org/libc v1.50.5 h1:ZzeUd0dIc/sUtoPTCYIrgypkuzoGzNu6kbEWj2VuEmk=
+modernc.org/libc v1.50.5/go.mod h1:rhzrUx5oePTSTIzBgM0mTftwWHK8tiT9aNFUt1mldl0=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow=
+modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/services/ical/main.go b/services/ical/main.go
new file mode 100644
index 0000000..4865e7c
--- /dev/null
+++ b/services/ical/main.go
@@ -0,0 +1,48 @@
+//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 main
+
+import (
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/log"
+ "github.com/gofiber/fiber/v3/middleware/logger"
+ "htwkalender-ical/service"
+)
+
+// main function for the ical service
+// uses rest api to get the data from the data-manager
+// exposes rest api endpoints with fiber to serve the data for clients
+func main() {
+
+ // Initialize a new Fiber app
+ webdavRequestMethods := []string{"PROPFIND", "MKCOL", "COPY", "MOVE"}
+
+ app := fiber.New(fiber.Config{
+ CaseSensitive: true,
+ StrictRouting: true,
+ ServerHeader: "Fiber",
+ AppName: "App Name",
+ RequestMethods: append(fiber.DefaultMethods[:], webdavRequestMethods...),
+ })
+
+ app.Use(logger.New())
+
+ // Add routes to the app instance for the data-manager ical service
+ service.AddFeedRoutes(app)
+
+ log.Fatal(app.Listen(":8091"))
+}
diff --git a/services/ical/model/eventModel.go b/services/ical/model/eventModel.go
new file mode 100644
index 0000000..2d92a0d
--- /dev/null
+++ b/services/ical/model/eventModel.go
@@ -0,0 +1,111 @@
+//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 model
+
+import (
+ "github.com/pocketbase/pocketbase/models"
+ "slices"
+ "sort"
+ "strings"
+ "time"
+)
+
+type Events []Event
+
+func (events Events) Contains(event Event) bool {
+ return slices.Contains(events, event)
+}
+
+func (events Events) Sort() {
+ // Sort events by start time
+ sort.Slice(events, func(i, j int) bool {
+ return time.Time(events[i].Start).Before(time.Time(events[j].Start))
+ })
+}
+
+type AnonymizedEventDTO struct {
+ Day string `db:"Day" json:"day"`
+ Week string `db:"Week" json:"week"`
+ Start JSONTime `db:"start" json:"start"`
+ End JSONTime `db:"end" json:"end"`
+ Rooms string `db:"Rooms" json:"rooms"`
+ Free bool `json:"free"`
+}
+
+type Event struct {
+ UUID string `db:"uuid" json:"uuid"`
+ Day string `db:"Day" json:"day"`
+ Week string `db:"Week" json:"week"`
+ Start JSONTime `db:"start" json:"start"`
+ End JSONTime `db:"end" json:"end"`
+ Name string `db:"Name" json:"name"`
+ EventType string `db:"EventType" json:"eventType"`
+ Compulsory string `db:"Compulsory" json:"compulsory"`
+ Prof string `db:"Prof" json:"prof"`
+ Rooms string `db:"Rooms" json:"rooms"`
+ Notes string `db:"Notes" json:"notes"`
+ BookedAt string `db:"BookedAt" json:"bookedAt"`
+ Course string `db:"course" json:"course"`
+ Semester string `db:"semester" json:"semester"`
+ models.BaseModel
+}
+
+type EventType struct {
+ EventType string `db:"EventType" json:"eventType"`
+}
+
+func (e *Event) Equals(event Event) bool {
+ return e.Day == event.Day &&
+ e.Week == event.Week &&
+ e.Start == event.Start &&
+ e.End == event.End &&
+ e.Name == event.Name &&
+ e.Course == event.Course &&
+ e.Prof == event.Prof &&
+ e.Rooms == event.Rooms &&
+ e.EventType == event.EventType
+}
+
+func (e *Event) TableName() string {
+ return "events"
+}
+
+// SetCourse func to set the course and returns the event
+func (e *Event) SetCourse(course string) Event {
+ e.Course = course
+ return *e
+}
+
+// AnonymizeEvent Creates an AnonymizedEventDTO from an Event hiding all sensitive data
+func (e *Event) AnonymizeEvent() AnonymizedEventDTO {
+ return AnonymizedEventDTO{
+ Day: e.Day,
+ Week: e.Week,
+ Start: e.Start,
+ End: e.End,
+ Rooms: e.Rooms,
+ Free: strings.Contains(strings.ToLower(e.Name), "zur freien verfügung"),
+ }
+}
+
+func (e *Event) GetName() string {
+ return e.Name
+}
+
+func (e *Event) SetName(name string) {
+ e.Name = name
+}
diff --git a/services/ical/model/eventModel_test.go b/services/ical/model/eventModel_test.go
new file mode 100644
index 0000000..bee7eb5
--- /dev/null
+++ b/services/ical/model/eventModel_test.go
@@ -0,0 +1,483 @@
+//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 model
+
+import (
+ "github.com/pocketbase/pocketbase/models"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestEvents_Contains(t *testing.T) {
+ // parse time for JSONTime "2020-01-01 12:00:00.000Z"
+ specificTime, _ := time.Parse("2006-01-02T15:04:05.000Z", "2020-01-01T12:00:00.000Z")
+ specificJSONTime := JSONTime(specificTime)
+
+ type args struct {
+ event Event
+ }
+ tests := []struct {
+ name string
+ m Events
+ args args
+ want bool
+ }{
+ {
+ name: "empty events",
+ m: Events{},
+ args: args{event: Event{}},
+ want: false,
+ },
+ {
+ name: "one event",
+ m: Events{{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"}},
+ args: args{event: Event{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"}},
+ want: true,
+ },
+ {
+ name: "two events",
+ m: Events{{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"}, {Day: "test2", Week: "test2", Start: specificJSONTime, End: specificJSONTime, Name: "test2", Course: "test2", Prof: "test2", Rooms: "test2", EventType: "test2"}},
+ args: args{event: Event{Day: "test2", Week: "test2", Start: specificJSONTime, End: specificJSONTime, Name: "test2", Course: "test2", Prof: "test2", Rooms: "test2", EventType: "test2"}},
+ want: true,
+ },
+ {
+ name: "two events with different values",
+ m: Events{{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test", UUID: "439ßu56rf8u9ijn4f4-2345345"}, {Day: "test2", Week: "test2", Start: specificJSONTime, End: specificJSONTime, Name: "test2", Course: "test2", Prof: "test2", Rooms: "test2", EventType: "test2", UUID: "432a39ßu545349ijn4f4-23dsa45"}},
+ args: args{event: Event{Day: "test3", Week: "test3", Start: specificJSONTime, End: specificJSONTime, Name: "test3", Course: "test3", Prof: "test3", Rooms: "test3", EventType: "test3", UUID: "934mf43r34f-g68h7655tg3"}},
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.m.Contains(tt.args.event); got != tt.want {
+ t.Errorf("Contains() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_Equals(t *testing.T) {
+
+ specificTime, _ := time.Parse("2006-01-02T15:04:05.000Z", "2020-01-01T12:00:00.000Z")
+ specificJSONTime := JSONTime(specificTime)
+
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ event Event
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want bool
+ }{
+ {
+ name: "empty events",
+ fields: fields{},
+ args: args{event: Event{}},
+ want: true,
+ },
+ {
+ name: "one empty one not",
+ fields: fields{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"},
+ args: args{event: Event{}},
+ want: false,
+ },
+ {
+ name: "one event",
+ fields: fields{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"},
+ args: args{event: Event{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"}},
+ want: true,
+ },
+ {
+ name: "two events",
+ fields: fields{Day: "test", Week: "test", Start: specificJSONTime, End: specificJSONTime, Name: "test", Course: "test", Prof: "test", Rooms: "test", EventType: "test"},
+ args: args{event: Event{Day: "test2", Week: "test2", Start: specificJSONTime, End: specificJSONTime, Name: "test2", Course: "test2", Prof: "test2", Rooms: "test2", EventType: "test2"}},
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := m.Equals(tt.args.event); got != tt.want {
+ t.Errorf("Equals() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_AnonymizeEvent(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want AnonymizedEventDTO
+ }{
+ {
+ name: "empty event",
+ fields: fields{},
+ want: AnonymizedEventDTO{Day: "", Week: "", Start: JSONTime{}, End: JSONTime{}, Rooms: "", Free: false},
+ },
+ {
+ name: "one event",
+ fields: fields{Name: "Event", Day: "test", Week: "test", Rooms: "test"},
+ want: AnonymizedEventDTO{Day: "test", Week: "test", Start: JSONTime{}, End: JSONTime{}, Rooms: "test", Free: false},
+ },
+ {
+ name: "one event with free",
+ fields: fields{Name: "Räume zur freien Verfügung", Day: "test", Week: "test", Rooms: "test", Course: "test"},
+ want: AnonymizedEventDTO{Day: "test", Week: "test", Start: JSONTime{}, End: JSONTime{}, Rooms: "test", Free: true},
+ },
+ {
+ name: "another free event",
+ fields: fields{Name: "Zur freien Verfügung", Day: "Montag", Week: "5", Start: JSONTime{}, End: JSONTime{}, Rooms: "TR_A1.28-S", Course: "42INM-3"},
+ want: AnonymizedEventDTO{Day: "Montag", Week: "5", Start: JSONTime{}, End: JSONTime{}, Rooms: "TR_A1.28-S", Free: true},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := m.AnonymizeEvent(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Event.AnonymizeEvent() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_GetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {
+ name: "empty event",
+ fields: fields{},
+ want: "",
+ },
+ {
+ name: "one event",
+ fields: fields{Name: "Event"},
+ want: "Event",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.GetName(); got != tt.want {
+ t.Errorf("GetName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_SetCourse(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ course string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want Event
+ }{
+ {
+ name: "set course",
+ fields: fields{},
+ args: args{course: "test"},
+ want: Event{Course: "test"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.SetCourse(tt.args.course); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("SetCourse() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_SetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ }{
+ {
+ name: "set name",
+ fields: fields{
+ Name: "name",
+ },
+ args: args{
+ name: "name",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ e.SetName(tt.args.name)
+ })
+ }
+}
+
+func TestEvent_TableName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start JSONTime
+ End JSONTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {
+ name: "table name",
+ fields: fields{},
+ want: "events",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.TableName(); got != tt.want {
+ t.Errorf("TableName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvents_Contains1(t *testing.T) {
+ type args struct {
+ event Event
+ }
+ tests := []struct {
+ name string
+ m Events
+ args args
+ want bool
+ }{
+ {
+ name: "empty events",
+ m: Events{},
+ args: args{event: Event{}},
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.m.Contains(tt.args.event); got != tt.want {
+ t.Errorf("Contains() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/services/ical/model/feedModel.go b/services/ical/model/feedModel.go
new file mode 100644
index 0000000..864d629
--- /dev/null
+++ b/services/ical/model/feedModel.go
@@ -0,0 +1,44 @@
+//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 model
+
+type BaseModel struct {
+ Id string `db:"id" json:"id"`
+ Created JSONTime `db:"created" json:"created"`
+ Updated JSONTime `db:"updated" json:"updated"`
+ CollectionName string `db:"collection_name" json:"collectionName"`
+ CollectionId string `db:"collection_id" json:"collectionId"`
+}
+
+type Feed struct {
+ Modules string `db:"modules" json:"modules"`
+ Retrieved JSONTime `db:"retrieved" json:"retrieved"`
+ BaseModel
+}
+
+// SetModules set modules field
+func (f *Feed) SetModules(modules string) {
+ f.Modules = modules
+}
+
+type FeedCollection struct {
+ UUID string `db:"uuid" json:"uuid"`
+ Name string `db:"Name" json:"name"`
+ Course string `db:"course" json:"course"`
+ UserDefinedName string `db:"userDefinedName" json:"userDefinedName"`
+ Reminder bool `db:"reminder" json:"reminder"`
+}
diff --git a/services/ical/model/icalModel.go b/services/ical/model/icalModel.go
new file mode 100644
index 0000000..a8b09ed
--- /dev/null
+++ b/services/ical/model/icalModel.go
@@ -0,0 +1,82 @@
+//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 model
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// IcalModel local type for EmitICal function
+type IcalModel struct {
+ Events Events
+ Mapping map[string]FeedCollection
+}
+
+// FeedModel is an iCal feed
+type FeedModel struct {
+ Content string
+ ExpiresAt JSONTime
+ Semester string
+ Course string
+}
+
+// Entry is a time entry
+type Entry struct {
+ DateStart JSONTime `json:"dateStart"`
+ DateEnd JSONTime `json:"dateEnd"`
+ Description string `json:"description"`
+}
+
+// Entries is a collection of entries
+type Entries []*Entry
+
+type FeedModule struct {
+ UUID string `db:"uuid" json:"uuid"`
+ Name string `db:"Name" json:"name"`
+ Course string `db:"course" json:"course"`
+ UserDefinedName string `db:"userDefinedName" json:"userDefinedName"`
+ Reminder bool `db:"reminder" json:"reminder"`
+}
+
+type FeedRecord struct {
+ Modules []FeedModule `db:"modules" json:"modules"`
+ Retrieved JSONTime `db:"retrieved" json:"retrieved"`
+ BaseModel
+}
+
+type JSONTime time.Time
+
+// MarshalJSON Implement Marshaler and Unmarshaler interface
+func (j JSONTime) MarshalJSON() ([]byte, error) {
+ return json.Marshal(time.Time(j))
+}
+
+func (jt *JSONTime) UnmarshalJSON(b []byte) error {
+ timeString := strings.Trim(string(b), `"`)
+ if timeString == "null" || timeString == "" {
+ return nil
+ }
+ t, err := time.Parse("2006-01-02 15:04:05.000Z", timeString)
+ if err == nil {
+ *jt = JSONTime(t)
+ return nil
+ }
+ return fmt.Errorf("error parsing time string %s: %w", timeString, err)
+}
diff --git a/services/ical/model/moduleModel.go b/services/ical/model/moduleModel.go
new file mode 100644
index 0000000..eddb839
--- /dev/null
+++ b/services/ical/model/moduleModel.go
@@ -0,0 +1,12 @@
+package model
+
+type Module struct {
+ UUID string `db:"uuid" json:"uuid"`
+ Name string `db:"Name" json:"name"`
+ Course string `db:"course" json:"course"`
+ Prof string `db:"prof" json:"prof"`
+ Semester string `db:"semester" json:"semester"`
+ Events Events `json:"events"`
+}
+
+type Modules []Module
diff --git a/services/ical/service/connector/feedConnector.go b/services/ical/service/connector/feedConnector.go
new file mode 100644
index 0000000..7c98663
--- /dev/null
+++ b/services/ical/service/connector/feedConnector.go
@@ -0,0 +1,101 @@
+package connector
+
+import (
+ "encoding/json"
+ "errors"
+ "htwkalender-ical/model"
+ "log/slog"
+)
+
+func GetFeedByToken(token string) (model.FeedRecord, error) {
+ var feed model.FeedRecord
+
+ // /api/collections/feeds/records/{id}
+
+ response, err := RequestApi("/api/collections/feeds/records/" + token)
+ if err != nil {
+ return model.FeedRecord{}, err
+ }
+
+ // parse the response body json to FeedRecord struct
+ feed, err = parseResponse(response.Body())
+ if err != nil {
+ slog.Error("Failed to read response body", "error", err)
+ return model.FeedRecord{}, err
+ }
+
+ return feed, nil
+
+}
+
+func parseResponse(response []byte) (model.FeedRecord, error) {
+ var feedRecord model.FeedRecord
+
+ err := json.Unmarshal(response, &feedRecord)
+ if err != nil {
+ return model.FeedRecord{}, err
+ }
+
+ return feedRecord, nil
+}
+
+func DeleteFeedRecord(token string) error {
+ err := DeleteRequestApi("/api/feed?token=" + token)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GetModuleWithEvents(module model.FeedModule) (model.Module, error) {
+ var modules model.Module
+
+ // /api/module?uuid=
+
+ response, err := RequestApi("/api/module?uuid=" + module.UUID)
+ if err != nil {
+ return model.Module{}, err
+ }
+
+ // parse the response body json to Events struct
+ modules, err = parseModuleResponse(response.Body())
+ if err != nil {
+ slog.Error("Failed to read response body", "error", err)
+ return model.Module{}, err
+ }
+
+ return modules, nil
+}
+
+func parseModuleResponse(body []byte) (model.Module, error) {
+ var module model.Module
+
+ err := json.Unmarshal(body, &module)
+ if err != nil {
+ return model.Module{}, err
+ }
+
+ return module, nil
+}
+
+func SaveFeedRecord(modules []model.FeedCollection) (string, error) {
+ var token string
+
+ response, err := PostRequestApi("/api/feed", modules)
+ if err != nil {
+ return "", err
+ }
+
+ if response.StatusCode() != 200 {
+ return "", errors.New("failed to save feed")
+ }
+
+ // parse the response body json to string
+ err = json.Unmarshal(response.Body(), &token)
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/services/ical/service/connector/restHandler.go b/services/ical/service/connector/restHandler.go
new file mode 100644
index 0000000..7fc3381
--- /dev/null
+++ b/services/ical/service/connector/restHandler.go
@@ -0,0 +1,59 @@
+package connector
+
+import (
+ "github.com/gofiber/fiber/v3/client"
+ "htwkalender-ical/model"
+ "time"
+)
+
+const host = "http://htwkalender-data-manager:8090"
+
+func RequestApi(path string) (*client.Response, error) {
+
+ cc := client.New()
+ cc.SetTimeout(5 * time.Second)
+
+ // set retry to 0
+ response, err := cc.Get(host + path)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, nil
+}
+
+func DeleteRequestApi(path string) error {
+
+ cc := client.New()
+ cc.SetTimeout(5 * time.Second)
+
+ // set retry to 0
+ _, err := cc.Delete(host + path)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func PostRequestApi(path string, body []model.FeedCollection) (*client.Response, error) {
+
+ cc := client.New()
+ cc.SetTimeout(5 * time.Second)
+
+ config := client.Config{
+ Body: body,
+ Header: map[string]string{
+ "Content-Type": "application/json",
+ "Accept": "*/*",
+ },
+ }
+
+ // set retry to 0
+ response, err := cc.Post(host+path, config)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, nil
+}
diff --git a/services/ical/service/functions/semester.go b/services/ical/service/functions/semester.go
new file mode 100644
index 0000000..9cf7006
--- /dev/null
+++ b/services/ical/service/functions/semester.go
@@ -0,0 +1,47 @@
+//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 functions
+
+import (
+ localTime "htwkalender-ical/service/functions/time"
+ "time"
+)
+
+// GetCurrentSemesterString returns the current semester as string
+// if current month is between 10 and 03 -> winter semester "ws"
+func GetCurrentSemesterString(localeTime localTime.Clock) string {
+ if localeTime.Now().Month() >= 10 || localeTime.Now().Month() <= 3 {
+ return "ws"
+ } else {
+ return "ss"
+ }
+}
+
+func CalculateSemesterList(clock localTime.Clock) []string {
+ summerSemester := clock.Now().Month() >= time.March && clock.Now().Month() <= time.September
+ winterSemester := clock.Now().Month() <= time.March || clock.Now().Month() >= time.September
+
+ if summerSemester && !winterSemester {
+ return []string{"ss"}
+ }
+
+ if !summerSemester && winterSemester {
+ return []string{"ws"}
+ }
+
+ return []string{"ss", "ws"}
+}
diff --git a/services/ical/service/functions/semester_test.go b/services/ical/service/functions/semester_test.go
new file mode 100644
index 0000000..5a5f96c
--- /dev/null
+++ b/services/ical/service/functions/semester_test.go
@@ -0,0 +1,91 @@
+package functions
+
+import (
+ mockTime "htwkalender-ical/service/functions/time"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func Test_calculateSemesterList(t *testing.T) {
+ type args struct {
+ clock mockTime.Clock
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ name: "is summer semester",
+ args: args{
+ clock: mockTime.MockClock{
+ NowTime: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ want: []string{"ss"},
+ },
+ {
+ name: "is winter semester",
+ args: args{
+ clock: mockTime.MockClock{
+ NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ want: []string{"ws"},
+ },
+ {
+ name: "is in both",
+ args: args{
+ clock: mockTime.MockClock{
+ NowTime: time.Date(2024, 3, 22, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ want: []string{"ss", "ws"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := CalculateSemesterList(tt.args.clock); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("calculateSemesterList() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestGetCurrentSemesterString(t *testing.T) {
+ type args struct {
+ localeTime mockTime.Clock
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "is winter semester",
+ args: args{
+ localeTime: mockTime.MockClock{
+ NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ want: "ws",
+ },
+ {
+ name: "is summer semester",
+ args: args{
+ localeTime: mockTime.MockClock{
+ NowTime: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ want: "ss",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := GetCurrentSemesterString(tt.args.localeTime); got != tt.want {
+ t.Errorf("GetCurrentSemesterString() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/services/ical/service/functions/string.go b/services/ical/service/functions/string.go
new file mode 100644
index 0000000..77984a4
--- /dev/null
+++ b/services/ical/service/functions/string.go
@@ -0,0 +1,62 @@
+//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 functions
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "strings"
+)
+
+// check if string is empty or contains only whitespaces
+func OnlyWhitespace(word string) bool {
+ return len(strings.TrimSpace(word)) == 0
+}
+
+// return function to check if rune is a separator
+func IsSeparator(separator []rune) func(rune) bool {
+ return func(character rune) bool {
+ for _, sep := range separator {
+ if sep == character {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+func Contains(s []string, e string) bool {
+ for _, a := range s {
+ if a == e {
+ return true
+ }
+ }
+ return false
+}
+
+func HashString(s string) string {
+ hash := sha256.New()
+ hash.Write([]byte(s))
+ hashInBytes := hash.Sum(nil)
+ return hex.EncodeToString(hashInBytes)
+}
+
+func SeperateRoomString(rooms string) []string {
+ return strings.FieldsFunc(rooms, IsSeparator(
+ []rune{',', '\t', '\n', '\r', ';', ' ', '\u00A0'}),
+ )
+}
diff --git a/services/ical/service/functions/string_test.go b/services/ical/service/functions/string_test.go
new file mode 100644
index 0000000..ee8316a
--- /dev/null
+++ b/services/ical/service/functions/string_test.go
@@ -0,0 +1,145 @@
+//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 functions
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestOnlyWhitespace(t *testing.T) {
+ type args struct {
+ word string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {"empty string", args{""}, true},
+ {"whitespace", args{" "}, true},
+ {"whitespaces", args{" "}, true},
+ {"whitespaces and tabs", args{" \t"}, true},
+ {"whitespaces and tabs and newlines", args{" \t\n"}, true},
+ {"whitespaces and tabs and newlines and non-breaking spaces", args{" \t\n\u00A0"}, true},
+ {"non-whitespace", args{"a"}, false},
+ {"non-whitespaces", args{"abc"}, false},
+ {"non-whitespaces and tabs", args{"abc\t"}, false},
+ {"non-whitespaces and tabs and newlines", args{"abc\t\n"}, false},
+ {"non-whitespaces and tabs and newlines and non-breaking spaces", args{"abc\t\n\u00A0"}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := OnlyWhitespace(tt.args.word); got != tt.want {
+ t.Errorf("OnlyWhitespace() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestHashString(t *testing.T) {
+ type args struct {
+ s string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {"empty string", args{""}, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
+ {"non-empty string", args{"abc"}, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := HashString(tt.args.s); got != tt.want {
+ t.Errorf("HashString() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestIsSeparator(t *testing.T) {
+ type args struct {
+ separator []rune
+ character rune
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {"empty separator", args{[]rune{}, 'a'}, false},
+ {"separator with one rune equal", args{[]rune{'a'}, 'a'}, true},
+ {"separator with one rune different", args{[]rune{'a'}, 'b'}, false},
+ {"separator with two runes equal", args{[]rune{'a', 'b'}, 'a'}, true},
+ {"separator with two runes equal second", args{[]rune{'a', 'b'}, 'b'}, true},
+ {"separator with two runes different", args{[]rune{'a', 'b'}, 'c'}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := IsSeparator(tt.args.separator)(tt.args.character); got != tt.want {
+ t.Errorf("IsSeparator()() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestContains(t *testing.T) {
+ type args struct {
+ s []string
+ e string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {"empty slice", args{[]string{}, "a"}, false},
+ {"slice with one element equal", args{[]string{"a"}, "a"}, true},
+ {"slice with one element different", args{[]string{"a"}, "b"}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Contains(tt.args.s, tt.args.e); got != tt.want {
+ t.Errorf("Contains() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSeperateRoomString(t *testing.T) {
+ type args struct {
+ rooms string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {"empty string", args{""}, []string{}},
+ {"one room", args{"a"}, []string{"a"}},
+ {"two rooms", args{"a,b"}, []string{"a", "b"}},
+ {"two rooms with whitespace", args{"a, b"}, []string{"a", "b"}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := SeperateRoomString(tt.args.rooms); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("SeperateRoomString() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/services/ical/service/functions/time/mockClock.go b/services/ical/service/functions/time/mockClock.go
new file mode 100644
index 0000000..cfac7e4
--- /dev/null
+++ b/services/ical/service/functions/time/mockClock.go
@@ -0,0 +1,26 @@
+//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 time
+
+import "time"
+
+type MockClock struct {
+ NowTime time.Time
+}
+
+func (m MockClock) Now() time.Time { return m.NowTime }
+func (MockClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
diff --git a/services/ical/service/functions/time/parse.go b/services/ical/service/functions/time/parse.go
new file mode 100644
index 0000000..97a4f22
--- /dev/null
+++ b/services/ical/service/functions/time/parse.go
@@ -0,0 +1,27 @@
+//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 time
+
+import (
+ "htwkalender-ical/model"
+ "time"
+)
+
+func ParseAsTypesDatetime(time time.Time) model.JSONTime {
+ dateTime := model.JSONTime(time)
+ return dateTime
+}
diff --git a/services/ical/service/functions/time/realClock.go b/services/ical/service/functions/time/realClock.go
new file mode 100644
index 0000000..cb98d1f
--- /dev/null
+++ b/services/ical/service/functions/time/realClock.go
@@ -0,0 +1,24 @@
+//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 time
+
+import "time"
+
+type RealClock struct{}
+
+func (RealClock) Now() time.Time { return time.Now() }
+func (RealClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
diff --git a/services/ical/service/functions/time/time.go b/services/ical/service/functions/time/time.go
new file mode 100644
index 0000000..ba239aa
--- /dev/null
+++ b/services/ical/service/functions/time/time.go
@@ -0,0 +1,24 @@
+//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 time
+
+import "time"
+
+type Clock interface {
+ Now() time.Time
+ After(d time.Duration) <-chan time.Time
+}
diff --git a/services/ical/service/ical/ical.go b/services/ical/service/ical/ical.go
new file mode 100644
index 0000000..05ae05d
--- /dev/null
+++ b/services/ical/service/ical/ical.go
@@ -0,0 +1,76 @@
+package ical
+
+import (
+ "bytes"
+ "github.com/jordic/goics"
+ "htwkalender-ical/model"
+ "htwkalender-ical/service/connector"
+ "log/slog"
+ "time"
+)
+
+const expirationTime = 5 * time.Minute
+
+func Feed(token string) (string, error) {
+ // get feed by token
+ feed, err := connector.GetFeedByToken(token)
+ if err != nil {
+ return "", err
+ }
+
+ var events model.Events
+
+ for _, module := range feed.Modules {
+ moduleWithEvents, err := connector.GetModuleWithEvents(module)
+ if err != nil {
+ return "", err
+ }
+
+ events = append(events, moduleWithEvents.Events...)
+ }
+
+ // Sorte events by start date
+ events.Sort()
+
+ modules := make(map[string]model.FeedCollection)
+ for _, module := range feed.Modules {
+ modules[module.UUID] = model.FeedCollection(module)
+ }
+
+ 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))}
+
+ return icalFeed.Content, nil
+}
+
+func FeedRecord(token string) (model.FeedRecord, error) {
+
+ feedRecord, err := connector.GetFeedByToken(token)
+ if err != nil {
+ return model.FeedRecord{}, err
+ }
+
+ return feedRecord, nil
+
+}
+
+func DeleteFeedRecord(token string) error {
+ err := connector.DeleteFeedRecord(token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func CreateFeed(modules []model.FeedCollection) (string, error) {
+
+ // Save feed
+ token, err := connector.SaveFeedRecord(modules)
+ if err != nil {
+ slog.Error("Failed to save feed", "error", err)
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/services/ical/service/ical/icalFileGeneration.go b/services/ical/service/ical/icalFileGeneration.go
new file mode 100644
index 0000000..57eb4a3
--- /dev/null
+++ b/services/ical/service/ical/icalFileGeneration.go
@@ -0,0 +1,173 @@
+//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-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
+}
+
+// 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(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)))
+
+ 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 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)
+ }
+}
+
+// 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.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/ical/icalFileGeneration_test.go b/services/ical/service/ical/icalFileGeneration_test.go
new file mode 100644
index 0000000..c1a92f0
--- /dev/null
+++ b/services/ical/service/ical/icalFileGeneration_test.go
@@ -0,0 +1,258 @@
+//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 (
+ "github.com/jordic/goics"
+ "htwkalender-ical/model"
+ mockTime "htwkalender-ical/service/functions/time"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestIcalModel_EmitICal(t *testing.T) {
+ type fields struct {
+ Events model.Events
+ Mapping map[string]model.FeedCollection
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want *goics.Component
+ }{
+ {
+ name: "Test EmitICal",
+ fields: fields{
+ Events: model.Events{
+ {
+ UUID: "123",
+ Name: "Test",
+ EventType: "Test",
+ Notes: "Test",
+ Prof: "Test",
+ Rooms: "Test",
+ BookedAt: "Test",
+ },
+ },
+ Mapping: map[string]model.FeedCollection{
+ "123": {
+ UUID: "123",
+ Name: "Test",
+ Course: "Test",
+ UserDefinedName: "Test",
+ },
+ },
+ },
+ want: &goics.Component{
+ Tipo: "VCALENDAR",
+ Elements: []goics.Componenter{
+ &goics.Component{
+ Tipo: "VTIMEZONE",
+ Elements: []goics.Componenter{
+ &HtwkalenderComponent{
+ Component: &goics.Component{
+ Tipo: "STANDARD",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTART": {"19701025T030000"},
+ "RRULE": {"FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU"},
+ "TZOFFSETFROM": {"+0200"},
+ "TZOFFSETTO": {"+0100"},
+ "TZNAME": {"CET"},
+ },
+ },
+ },
+ &HtwkalenderComponent{
+ Component: &goics.Component{
+ Tipo: "DAYLIGHT",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTART": {"19700329T020000"},
+ "TZOFFSETFROM": {"+0100"},
+ "TZOFFSETTO": {"+0200"},
+ "TZNAME": {"CEST"},
+ "RRULE": {"FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU"},
+ },
+ },
+ },
+ },
+ Properties: map[string][]string{
+ "TZID": {"EUROPE/BERLIN"},
+ },
+ },
+ &goics.Component{
+ Tipo: "VEVENT",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTAMP": {"20231201T000000Z"},
+ "UID": {"5166fc0abd9d7750077261f1e26a26168d32c88af77198fe83af63e1ba6310dc@htwkalender.de"},
+ "DTEND": {"00010101T000000Z"},
+ "DTSTART": {"00010101T000000Z"},
+ "SUMMARY": {"Test"},
+ "DESCRIPTION": {"Profs: Test\nTyp: Test\nNotizen: Test\n"},
+ "LOCATION": {"Test"},
+ },
+ },
+ },
+ Properties: map[string][]string{
+ "PRODID": {"-//HTWK Kalender//htwkalender.de//DE"},
+ "VERSION": {"2.0"},
+ "CALSCALE": {"GREGORIAN"},
+ "TZID": {"EUROPE/BERLIN"},
+ "X-WR-CALNAME": {"HTWK Kalender"},
+ "X-WR-TIMEZONE": {"EUROPE/BERLIN"},
+ },
+ },
+ },
+ {
+ name: "Test Similar Events like Sport Courses",
+ fields: fields{
+ Events: model.Events{
+ {
+ UUID: "123",
+ Name: "Test",
+ Course: "Test",
+ EventType: "Test",
+ Notes: "Test",
+ Prof: "Test",
+ Rooms: "ZU430",
+ BookedAt: "Test",
+ Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
+ End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 1, 0, 0, 0, time.UTC)),
+ },
+ {
+ UUID: "123",
+ Name: "Test",
+ Course: "Test",
+ EventType: "Test",
+ Notes: "Test",
+ Prof: "Test",
+ Rooms: "ZU221",
+ BookedAt: "Test",
+ Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
+ End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 1, 0, 0, 0, time.UTC)),
+ },
+ },
+ Mapping: map[string]model.FeedCollection{
+ "123": {
+ UUID: "123",
+ Name: "Test",
+ Course: "Test",
+ UserDefinedName: "UserDefinedName",
+ },
+ },
+ },
+ want: &goics.Component{
+ Tipo: "VCALENDAR",
+ Elements: []goics.Componenter{
+ &goics.Component{
+ Tipo: "VTIMEZONE",
+ Elements: []goics.Componenter{
+ &HtwkalenderComponent{
+ Component: &goics.Component{
+ Tipo: "STANDARD",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTART": {"19701025T030000"},
+ "RRULE": {"FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU"},
+ "TZOFFSETFROM": {"+0200"},
+ "TZOFFSETTO": {"+0100"},
+ "TZNAME": {"CET"},
+ },
+ },
+ },
+ &HtwkalenderComponent{
+ Component: &goics.Component{
+ Tipo: "DAYLIGHT",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTART": {"19700329T020000"},
+ "TZOFFSETFROM": {"+0100"},
+ "TZOFFSETTO": {"+0200"},
+ "TZNAME": {"CEST"},
+ "RRULE": {"FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU"},
+ },
+ },
+ },
+ },
+ Properties: map[string][]string{
+ "TZID": {"EUROPE/BERLIN"},
+ },
+ },
+ &goics.Component{
+ Tipo: "VEVENT",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTAMP": {"20231201T000000Z"},
+ "UID": {"2463aac347bca19130d8e579b4b6d89a32c88f7c7e7f858e56477d94b71543a7@htwkalender.de"},
+ "DTEND": {"20231201T010000Z"},
+ "DTSTART": {"20231201T000000Z"},
+ "SUMMARY": {"UserDefinedName"},
+ "DESCRIPTION": {"Profs: Test\nGruppen: Test\nTyp: Test\nNotizen: Test\n"},
+ "LOCATION": {"ZU430"},
+ },
+ },
+ &goics.Component{
+ Tipo: "VEVENT",
+ Elements: []goics.Componenter{},
+ Properties: map[string][]string{
+ "DTSTAMP": {"20231201T000000Z"},
+ "UID": {"ea42fc31835128735636b235be552af559fae5329fe7e501f529130e11a7f3a1@htwkalender.de"},
+ "DTEND": {"20231201T010000Z"},
+ "DTSTART": {"20231201T000000Z"},
+ "SUMMARY": {"UserDefinedName"},
+ "DESCRIPTION": {"Profs: Test\nGruppen: Test\nTyp: Test\nNotizen: Test\n"},
+ "LOCATION": {"ZU221"},
+ },
+ },
+ },
+ Properties: map[string][]string{
+ "PRODID": {"-//HTWK Kalender//htwkalender.de//DE"},
+ "VERSION": {"2.0"},
+ "CALSCALE": {"GREGORIAN"},
+ "TZID": {"EUROPE/BERLIN"},
+ "X-WR-CALNAME": {"HTWK Kalender"},
+ "X-WR-TIMEZONE": {"EUROPE/BERLIN"},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ icalModel := IcalModel{
+ Events: tt.fields.Events,
+ Mapping: tt.fields.Mapping,
+ }
+
+ mockClock := mockTime.MockClock{
+ NowTime: time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC),
+ }
+
+ if got := generateIcalEmit(icalModel, mockClock); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("EmitICal() = \n%v, want \n%v", got, tt.want)
+
+ // Print the differences
+ for i, element := range got.Elements {
+ if !reflect.DeepEqual(element, tt.want.Elements[i]) {
+ t.Errorf("Element %d: got \n%v, want \n%v", i, element, tt.want.Elements[i])
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/services/ical/service/ical/icsComponenter.go b/services/ical/service/ical/icsComponenter.go
new file mode 100644
index 0000000..b8c3deb
--- /dev/null
+++ b/services/ical/service/ical/icsComponenter.go
@@ -0,0 +1,62 @@
+//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 (
+ "github.com/jordic/goics"
+ "sort"
+ "strings"
+)
+
+type HtwkalenderComponent struct {
+ *goics.Component
+}
+
+func NewHtwkalenderComponent() *HtwkalenderComponent {
+ return &HtwkalenderComponent{
+ Component: goics.NewComponent(),
+ }
+}
+
+// Writes the component to the Writer
+func (c *HtwkalenderComponent) Write(w *goics.ICalEncode) {
+ w.WriteLine("BEGIN:" + c.Tipo + goics.CRLF)
+
+ // Iterate over component properties
+ var keys []string
+ for k := range c.Properties {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, key := range keys {
+ vals := c.Properties[key]
+ for _, val := range vals {
+ w.WriteLine(WriteStringField(key, val))
+ }
+ }
+
+ for _, xc := range c.Elements {
+ xc.Write(w)
+ }
+
+ w.WriteLine("END:" + c.Tipo + goics.CRLF)
+}
+
+// WriteStringField UID:asdfasdfаs@dfasdf.com
+func WriteStringField(key string, val string) string {
+ return strings.ToUpper(key) + ":" + (val) + goics.CRLF
+}
diff --git a/services/ical/service/names/userDefinedNameTemplates.go b/services/ical/service/names/userDefinedNameTemplates.go
new file mode 100644
index 0000000..59250ff
--- /dev/null
+++ b/services/ical/service/names/userDefinedNameTemplates.go
@@ -0,0 +1,39 @@
+//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 names
+
+import (
+ "htwkalender-ical/model"
+ "regexp"
+)
+
+func ReplaceTemplateSubStrings(rawString string, event model.Event) string {
+ re := regexp.MustCompile(`\%(.)`)
+
+ return re.ReplaceAllStringFunc(rawString, func(match string) string {
+ switch match {
+ case "%%":
+ return "%"
+ case "%t":
+ return event.EventType
+ case "%p":
+ return event.Compulsory
+ default:
+ return match
+ }
+ })
+}
diff --git a/services/ical/service/names/userDefinedNameTemplates_test.go b/services/ical/service/names/userDefinedNameTemplates_test.go
new file mode 100644
index 0000000..bb5ee2b
--- /dev/null
+++ b/services/ical/service/names/userDefinedNameTemplates_test.go
@@ -0,0 +1,94 @@
+//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 names
+
+import (
+ "htwkalender-ical/model"
+ "testing"
+)
+
+func TestReplaceTemplateSubStrings(t *testing.T) {
+ type args struct {
+ rawString string
+ event model.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "Test 1",
+ args: args{
+ rawString: "%t",
+ event: model.Event{
+ EventType: "Test",
+ },
+ },
+ want: "Test",
+ },
+ {
+ name: "Test 2",
+ args: args{
+ rawString: "%p",
+ event: model.Event{
+ Compulsory: "Test",
+ },
+ },
+ want: "Test",
+ },
+ {
+ name: "Test 3",
+ args: args{
+ rawString: "%%",
+ event: model.Event{
+ EventType: "Test",
+ },
+ },
+ want: "%",
+ },
+ {
+ name: "Test 4",
+ args: args{
+ rawString: "%t %p",
+ event: model.Event{
+ EventType: "Test",
+ Compulsory: "Test",
+ },
+ },
+ want: "Test Test",
+ },
+ {
+ name: "Test 5",
+ args: args{
+ rawString: "%t %p %%",
+ event: model.Event{
+ EventType: "Test",
+ Compulsory: "Test",
+ },
+ },
+ want: "Test Test %",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ReplaceTemplateSubStrings(tt.args.rawString, tt.args.event); got != tt.want {
+ t.Errorf("ReplaceTemplateSubStrings() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/services/ical/service/routes.go b/services/ical/service/routes.go
new file mode 100644
index 0000000..ee59f0f
--- /dev/null
+++ b/services/ical/service/routes.go
@@ -0,0 +1,108 @@
+package service
+
+import (
+ "encoding/json"
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/log"
+ "htwkalender-ical/model"
+ "htwkalender-ical/service/ical"
+ "log/slog"
+ "net/http"
+)
+
+// AddFeedRoutes add routes to the app instance for the data-manager ical service
+// with golang fiber
+func AddFeedRoutes(app *fiber.App) {
+
+ // Define a route for the GET method on the root path '/'
+ app.Get("/api/feed", func(c fiber.Ctx) error {
+
+ token := c.Query("token")
+ results, err := ical.Feed(token)
+
+ if err != nil {
+ slog.Error("Failed to get feed", "error", err)
+ return c.SendStatus(fiber.StatusInternalServerError)
+ }
+ c.Response().Header.Set("Content-type", "text/calendar")
+ c.Response().Header.Set("charset", "utf-8")
+ c.Response().Header.Set("Content-Disposition", "inline")
+ c.Response().Header.Set("filename", "calendar.ics")
+
+ return c.SendString(results)
+ })
+
+ // Define a route for the POST method on the root path '/api/feed'
+ app.Post("/api/feed", func(c fiber.Ctx) error {
+ var modules []model.FeedCollection
+ //obtain the body of the request
+ err := json.Unmarshal(c.Body(), &modules)
+ if err != nil {
+ log.Error("Failed to unmarshal request body", "error", err)
+ return c.SendStatus(fiber.StatusBadRequest)
+ }
+
+ //create a new feed
+ token, err := ical.CreateFeed(modules)
+ if err != nil {
+ println(err)
+ log.Error("Failed to create feed", "error", err)
+ return c.SendStatus(fiber.StatusInternalServerError)
+ }
+
+ return c.JSON(token)
+ })
+
+ // Define a route for the GET method on the root path '/'
+ app.Get("/api/collections/feeds/records/:token", func(c fiber.Ctx) error {
+
+ token := c.Params("token")
+ results, err := ical.FeedRecord(token)
+
+ if err != nil {
+ slog.Error("Failed to get feed", "error", err)
+ return c.SendStatus(fiber.StatusInternalServerError)
+ }
+ c.Response().Header.Set("Content-type", "application/json; charset=UTF-8")
+
+ return c.JSON(results)
+ })
+
+ app.Delete("/api/feed", func(c fiber.Ctx) error {
+
+ token := c.Query("token")
+ err := ical.DeleteFeedRecord(token)
+ if err != nil {
+ slog.Error("Feed could not be deleted", "error", err)
+ return c.JSON(http.StatusNotFound, "Feed could not be deleted")
+ } else {
+ return c.JSON(http.StatusOK, "Feed deleted")
+ }
+ })
+
+ app.Put("/api/feed", func(c fiber.Ctx) error {
+ token := c.Query("token")
+ return c.JSON(http.StatusOK, "token: "+token)
+ })
+
+ app.Head("/api/feed", func(c fiber.Ctx) error {
+ return c.JSON(http.StatusOK, "")
+ })
+
+ app.Add([]string{"PROPFIND"}, "/api/feed", func(c fiber.Ctx) error {
+ return c.JSON(http.StatusOK, "")
+ })
+
+ // Route for Thunderbird to get calendar server information
+ // Response is a 200 OK without additional content
+ app.Add([]string{"PROPFIND"}, "/.well-known/caldav", func(c fiber.Ctx) error {
+ return c.JSON(http.StatusOK, "")
+ })
+
+ // Route for Thunderbird to get calendar server information
+ // Response is a 200 OK without additional content
+ app.Add([]string{"PROPFIND"}, "/", func(c fiber.Ctx) error {
+ return c.JSON(http.StatusOK, "")
+ })
+
+}