diff --git a/addRoute.go b/addRoute.go index 620ae8b..9510a66 100644 --- a/addRoute.go +++ b/addRoute.go @@ -73,12 +73,10 @@ func addRoutes(app *pocketbase.PocketBase) { app.OnBeforeServe().Add(func(e *core.ServeEvent) error { _, err := e.Router.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/api/feedURL", + Method: http.MethodPost, + Path: "/api/createFeed", Handler: func(c echo.Context) error { - course := c.QueryParam("course") - semester := c.QueryParam("semester") - return ical.FeedURL(c, app, course, semester) + return ical.CreateIndividualFeed(c, app) }, Middlewares: []echo.MiddlewareFunc{ apis.ActivityLogger(app), @@ -111,7 +109,7 @@ func addRoutes(app *pocketbase.PocketBase) { app.OnBeforeServe().Add(func(e *core.ServeEvent) error { _, err := e.Router.AddRoute(echo.Route{ Method: http.MethodGet, - Path: "/api/modules", + Path: "/api/course/modules", Handler: func(c echo.Context) error { course := c.QueryParam("course") semester := c.QueryParam("semester") @@ -127,6 +125,23 @@ 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/modules", + Handler: func(c echo.Context) error { + return events.GetAllModulesDistinct(app, c) + }, + 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, diff --git a/model/feedModel.go b/model/feedModel.go new file mode 100644 index 0000000..0f2cb35 --- /dev/null +++ b/model/feedModel.go @@ -0,0 +1,8 @@ +package model + +type Feed struct { + Id string `db:"id" json:"id"` + Modules string `db:"modules" json:"modules"` + Created string `db:"created" json:"created"` + Updated string `db:"updated" json:"updated"` +} diff --git a/pb_schema.json b/pb_schema.json index c009f43..358691b 100644 --- a/pb_schema.json +++ b/pb_schema.json @@ -304,5 +304,28 @@ "updateRule": null, "deleteRule": null, "options": {} + }, + { + "id": "d65h4wh7zk13gxp", + "name": "feeds", + "type": "base", + "system": false, + "schema": [ + { + "id": "cowxjfmc", + "name": "modules", + "type": "json", + "system": false, + "required": true, + "options": {} + } + ], + "indexes": [], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} } ] \ No newline at end of file diff --git a/service/db/dbEvents.go b/service/db/dbEvents.go index 037d653..f74f1a2 100644 --- a/service/db/dbEvents.go +++ b/service/db/dbEvents.go @@ -101,7 +101,7 @@ func (e Events) EmitICal() goics.Componenter { return c } -// GetPlanForCourseAndSemester gets all events for specific course and semester +// gets all events for specific course and semester // TODO add filter for year func GetPlanForCourseAndSemester(app *pocketbase.PocketBase, course string, semester string) Events { var events Events @@ -114,6 +114,29 @@ func GetPlanForCourseAndSemester(app *pocketbase.PocketBase, course string, seme return events } +func GetPlanForModules(app *pocketbase.PocketBase, modules []string) Events { + + // build query string with name equals elements in modules for dbx query + + var queryString string + for i, module := range modules { + if i == 0 { + queryString = "Name = '" + module + "'" + } else { + queryString = queryString + " OR Name = '" + module + "'" + } + } + + var events Events + // get all events from event records in the events collection + err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp(queryString)).All(&events) + if err != nil { + print("Error while getting events from database: ", err) + return nil + } + return events +} + func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) ([]string, error) { var events []struct { Name string `db:"Name" json:"Name"` @@ -133,3 +156,32 @@ func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester } return eventArray, nil } + +func GetAllModulesDistinct(app *pocketbase.PocketBase) ([]struct { + Name string + Course string +}, error) { + var events []struct { + Name string `db:"Name" json:"Name"` + Course string `db:"course" json:"course"` + } + + var eventArray []struct { + Name string + Course string + } + + err := app.Dao().DB().Select("Name", "course").From("events").Distinct(true).All(&events) + if err != nil { + print("Error while getting events from database: ", err) + return eventArray, err + } + + for _, event := range events { + eventArray = append(eventArray, struct { + Name string + Course string + }{event.Name, event.Course}) + } + return eventArray, nil +} diff --git a/service/db/dbFeeds.go b/service/db/dbFeeds.go new file mode 100644 index 0000000..09227e4 --- /dev/null +++ b/service/db/dbFeeds.go @@ -0,0 +1,28 @@ +package db + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/models" + "htwk-planner/model" +) + +func SaveFeed(feed model.Feed, collection *models.Collection, app *pocketbase.PocketBase) (*models.Record, error) { + record := models.NewRecord(collection) + record.Set("modules", feed.Modules) + err := app.Dao().SaveRecord(record) + + if err != nil { + return nil, err + } + return record, nil +} + +func FindFeedByToken(token string, app *pocketbase.PocketBase) (*model.Feed, error) { + var feed model.Feed + err := app.Dao().DB().Select("*").From("feeds").Where(dbx.NewExp("id = {:id}", dbx.Params{"id": token})).One(&feed) + if err != nil { + return nil, err + } + return &feed, err +} diff --git a/service/db/dbFunctions.go b/service/db/dbFunctions.go new file mode 100644 index 0000000..a8366db --- /dev/null +++ b/service/db/dbFunctions.go @@ -0,0 +1,11 @@ +package db + +import ( + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/models" +) + +func FindCollection(app *pocketbase.PocketBase, collectionName string) (*models.Collection, error) { + collection, dbError := app.Dao().FindCollectionByNameOrId(collectionName) + return collection, dbError +} diff --git a/service/events/eventService.go b/service/events/eventService.go index 8b993f2..057e9a7 100644 --- a/service/events/eventService.go +++ b/service/events/eventService.go @@ -10,21 +10,32 @@ import ( func GetModulesForCourseDistinct(app *pocketbase.PocketBase, c echo.Context, course string, semester string) error { modules, err := db.GetAllModulesForCourse(app, course, semester) - replaceEmptyEntry(modules, "Sonderveranstaltungen") + replaceEmptyEntryInStringArray(modules, "Sonderveranstaltungen") if err != nil { return c.JSON(400, err) } else { return c.JSON(200, modules) } - } -func replaceEmptyEntry(courses []string, replacement string) { +func replaceEmptyEntryInStringArray(modules []string, replacement string) { //replace empty string with "Sonderveranstaltungen" - for i, course := range courses { - if checkIfOnlyWhitespace(course) { - courses[i] = replacement + for i, module := range modules { + if checkIfOnlyWhitespace(module) { + modules[i] = replacement + } + } +} + +func replaceEmptyEntry(modules []struct { + Name string + Course string +}, replacement string) { + //replace empty string with "Sonderveranstaltungen" + for i, module := range modules { + if checkIfOnlyWhitespace(module.Name) { + modules[i].Name = replacement } } } @@ -38,3 +49,15 @@ func checkIfOnlyWhitespace(word string) bool { } return true } + +func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error { + modules, err := db.GetAllModulesDistinct(app) + + replaceEmptyEntry(modules, "Sonderveranstaltungen") + + if err != nil { + return c.JSON(400, err) + } else { + return c.JSON(200, modules) + } +} diff --git a/service/fetch/fetchSeminarEventService.go b/service/fetch/fetchSeminarEventService.go index 6df31e7..9cad524 100644 --- a/service/fetch/fetchSeminarEventService.go +++ b/service/fetch/fetchSeminarEventService.go @@ -5,7 +5,6 @@ import ( "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" - "github.com/pocketbase/pocketbase/models" "golang.org/x/net/html" "htwk-planner/model" "htwk-planner/service/date" @@ -24,7 +23,7 @@ func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error { seminarGroups := GetSeminarGroupsEventsFromHTML(seminarGroupsLabel) - collection, dbError := findCollection(app, "events") + collection, dbError := db.FindCollection(app, "events") if dbError != nil { return apis.NewNotFoundError("Collection not found", dbError) } @@ -56,11 +55,6 @@ func GetSeminarGroupsEventsFromHTML(seminarGroupsLabel []string) []model.Seminar return seminarGroups } -func findCollection(app *pocketbase.PocketBase, collectionName string) (*models.Collection, error) { - collection, dbError := app.Dao().FindCollectionByNameOrId(collectionName) - return collection, dbError -} - func parseSeminarGroup(result string) model.SeminarGroup { doc, err := html.Parse(strings.NewReader(result)) if err != nil { diff --git a/service/fetch/fetchSeminarGroupService.go b/service/fetch/fetchSeminarGroupService.go index 41b85c2..fa180ca 100644 --- a/service/fetch/fetchSeminarGroupService.go +++ b/service/fetch/fetchSeminarGroupService.go @@ -12,8 +12,8 @@ import ( "net/http" ) -func getSeminarHTML() (string, error) { - url := "https://stundenplan.htwk-leipzig.de/stundenplan/xml/public/semgrp_ss.xml" +func getSeminarHTML(semester string) (string, error) { + url := "https://stundenplan.htwk-leipzig.de/stundenplan/xml/public/semgrp_" + semester + ".xml" // Send GET request response, err := http.Get(url) @@ -41,14 +41,18 @@ func getSeminarHTML() (string, error) { } func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { - - result, _ := getSeminarHTML() - var groups []model.SeminarGroup - groups = parseSeminarGroups(result) + resultSummer, _ := getSeminarHTML("ss") + resultWinter, _ := getSeminarHTML("ws") - collection, dbError := findCollection(app, "groups") + groups = parseSeminarGroups(resultSummer) + groups = append(groups, parseSeminarGroups(resultWinter)...) + + // filter duplicates + groups = removeDuplicates(groups) + + collection, dbError := db.FindCollection(app, "groups") if dbError != nil { return apis.NewNotFoundError("Collection not found", dbError) } @@ -61,6 +65,25 @@ func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error { return c.JSON(http.StatusOK, groups) } +func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup { + var uniqueGroups []model.SeminarGroup + for _, group := range groups { + if !contains(uniqueGroups, group) { + uniqueGroups = append(uniqueGroups, group) + } + } + return uniqueGroups +} + +func contains(groups []model.SeminarGroup, group model.SeminarGroup) bool { + for _, a := range groups { + if a.Course == group.Course { + return true + } + } + return false +} + func parseSeminarGroups(result string) []model.SeminarGroup { var studium model.Studium diff --git a/service/ical/ical.go b/service/ical/ical.go index a5a39f1..051a584 100644 --- a/service/ical/ical.go +++ b/service/ical/ical.go @@ -3,41 +3,37 @@ package ical import ( "bytes" "crypto/rand" + "encoding/json" "fmt" "github.com/jordic/goics" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" "htwk-planner/model" "htwk-planner/service/db" + "io" "net/http" "time" ) const expirationTime = 5 * time.Minute -var cache = make(map[string]*model.FeedModel) - -func FeedURL(c echo.Context, app *pocketbase.PocketBase, course string, semester string) error { - token := randomToken(20) - _, err := createFeedForToken(app, token, course, semester) - if err != nil { - return err - } - return c.JSON(http.StatusOK, fmt.Sprintf("FeedToken: %s, Course: %s, Semester: %s", token, course, semester)) -} - func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error { + layout := "2006-01-02 15:04:05 -0700 MST" var result string var responseWriter = c.Response().Writer - feed, ok := cache[token] - if !ok || feed == nil { - return c.JSON(http.StatusNotFound, "No FeedModel for this Token") + feed, err := db.FindFeedByToken(token, app) + if feed == nil && err != nil { + return c.JSON(http.StatusNotFound, err) } - result = feed.Content - if feed.ExpiresAt.Before(time.Now()) { - newFeed, err := createFeedForToken(app, token, feed.Course, feed.Semester) + created, _ := time.Parse(layout, feed.Created) + + var modules []string + _ = json.Unmarshal([]byte(feed.Modules), &modules) + if created.Add(time.Hour * 265).Before(time.Now()) { + newFeed, err := createFeedForToken(app, modules) if err != nil { return c.JSON(http.StatusInternalServerError, err) } @@ -53,12 +49,11 @@ func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error { return nil } -func createFeedForToken(app *pocketbase.PocketBase, token string, course string, semester string) (*model.FeedModel, error) { - res := db.GetPlanForCourseAndSemester(app, course, semester) +func createFeedForToken(app *pocketbase.PocketBase, modules []string) (*model.FeedModel, error) { + res := db.GetPlanForModules(app, modules) b := bytes.Buffer{} goics.NewICalEncode(&b).Encode(res) feed := &model.FeedModel{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)} - cache[token] = feed return feed, nil } @@ -75,3 +70,34 @@ func writeSuccess(message string, w http.ResponseWriter) { w.WriteHeader(http.StatusOK) w.Write([]byte(message)) } + +func CreateIndividualFeed(c echo.Context, app *pocketbase.PocketBase) error { + + // read json from request body + var modules []string + requestBodyBytes, err := io.ReadAll(c.Request().Body) + if err != nil { + return apis.NewApiError(400, "Could not bind request body", err) + } + + err = json.Unmarshal(requestBodyBytes, &modules) + if err != nil { + return apis.NewApiError(400, "Could not bind request body", err) + } + + var feed model.Feed + jsonModules, _ := json.Marshal(modules) + feed.Modules = string(jsonModules) + + collection, dbError := db.FindCollection(app, "feeds") + if dbError != nil { + return apis.NewNotFoundError("Collection not found", dbError) + } + + record, err := db.SaveFeed(feed, collection, app) + if err != nil { + return apis.NewNotFoundError("Feed could not be saved", dbError) + } + + return c.JSON(http.StatusOK, record.Id) +}