diff --git a/go.mod b/go.mod index 3fc023e..4cce330 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.21.0 require ( git.ex.umbach.dev/Alex/roese-utils v1.0.21 + git.ex.umbach.dev/LMS/libcore v1.0.6 github.com/gofiber/fiber/v2 v2.52.5 + github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.31.0 @@ -14,7 +16,6 @@ require ( ) require ( - git.ex.umbach.dev/LMS/libcore v1.0.6 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/fasthttp/websocket v1.5.3 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -22,7 +23,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.15.5 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/gofiber/websocket/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.0 // indirect diff --git a/go.sum b/go.sum index 257c870..653fa5c 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ git.ex.umbach.dev/Alex/roese-utils v1.0.21 h1:ae1AHQh8UHJVLbpk5Gf4S5IP0EEWGD1JFIeX9cIcYcc= git.ex.umbach.dev/Alex/roese-utils v1.0.21/go.mod h1:hFcnKQl6nuGFEMCxK/eVQBUD6ixBFlAaiy4E2aQqUL8= -git.ex.umbach.dev/LMS/libcore v1.0.0 h1:0vhIxeFNHdo4ftVHEGxZ35Mf0jkRuSs9QkfelWFDySc= -git.ex.umbach.dev/LMS/libcore v1.0.0/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= -git.ex.umbach.dev/LMS/libcore v1.0.1 h1:J9sRarL6OJ4JOaE8UH1yykOYdjq5H6TehMDq86269iA= -git.ex.umbach.dev/LMS/libcore v1.0.1/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= -git.ex.umbach.dev/LMS/libcore v1.0.2 h1:RvE0e+Eja/9HOU5reEG2UU18mgDgKYD8gXJOrOntRmc= -git.ex.umbach.dev/LMS/libcore v1.0.2/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= git.ex.umbach.dev/LMS/libcore v1.0.6 h1:Af+2jD4aC3+4qMgSmTn4nvRosAS5ETTX9JR3gznlGPI= git.ex.umbach.dev/LMS/libcore v1.0.6/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= diff --git a/modules/utils/globals.go b/modules/utils/globals.go index 113e94b..7f109a4 100644 --- a/modules/utils/globals.go +++ b/modules/utils/globals.go @@ -80,13 +80,22 @@ var Roles = map[string][]uint16{ // commands sent to websocket clients const ( - SendCmdSettingsUpdated = 1 - SendCmdSettingsUpdatedLogo = 2 - SendCmdSettingsUpdatedBanner = 3 - SendCmdSettingsUpdatedSubdomain = 4 - SendCmdTeamAddedMember = 5 - SendCmdTeamUpdatedMemberRole = 6 - SendCmdTeamDeletedMember = 7 + SendCmdSettingsUpdated = 1 + SendCmdSettingsUpdatedLogo = 2 + SendCmdSettingsUpdatedBanner = 3 + SendCmdSettingsUpdatedSubdomain = 4 + SendCmdTeamAddedMember = 5 + SendCmdTeamUpdatedMemberRole = 6 + SendCmdTeamDeletedMember = 7 + SendCmdLessonCreated = 8 + SendCmdLessonPreviewTitleUpdated = 9 + SendCmdLessonPreviewThumbnailUpdated = 10 + SendCmdLessonStateUpdated = 11 + SendCmdLessonAddedContent = 12 + SendCmdLessonDeletedContent = 13 + SendCmdLessonContentUpdated = 14 + SendCmdLessonContentUpdatedPosition = 15 + SendCmdLessonContentFileUpdated = 16 ) // commands received from websocket clients @@ -101,3 +110,11 @@ const ( SubscribedTopicSettings = "/settings" SubscribedTopicAccount = "/account" ) + +func SubscribedTopicLessonsId(organizationId string) string { + return SubscribedTopicLessons + "/" + organizationId +} + +func SubscribedTopicLessonsEditorId(organizationId string) string { + return SubscribedTopicLessons + "/" + organizationId + "/editor" +} diff --git a/routers/router/api/v1/lessons/lessons.go b/routers/router/api/v1/lessons/lessons.go index bf5a2a1..2cbc8ed 100644 --- a/routers/router/api/v1/lessons/lessons.go +++ b/routers/router/api/v1/lessons/lessons.go @@ -1,6 +1,7 @@ package lessons import ( + "errors" "sort" "strings" @@ -11,6 +12,7 @@ import ( "lms.de/backend/modules/database" "lms.de/backend/modules/structs" "lms.de/backend/modules/utils" + "lms.de/backend/socketclients" ) func GetLessons(c *fiber.Ctx) error { @@ -68,6 +70,16 @@ func CreateLesson(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessons, + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonCreated, + Body: lesson, + }, + ) + return c.JSON( structs.CreateLessonResponse{ Id: lesson.Id, @@ -192,10 +204,26 @@ func UpdateLessonPreviewTitle(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title); err != nil { + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicsExceptBrowserTabSession( + c.Locals("organizationId").(string), + []string{utils.SubscribedTopicLessons, utils.SubscribedTopicLessonsEditorId(params.LessonId)}, + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonPreviewTitleUpdated, + Body: struct { + LessonId string + Title string + }{ + LessonId: params.LessonId, + Title: body.Title, + }, + }, + ) + return c.JSON( fiber.Map{ "message": "Lesson preview updated successfully", @@ -250,7 +278,7 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { lesson := models.Lesson{} - if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).First(&lesson); err != nil { + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).First(&lesson).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } @@ -266,10 +294,27 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { utils.CreateFolderStructureIfNotExists(publicPath) - if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", databasePath+fileName); err != nil { + thumbnailUrl := databasePath + fileName + + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", thumbnailUrl).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopics( + c.Locals("organizationId").(string), + []string{utils.SubscribedTopicLessons, utils.SubscribedTopicLessonsEditorId(params.LessonId)}, + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonPreviewThumbnailUpdated, + Body: struct { + LessonId string + ThumbnailUrl string + }{ + LessonId: params.LessonId, + ThumbnailUrl: thumbnailUrl, + }, + }, + ) + return c.SaveFile(fileHeader, publicPath+fileName) } @@ -308,10 +353,29 @@ func UpdateLessonState(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("state", body.State); err != nil { + if err := database.DB.Model(&models.Lesson{}). + Where("id = ?", params.LessonId). + Where("organization_id = ?", c.Locals("organizationId").(string)). + Update("state", body.State).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonStateUpdated, + Body: struct { + LessonId string + State uint8 + }{ + LessonId: params.LessonId, + State: body.State, + }, + }, + ) + return c.JSON( fiber.Map{ "message": "Lesson state updated successfully", @@ -360,8 +424,13 @@ func AddLessonContent(c *fiber.Ctx) error { var lastContent models.LessonContent - if err := database.DB.Select("position").Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent); err != nil { - return c.SendStatus(fiber.StatusInternalServerError) + if err := database.DB.Select("position").Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + + // no content found, set position to 1 + lastContent.Position = 1 } // create new content @@ -375,10 +444,20 @@ func AddLessonContent(c *fiber.Ctx) error { Data: body.Data, } - if err := database.DB.Create(&content); err != nil { + if err := database.DB.Create(&content).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonAddedContent, + Body: content, + }, + ) + return c.JSON( structs.AddLessonContentResponse{ Id: content.Id, @@ -449,7 +528,7 @@ func UploadLessonContentFile(c *fiber.Ctx) error { content := models.LessonContent{} - if err := database.DB.Select("type", "data").Model(&models.LessonContent{}).Where("id = ?", params.ContentId).First(&content); err != nil { + if err := database.DB.Select("type", "data").Model(&models.LessonContent{}).Where("id = ?", params.ContentId).First(&content).Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } @@ -475,6 +554,24 @@ func UploadLessonContentFile(c *fiber.Ctx) error { }) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonContentFileUpdated, + Body: struct { + ContentId string + LessonId string + Data string + }{ + ContentId: params.ContentId, + LessonId: params.LessonId, + Data: databasePath + fileName, + }, + }, + ) + return c.JSON(fiber.Map{ "Data": databasePath + fileName, }) @@ -523,6 +620,24 @@ func UpdateLessonContent(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonContentUpdated, + Body: struct { + ContentId string + LessonId string + Data string + }{ + ContentId: params.ContentId, + LessonId: params.LessonId, + Data: body.Data, + }, + }, + ) + return c.JSON( fiber.Map{ "message": "Lesson content updated successfully", @@ -624,6 +739,24 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonContentUpdatedPosition, + Body: struct { + ContentId string + LessonId string + Position uint16 + }{ + ContentId: params.ContentId, + LessonId: params.LessonId, + Position: body.Position, + }, + }, + ) + return c.JSON( fiber.Map{ "message": "Lesson content position updated successfully", @@ -692,6 +825,22 @@ func DeleteLessonContent(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } + socketclients.BroadcastMessageToTopicStartsWithExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicLessonsId(params.LessonId), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdLessonDeletedContent, + Body: struct { + ContentId string + LessonId string + }{ + ContentId: params.ContentId, + LessonId: params.LessonId, + }, + }, + ) + return c.JSON( fiber.Map{ "message": "Lesson content deleted successfully", diff --git a/socketclients/socketclients.go b/socketclients/socketclients.go index 44105f5..c7fea33 100644 --- a/socketclients/socketclients.go +++ b/socketclients/socketclients.go @@ -33,12 +33,44 @@ func BroadcastMessageExceptBrowserTabSession(organizationId string, browserTabSe func BroadcastMessageToTopicExceptBrowserTabSession(organizationId string, topic string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { for _, client := range cache.GetSocketClients() { - if hasClientSubscribedToTopic(topic, client.SubscribedTopic) && client.BrowserTabSession != browserTabSession && client.OrganizationId == organizationId { + if client.SubscribedTopic == topic && client.BrowserTabSession != browserTabSession && client.OrganizationId == organizationId { client.SendMessage(sendSocketMessage) } } } +func BroadcastMessageToTopicStartsWithExceptBrowserTabSession(organizationId string, topic string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if strings.HasPrefix(client.SubscribedTopic, topic) && client.BrowserTabSession != browserTabSession && client.OrganizationId == organizationId { + client.SendMessage(sendSocketMessage) + } + } +} + +func BroadcastMessageToTopicsExceptBrowserTabSession(organizationId string, topics []string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if client.OrganizationId == organizationId && client.BrowserTabSession != browserTabSession { + for _, topic := range topics { + if client.SubscribedTopic == topic { + client.SendMessage(sendSocketMessage) + } + } + } + } +} + +func BroadcastMessageToTopics(organizationId string, topics []string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if client.OrganizationId == organizationId { + for _, topic := range topics { + if client.SubscribedTopic == topic { + client.SendMessage(sendSocketMessage) + } + } + } + } +} + func hasClientSubscribedToTopic(topic string, clientTopic string) bool { return clientTopic == topic || strings.HasPrefix(clientTopic, topic) }