diff --git a/addRoute.go b/addRoute.go index 2e037db..f217fa9 100644 --- a/addRoute.go +++ b/addRoute.go @@ -6,6 +6,8 @@ import ( "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "htwk-planner/service" + "htwk-planner/service/fetch" + "htwk-planner/service/ical" "net/http" "os" ) @@ -22,7 +24,7 @@ func addRoutes(app *pocketbase.PocketBase) { Method: http.MethodGet, Path: "/api/fetchPlans", Handler: func(c echo.Context) error { - return service.FetchHTWK(c, app) + return fetch.FetchHTWK(c, app) }, Middlewares: []echo.MiddlewareFunc{ apis.ActivityLogger(app), @@ -39,7 +41,7 @@ func addRoutes(app *pocketbase.PocketBase) { Method: http.MethodGet, Path: "/api/fetchGroups", Handler: func(c echo.Context) error { - return service.FetchSeminarGroups(c, app) + return fetch.SeminarGroups(c, app) }, Middlewares: []echo.MiddlewareFunc{ apis.ActivityLogger(app), @@ -68,4 +70,38 @@ func addRoutes(app *pocketbase.PocketBase) { return nil }) + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + _, err := e.Router.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/api/feedURL", + Handler: func(c echo.Context) error { + return ical.FeedURL(c, app) + }, + 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 { + return ical.Feed(c, app) + }, + Middlewares: []echo.MiddlewareFunc{ + apis.ActivityLogger(app), + }, + }) + if err != nil { + return err + } + return nil + }) + } diff --git a/go.mod b/go.mod index 097fe29..ce19c60 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module htwk-planner go 1.20 require ( + github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0 github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 github.com/pocketbase/pocketbase v0.17.5 golang.org/x/net v0.14.0 diff --git a/go.sum b/go.sum index 04d44b7..4bf93fe 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,7 @@ github.com/ganigeorgiev/fexpr v0.3.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO 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.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 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= @@ -125,6 +126,7 @@ github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1 github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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= @@ -133,12 +135,16 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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= @@ -146,6 +152,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/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.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= diff --git a/model/seminarGroup.go b/model/seminarGroup.go index 014d19e..98a5c58 100644 --- a/model/seminarGroup.go +++ b/model/seminarGroup.go @@ -7,10 +7,10 @@ type SeminarGroup struct { Course string Faculty string FacultyId string - Events []Events + Events []Event } -type Events struct { +type Event struct { Day string Week string Start string @@ -21,4 +21,5 @@ type Events struct { Rooms string Notes string BookedAt string + Semester string } diff --git a/pb_schema.json b/pb_schema.json index 957637a..c009f43 100644 --- a/pb_schema.json +++ b/pb_schema.json @@ -281,6 +281,18 @@ "max": null, "pattern": "" } + }, + { + "id": "vlbpm9fz", + "name": "semester", + "type": "text", + "system": false, + "required": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } } ], "indexes": [ diff --git a/service/date/dateFormat.go b/service/date/dateFormat.go new file mode 100644 index 0000000..41d9a82 --- /dev/null +++ b/service/date/dateFormat.go @@ -0,0 +1,47 @@ +package date + +import "time" + +func GetDateFromWeekNumber(year int, weekNumber int, dayName string) (time.Time, error) { + // Create a time.Date for the first day of the year + firstDayOfYear := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC) + + // Calculate the number of days to add to reach the desired week + daysToAdd := time.Duration((weekNumber-1)*7) * 24 * time.Hour + + // Find the starting day of the week (e.g., Monday) + startingDayOfWeek := firstDayOfYear + + // check if the first day of the year is friday or saturday or sunday + + if startingDayOfWeek.Weekday() == time.Friday || startingDayOfWeek.Weekday() == time.Saturday || startingDayOfWeek.Weekday() == time.Sunday { + for startingDayOfWeek.Weekday() != time.Monday { + startingDayOfWeek = startingDayOfWeek.Add(24 * time.Hour) + } + } else { + for startingDayOfWeek.Weekday() != time.Monday { + startingDayOfWeek = startingDayOfWeek.Add(-24 * time.Hour) + } + } + + // Calculate the desired date by adding daysToAdd and adjusting for the day name + desiredDate := startingDayOfWeek.Add(daysToAdd) + + // Find the day of the week + dayOfWeek := map[string]time.Weekday{ + "Montag": time.Monday, + "Dienstag": time.Tuesday, + "Mittwoch": time.Wednesday, + "Donnerstag": time.Thursday, + "Freitag": time.Friday, + "Samstag": time.Saturday, + "Sonntag": time.Sunday, + }[dayName] + + // Adjust to the desired day of the week + for desiredDate.Weekday() != dayOfWeek { + desiredDate = desiredDate.Add(24 * time.Hour) + } + + return desiredDate, nil +} diff --git a/service/date/dateFormat_test.go b/service/date/dateFormat_test.go new file mode 100644 index 0000000..32fc99f --- /dev/null +++ b/service/date/dateFormat_test.go @@ -0,0 +1,64 @@ +package date + +import ( + "reflect" + "testing" + "time" +) + +func Test_getDateFromWeekNumber(t *testing.T) { + type args struct { + year int + weekNumber int + dayName string + } + tests := []struct { + name string + args args + want time.Time + wantErr bool + }{ + { + name: "Test 1", + args: args{ + year: 2021, + weekNumber: 1, + dayName: "Montag", + }, + want: time.Date(2021, 1, 4, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + { + name: "Test 2", + args: args{ + year: 2023, + weekNumber: 57, + dayName: "Montag", + }, + want: time.Date(2024, 1, 29, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + { + name: "Test 3", + args: args{ + year: 2023, + weekNumber: 1, + dayName: "Montag", + }, + want: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetDateFromWeekNumber(tt.args.year, tt.args.weekNumber, tt.args.dayName) + if (err != nil) != tt.wantErr { + t.Errorf("getDateFromWeekNumber() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getDateFromWeekNumber() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/service/db/dbEvents.go b/service/db/dbEvents.go index 888b1bb..18f8771 100644 --- a/service/db/dbEvents.go +++ b/service/db/dbEvents.go @@ -1,10 +1,13 @@ package db import ( + "github.com/jordic/goics" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/models" "htwk-planner/model" "strings" + "time" ) func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) error { @@ -24,6 +27,7 @@ func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection record.Set("Notes", event.Notes) record.Set("BookedAt", event.BookedAt) record.Set("course", seminarGroup.Course) + record.Set("semester", event.Semester) err = app.Dao().SaveRecord(record) if err != nil { println("Error while saving record: ", err.Error()) @@ -42,7 +46,7 @@ func contains(s []string, e string) bool { return false } -// GetRooms function to get all rooms from database that are stored as a string in the Events struct +// GetRooms function to get all rooms from database that are stored as a string in the Event struct func GetRooms(app *pocketbase.PocketBase) []string { var events []struct { @@ -70,3 +74,39 @@ func GetRooms(app *pocketbase.PocketBase) []string { } return roomArray } + +type Events []*model.Event + +// EmitICal implements the interface for goics +func (e Events) EmitICal() goics.Componenter { + c := goics.NewComponent() + c.SetType("VCALENDAR") + c.AddProperty("CALSCAL", "GREGORIAN") + for _, event := range e { + s := goics.NewComponent() + s.SetType("VEVENT") + timeEnd, _ := time.Parse("2024-01-07 07:00:00 +0000 UTC", event.End) + timeStart, _ := time.Parse("2024-01-07 07:00:00 +0000 UTC", event.Start) + k, v := goics.FormatDateTimeField("DTEND", timeEnd) + s.AddProperty(k, v) + k, v = goics.FormatDateTimeField("DTSTART", timeStart) + s.AddProperty(k, v) + s.AddProperty("SUMMARY", event.Name) + s.AddProperty("DESCRIPTION", event.Notes) + s.AddProperty("LOCATION", event.Rooms) + c.AddComponent(s) + } + return c +} + +// GetPlanForCourseAndSemester gets all events for specific course and semester +func GetPlanForCourseAndSemester(app *pocketbase.PocketBase, course string, semester string) Events { + var events Events + // get all events from event records in the events collection + err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).All(&events) + if err != nil { + print("Error while getting events from database: ", err) + return nil + } + return events +} diff --git a/service/fetchSeminarGroupService.go b/service/fetch/fetchSeminarGroupService.go similarity index 91% rename from service/fetchSeminarGroupService.go rename to service/fetch/fetchSeminarGroupService.go index 1c815b6..ec5c281 100644 --- a/service/fetchSeminarGroupService.go +++ b/service/fetch/fetchSeminarGroupService.go @@ -1,4 +1,4 @@ -package service +package fetch import ( "encoding/xml" @@ -40,7 +40,7 @@ func getSeminarHTML() (string, error) { } -func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { +func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { result, _ := getSeminarHTML() @@ -55,7 +55,7 @@ func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { dbError = db.SaveGroups(groups, collection, app) if dbError != nil { - return apis.NewApiError(400, "Could not save Events into database", dbError) + return apis.NewApiError(400, "Could not save Event into database", dbError) } return c.JSON(http.StatusOK, groups) @@ -64,7 +64,6 @@ func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { func parseSeminarGroups(result string) []model.SeminarGroup { var studium model.Studium - err := xml.Unmarshal([]byte(result), &studium) if err != nil { return nil diff --git a/service/fetchService.go b/service/fetch/fetchService.go similarity index 79% rename from service/fetchService.go rename to service/fetch/fetchService.go index bad24cd..fa5b625 100644 --- a/service/fetchService.go +++ b/service/fetch/fetchService.go @@ -1,4 +1,4 @@ -package service +package fetch import ( "fmt" @@ -8,11 +8,14 @@ import ( "github.com/pocketbase/pocketbase/models" "golang.org/x/net/html" "htwk-planner/model" + "htwk-planner/service/date" "htwk-planner/service/db" "io" "net/http" + "regexp" "strconv" "strings" + "time" ) func FetchHTWK(c echo.Context, app *pocketbase.PocketBase) error { @@ -44,7 +47,7 @@ func FetchHTWK(c echo.Context, app *pocketbase.PocketBase) error { dbError = db.SaveEvents(seminarGroups, collection, app) if dbError != nil { - return apis.NewApiError(400, "Could not save Events into database", dbError) + return apis.NewApiError(400, "Could not save Event into database", dbError) } return c.JSON(http.StatusOK, seminarGroups) @@ -73,24 +76,87 @@ func parseSeminarGroup(result string) model.SeminarGroup { eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels) splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks) events := splitEventsBySingleWeek(splitEventsByWeekVal) + semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data + semester, year := extractSemesterAndYear(semesterString) + events = convertWeeksToDates(events, semester, year) var seminarGroup = model.SeminarGroup{ University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data, Course: findFirstSpanWithClass(table, "header-2-0-1").FirstChild.Data, Events: events, } - return seminarGroup } -func toEvents(tables [][]*html.Node, days []string) []model.Events { - var events []model.Events +func convertWeeksToDates(events []model.Event, semester string, year string) []model.Event { + var newEvents []model.Event + eventYear, _ := strconv.Atoi(year) + + // for each event we need to calculate the start and end date based on the week and the year + for _, event := range events { + eventWeek, _ := strconv.Atoi(event.Week) + eventDay, _ := date.GetDateFromWeekNumber(eventYear, eventWeek, event.Day) + start := addTimeToDate(eventDay, event.Start) + end := addTimeToDate(eventDay, event.End) + newEvent := event + newEvent.Start = start.String() + newEvent.End = end.String() + newEvent.Semester = semester + newEvents = append(newEvents, newEvent) + } + return newEvents +} + +func addTimeToDate(date time.Time, timeString string) time.Time { + //convert time string to time + timeParts := strings.Split(timeString, ":") + hour, _ := strconv.Atoi(timeParts[0]) + minute, _ := strconv.Atoi(timeParts[1]) + + return time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, time.UTC) +} + +func extractSemesterAndYear(semesterString string) (string, string) { + winterPattern := "Wintersemester" + summerPattern := "Sommersemester" + + winterMatch := strings.Contains(semesterString, winterPattern) + summerMatch := strings.Contains(semesterString, summerPattern) + + semester := "" + semesterShortcut := "" + + if winterMatch { + semester = "Wintersemester" + semesterShortcut = "ws" + } else if summerMatch { + semester = "Sommersemester" + semesterShortcut = "ss" + } else { + return "", "" + } + + yearPattern := `\d{4}` + combinedPattern := semester + `\s` + yearPattern + re := regexp.MustCompile(combinedPattern) + match := re.FindString(semesterString) + year := "" + + if match != "" { + reYear := regexp.MustCompile(yearPattern) + year = reYear.FindString(match) + } + return semesterShortcut, year +} + +func toEvents(tables [][]*html.Node, days []string) []model.Event { + var events []model.Event for table := range tables { for row := range tables[table] { tableData := findTableData(tables[table][row]) if len(tableData) > 0 { - events = append(events, model.Events{ + events = append(events, model.Event{ Day: days[table], Week: getTextContent(tableData[0]), Start: getTextContent(tableData[1]), @@ -110,8 +176,8 @@ func toEvents(tables [][]*html.Node, days []string) []model.Events { return events } -func splitEventsByWeek(events []model.Events) []model.Events { - var newEvents []model.Events +func splitEventsByWeek(events []model.Event) []model.Event { + var newEvents []model.Event for _, event := range events { weeks := strings.Split(event.Week, ",") @@ -124,8 +190,8 @@ func splitEventsByWeek(events []model.Events) []model.Events { return newEvents } -func splitEventsBySingleWeek(events []model.Events) []model.Events { - var newEvents []model.Events +func splitEventsBySingleWeek(events []model.Event) []model.Event { + var newEvents []model.Event for _, event := range events { if strings.Contains(event.Week, "-") { diff --git a/service/fetch/fetchService_test.go b/service/fetch/fetchService_test.go new file mode 100644 index 0000000..ecbe082 --- /dev/null +++ b/service/fetch/fetchService_test.go @@ -0,0 +1,52 @@ +package fetch + +import "testing" + +func Test_extractSemesterAndYear(t *testing.T) { + type args struct { + semesterString string + } + tests := []struct { + name string + args args + want string + want1 string + }{ + // TODO: Add test cases. + { + name: "Test 1", + args: args{ + semesterString: "Wintersemester 2023/24 (Planungszeitraum 01.09.2023 bis 03.03.2024)", + }, + want: "ws", + want1: "2023", + }, + { + name: "Test 2", + args: args{ + semesterString: "Sommersemester 2023 (Planungszeitraum 06.03. bis 31.08.2023)", + }, + want: "ss", + want1: "2023", + }, + { + name: "Test 3", + args: args{ + semesterString: "Sommersemester 2010 (Planungszeitraum 06.03. bis 31.08.2023)", + }, + want: "ss", + want1: "2010", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := extractSemesterAndYear(tt.args.semesterString) + if got != tt.want { + t.Errorf("extractSemesterAndYear() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("extractSemesterAndYear() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/service/ical/ical.go b/service/ical/ical.go new file mode 100644 index 0000000..bf9cc94 --- /dev/null +++ b/service/ical/ical.go @@ -0,0 +1,79 @@ +package ical + +import ( + "bytes" + "crypto/rand" + "fmt" + "github.com/jordic/goics" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase" + "htwk-planner/service/db" + "log" + "net/http" + "time" +) + +const expirationTime = 5 * time.Minute + +var cache = make(map[string]*FeedModel) + +func FeedURL(c echo.Context, app *pocketbase.PocketBase) error { + token := randomToken(20) + _, err := createFeedForToken(app, token) + if err != nil { + return err + } + return c.JSON(http.StatusOK, fmt.Sprintf("FeedToken: %s", token)) +} + +func Feed(c echo.Context, app *pocketbase.PocketBase) error { + + var result string + var responseWriter http.ResponseWriter + token := c.Request().Header.Get("token") + log.Print("iCal feed Token: " + token) + feed, ok := cache[token] + if !ok || feed == nil { + return c.JSON(http.StatusNotFound, "No FeedModel for this Token") + } + + result = feed.Content + if feed.ExpiresAt.Before(time.Now()) { + newFeed, err := createFeedForToken(app, token) + if err != nil { + return c.JSON(http.StatusInternalServerError, err) + } + result = newFeed.Content + } + + responseWriter.Header().Set("Content-type", "text/calendar") + responseWriter.Header().Set("charset", "utf-8") + responseWriter.Header().Set("Content-Disposition", "inline") + responseWriter.Header().Set("filename", "calendar.ics") + writeSuccess(result, responseWriter) + c.Response().Writer = responseWriter + return nil +} + +func createFeedForToken(app *pocketbase.PocketBase, token string) (*FeedModel, error) { + res := db.GetPlanForCourseAndSemester(app, "22INM", "ws") + b := bytes.Buffer{} + goics.NewICalEncode(&b).Encode(res) + feed := &FeedModel{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)} + cache[token] = feed + return feed, nil +} + +func randomToken(len int) string { + b := make([]byte, len) + read, err := rand.Read(b) + if err != nil { + return "" + } + return fmt.Sprintf("%x", read) +} + +func writeSuccess(message string, w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(message)) +} diff --git a/service/ical/icalModel.go b/service/ical/icalModel.go new file mode 100644 index 0000000..a44983c --- /dev/null +++ b/service/ical/icalModel.go @@ -0,0 +1,24 @@ +package ical + +import ( + "htwk-planner/model" + "time" +) + +// FeedModel is an iCal feed +type FeedModel struct { + Content string + ExpiresAt time.Time +} + +// Entry is a time entry +type Entry struct { + DateStart time.Time `json:"dateStart"` + DateEnd time.Time `json:"dateEnd"` + Description string `json:"description"` +} + +// Entries is a collection of entries +type Entries []*Entry + +type Events []*model.Event