websocket events for live editing

main
alex 2024-09-08 19:29:03 +02:00
parent 4948c72882
commit 63c8702899
5 changed files with 216 additions and 24 deletions

4
go.mod
View File

@ -4,7 +4,9 @@ go 1.21.0
require ( require (
git.ex.umbach.dev/Alex/roese-utils v1.0.21 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/fiber/v2 v2.52.5
github.com/gofiber/websocket/v2 v2.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
@ -14,7 +16,6 @@ require (
) )
require ( require (
git.ex.umbach.dev/LMS/libcore v1.0.6 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/fasthttp/websocket v1.5.3 // indirect github.com/fasthttp/websocket v1.5.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.5 // indirect github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/go-sql-driver/mysql v1.7.0 // 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/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/compress v1.17.0 // indirect

6
go.sum
View File

@ -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 h1:ae1AHQh8UHJVLbpk5Gf4S5IP0EEWGD1JFIeX9cIcYcc=
git.ex.umbach.dev/Alex/roese-utils v1.0.21/go.mod h1:hFcnKQl6nuGFEMCxK/eVQBUD6ixBFlAaiy4E2aQqUL8= 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 h1:Af+2jD4aC3+4qMgSmTn4nvRosAS5ETTX9JR3gznlGPI=
git.ex.umbach.dev/LMS/libcore v1.0.6/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= git.ex.umbach.dev/LMS/libcore v1.0.6/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=

View File

@ -87,6 +87,15 @@ const (
SendCmdTeamAddedMember = 5 SendCmdTeamAddedMember = 5
SendCmdTeamUpdatedMemberRole = 6 SendCmdTeamUpdatedMemberRole = 6
SendCmdTeamDeletedMember = 7 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 // commands received from websocket clients
@ -101,3 +110,11 @@ const (
SubscribedTopicSettings = "/settings" SubscribedTopicSettings = "/settings"
SubscribedTopicAccount = "/account" SubscribedTopicAccount = "/account"
) )
func SubscribedTopicLessonsId(organizationId string) string {
return SubscribedTopicLessons + "/" + organizationId
}
func SubscribedTopicLessonsEditorId(organizationId string) string {
return SubscribedTopicLessons + "/" + organizationId + "/editor"
}

View File

@ -1,6 +1,7 @@
package lessons package lessons
import ( import (
"errors"
"sort" "sort"
"strings" "strings"
@ -11,6 +12,7 @@ import (
"lms.de/backend/modules/database" "lms.de/backend/modules/database"
"lms.de/backend/modules/structs" "lms.de/backend/modules/structs"
"lms.de/backend/modules/utils" "lms.de/backend/modules/utils"
"lms.de/backend/socketclients"
) )
func GetLessons(c *fiber.Ctx) error { func GetLessons(c *fiber.Ctx) error {
@ -68,6 +70,16 @@ func CreateLesson(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusInternalServerError) 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( return c.JSON(
structs.CreateLessonResponse{ structs.CreateLessonResponse{
Id: lesson.Id, Id: lesson.Id,
@ -192,10 +204,26 @@ func UpdateLessonPreviewTitle(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusBadRequest) 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) 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( return c.JSON(
fiber.Map{ fiber.Map{
"message": "Lesson preview updated successfully", "message": "Lesson preview updated successfully",
@ -250,7 +278,7 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error {
lesson := models.Lesson{} 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) return c.SendStatus(fiber.StatusInternalServerError)
} }
@ -266,10 +294,27 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error {
utils.CreateFolderStructureIfNotExists(publicPath) 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) 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) return c.SaveFile(fileHeader, publicPath+fileName)
} }
@ -308,10 +353,29 @@ func UpdateLessonState(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusBadRequest) 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) 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( return c.JSON(
fiber.Map{ fiber.Map{
"message": "Lesson state updated successfully", "message": "Lesson state updated successfully",
@ -360,8 +424,13 @@ func AddLessonContent(c *fiber.Ctx) error {
var lastContent models.LessonContent 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 { if err := database.DB.Select("position").Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent).Error; err != nil {
return c.SendStatus(fiber.StatusInternalServerError) if !errors.Is(err, gorm.ErrRecordNotFound) {
return c.SendStatus(fiber.StatusNotFound)
}
// no content found, set position to 1
lastContent.Position = 1
} }
// create new content // create new content
@ -375,10 +444,20 @@ func AddLessonContent(c *fiber.Ctx) error {
Data: body.Data, 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) 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( return c.JSON(
structs.AddLessonContentResponse{ structs.AddLessonContentResponse{
Id: content.Id, Id: content.Id,
@ -449,7 +528,7 @@ func UploadLessonContentFile(c *fiber.Ctx) error {
content := models.LessonContent{} 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) 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{ return c.JSON(fiber.Map{
"Data": databasePath + fileName, "Data": databasePath + fileName,
}) })
@ -523,6 +620,24 @@ func UpdateLessonContent(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusInternalServerError) 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( return c.JSON(
fiber.Map{ fiber.Map{
"message": "Lesson content updated successfully", "message": "Lesson content updated successfully",
@ -624,6 +739,24 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusInternalServerError) 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( return c.JSON(
fiber.Map{ fiber.Map{
"message": "Lesson content position updated successfully", "message": "Lesson content position updated successfully",
@ -692,6 +825,22 @@ func DeleteLessonContent(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusInternalServerError) 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( return c.JSON(
fiber.Map{ fiber.Map{
"message": "Lesson content deleted successfully", "message": "Lesson content deleted successfully",

View File

@ -33,12 +33,44 @@ func BroadcastMessageExceptBrowserTabSession(organizationId string, browserTabSe
func BroadcastMessageToTopicExceptBrowserTabSession(organizationId string, topic string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { func BroadcastMessageToTopicExceptBrowserTabSession(organizationId string, topic string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) {
for _, client := range cache.GetSocketClients() { 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) 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 { func hasClientSubscribedToTopic(topic string, clientTopic string) bool {
return clientTopic == topic || strings.HasPrefix(clientTopic, topic) return clientTopic == topic || strings.HasPrefix(clientTopic, topic)
} }