From 6d4277bcdae9b39abf794322b99b3a3b2c75f351 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 8 Oct 2024 16:44:56 +0200 Subject: [PATCH 01/42] Added additional management page --- web/admin.go | 20 +- .../admin/lecture-live-management.gohtml | 560 +++++++++++++----- 2 files changed, 440 insertions(+), 140 deletions(-) diff --git a/web/admin.go b/web/admin.go index 4a4afc537..3c53347d6 100644 --- a/web/admin.go +++ b/web/admin.go @@ -5,9 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "strings" "net/http" "regexp" - "strings" "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" @@ -241,6 +241,24 @@ func (r mainRoutes) LectureStatsPage(c *gin.Context) { } } +func (r mainRoutes) LectureLiveManagementPage(c *gin.Context) { + foundContext, exists := c.Get("TUMLiveContext") + if !exists { + sentry.CaptureException(errors.New("context should exist but doesn't")) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + tumLiveContext := foundContext.(tools.TUMLiveContext) + indexData := NewIndexData() + indexData.TUMLiveContext = tumLiveContext + if err := templateExecutor.ExecuteTemplate(c.Writer, "lecture-live-management.gohtml", LiveLectureManagementData{ + IndexData: indexData, + Lecture: *tumLiveContext.Stream, + }); err != nil { + sentry.CaptureException(err) + } +} + func (r mainRoutes) LectureLiveManagementPage(c *gin.Context) { foundContext, exists := c.Get("TUMLiveContext") if !exists { diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 76eead59d..25b21e1a0 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -9,9 +9,8 @@ {{.IndexData.Branding.Title}} | {{$course.Name}}: {{$displayName}} {{template "headImports" .IndexData.VersionTag}} - - - + + @@ -27,171 +26,454 @@ {{end}} - - {{template "header" .IndexData.TUMLiveContext}} -
- -
-

{{$stream.Name}}

- | -

{{$course.Name}}

+ +{{template "header" .IndexData.TUMLiveContext}} + +
+
+
+
+

Share

+ +
+
+ URL +
+ + +
+
+
+
+
+{{if .IndexData.TUMLiveContext.User}} +
+
+ {{template "bookmarks-modal" $stream.ID}} +
+
+{{end}} +
+
+
+
+ +
+
+ +
+
+ +
-
-
-
-
-
- Time left: -
- + +
+ This livestream has ended.
-
-
- -
- - {{if $stream.PlaylistUrl}} - - {{else}} - - {{end}} - -
-
- {{if eq .Lecture.Description ""}} - No description available - {{else}} - {{.Lecture.Description}} - {{end}} -
- - -
- - -
- -
+ {{/*combined player*/}} + + {{else}}poster="/public/no_active_stream.jpg">{{end}} + {{if or .IndexData.TUMLiveContext.Stream.LiveNow .IndexData.TUMLiveContext.Stream.Recording}} + + + {{end}} +

+ To view this video please enable JavaScript. +

+
+
-
-
-

Video Stats

-
-
-
-
Resolution:
-
Bandwidth:
-
-
-
-
+ +
+ +
+ + +
-
- - {{if and $course.ChatEnabled $stream.ChatEnabled}}
-
- {{template "chat-component" .ChatData}} -
+ x-cloak + x-show="contextMenu.shown" + @click.outside="contextMenu = {...contextMenu, shown: false}" + class="origin-top-left absolute w-72 py-2 bg-gray-800/[.9] border-gray-100 border-1 rounded text-white text-sm" + :style="{ top: contextMenu.locY + 'px', left: contextMenu.locX + 'px' }" + role="menu" aria-orientation="vertical" tabindex="-1"> +
- {{end}} + + + +
+ + {{if and $course.ChatEnabled $stream.ChatEnabled}} +
+
+ {{/*template "chat-component" .ChatData*/}} +
+
+ {{end}} -
+ + +
+
+ +
+

+ {{$displayName}} +

+
+ - -
-
-
-
- -
-

Keep the recording after ending the stream?

- -
-
+ + +
+
+ {{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} + + {{end}} + + {{if .IndexData.TUMLiveContext.User}} + + {{end}} + + - + {{end}} + + + -
+
+
+
+ {{if $stream.LiveNow}} + +
+

Admin

+
+ {{/* Lecture hall is set, means no self-stream*/}} + {{if $stream.LiveNow}} + {{if $stream.LectureHallID}} + + + {{end}} + {{end}} +
+
+ {{if $stream.LiveNow}} +
+

Presets

+
+
+ {{range $preset := .Presets}} +
+ prev + +
+ {{end}} +
+
+
+ {{end}} + {{end}} + + + + + + +
+
+
+
+ +
+

Keep the recording after ending the stream?

+
+ +
+ + + +
+
+
+
+
+
+ + {{if $stream.Silences}} + watch.skipSilence({{$stream.GetSilencesJson}}); + {{end}} + From f16b1b2d683e61e34a12919395415720a0a6aca4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 9 Oct 2024 19:32:02 +0200 Subject: [PATCH 02/42] Added livestream and course/lecture name to management page --- .../admin/lecture-live-management.gohtml | 460 +----------------- 1 file changed, 19 insertions(+), 441 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 25b21e1a0..ddcb877ee 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -26,454 +26,32 @@ {{end}} - -{{template "header" .IndexData.TUMLiveContext}} - -
-
-
-
-

Share

- -
-
- URL -
- - -
-
+ + {{template "header" .IndexData.TUMLiveContext}} + -
-{{if .IndexData.TUMLiveContext.User}} -
-
- {{template "bookmarks-modal" $stream.ID}} -
-
-{{end}} -
-
-
-
- -
-
-
- -
-
- -
- - -
- This livestream has ended. -
- - {{/*combined player*/}} - - {{else}}poster="/public/no_active_stream.jpg">{{end}} - {{if or .IndexData.TUMLiveContext.Stream.LiveNow .IndexData.TUMLiveContext.Stream.Recording}} - +
+ +
+ + {{if $stream.PlaylistUrl}} + + {{else}} + {{end}} -

- To view this video please enable JavaScript. -

- -
- -
- -
- -
- - - -
-
- - - - - -
- - {{if and $course.ChatEnabled $stream.ChatEnabled}} -
-
- {{/*template "chat-component" .ChatData*/}} -
-
- {{end}} - - -
-
- -
-

- {{$displayName}} -

- - {{if not $stream.Recording}} - {{/*template "watch-info" .*/}} - {{end}} -
- - -
-
- {{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} - - {{end}} - - {{if .IndexData.TUMLiveContext.User}} - - {{end}} - - - - - {{if and $course.ChatEnabled $stream.ChatEnabled}} - - {{end}} - - - -
-
-
-
- {{if $stream.LiveNow}} - -
-

Admin

-
- {{/* Lecture hall is set, means no self-stream*/}} - {{if $stream.LiveNow}} - {{if $stream.LectureHallID}} - - - {{end}} - {{end}} -
-
- {{if $stream.LiveNow}} -
-

Presets

-
-
- {{range $preset := .Presets}} -
- prev - -
- {{end}} -
-
-
- {{end}} - {{end}} - - - - - - -
-
-
-
- -
-

Keep the recording after ending the stream?

-
- -
- - - -
-
-
-
-
-
- From e10d5b98ebf5a49868fa9e3e4db0332c9a708b9f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 10 Oct 2024 15:02:30 +0200 Subject: [PATCH 03/42] Added Video Stats --- .../admin/lecture-live-management.gohtml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index ddcb877ee..35658a391 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -52,6 +52,46 @@
+ + +
+
+

Video Stats

+
+
+
+
Resolution:
+
Bandwidth:
+
+
+
+
+
+
+
+
Buffer:
+
+
+
> Buffered Time:
+
> Chunks Requested:
+
> Requests Failed:
+
+
+
+
+
+
+
+
+
From bcafd9112639d250898d0e5dd3a7a4a6787b0fff Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 10 Oct 2024 16:26:59 +0200 Subject: [PATCH 04/42] Added stats and parts of the chat --- web/admin.go | 4 + .../admin/lecture-live-management.gohtml | 88 +++++++++++-------- web/ts/watch.ts | 1 + 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/web/admin.go b/web/admin.go index 3c53347d6..a9d27ee13 100644 --- a/web/admin.go +++ b/web/admin.go @@ -254,6 +254,10 @@ func (r mainRoutes) LectureLiveManagementPage(c *gin.Context) { if err := templateExecutor.ExecuteTemplate(c.Writer, "lecture-live-management.gohtml", LiveLectureManagementData{ IndexData: indexData, Lecture: *tumLiveContext.Stream, + ChatData: ChatData{ + IsAdminOfCourse: tumLiveContext.UserIsAdmin(), + IndexData: indexData, + }, }); err != nil { sentry.CaptureException(err) } diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 35658a391..7daae706a 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -36,11 +36,11 @@

{{$course.Name}}

-
+
{{if $stream.PlaylistUrl}} @@ -50,48 +50,62 @@ {{end}}
-
- - -
-
-

Video Stats

-
-
-
-
Resolution:
-
Bandwidth:
+ +
+
+

Video Stats

-
-
-
+
+
+
Resolution:
+
Bandwidth:
+
+
+
+
+
-
-
-
Buffer:
-
-
-
> Buffered Time:
-
> Chunks Requested:
-
> Requests Failed:
-
-
-
-
-
+
+
Buffer:
+
+
+
> Buffered Time:
+
> Chunks Requested:
+
> Requests Failed:
+
+
+
+
+
+
+ + {{if and $course.ChatEnabled $stream.ChatEnabled}} +
+
+ {{template "chat-component" .ChatData}} +
+
+ {{end}} + + +
+ + diff --git a/web/ts/watch.ts b/web/ts/watch.ts index 61d9c6d6e..650832823 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -1,5 +1,6 @@ import { getPlayers } from "./TUMLiveVjs"; import { copyToClipboard, Time } from "./global"; +import { seekbarOverlay } from "./seekbar-overlay"; export enum SidebarState { Hidden = "hidden", From bfefdc3b30a0a3e3e3e53e0d88f1c1be571f46fb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 10:28:38 +0100 Subject: [PATCH 05/42] Changed text color of stats --- web/template/admin/lecture-live-management.gohtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 7daae706a..79442db1a 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -55,7 +55,7 @@

Video Stats

From 8e57dee206ac6eecac3ff15d652cc37c124dd31d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 10:47:52 +0100 Subject: [PATCH 06/42] Moved chat to the right --- .../admin/lecture-live-management.gohtml | 116 +++++++++--------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 79442db1a..bdd830121 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -9,8 +9,8 @@ {{.IndexData.Branding.Title}} | {{$course.Name}}: {{$displayName}} {{template "headImports" .IndexData.VersionTag}} - - + + @@ -36,70 +36,72 @@

{{$course.Name}}

-
- -
- - {{if $stream.PlaylistUrl}} - - {{else}} - - {{end}} - -
- - -
-
-

Video Stats

+
+
+ +
+ + {{if $stream.PlaylistUrl}} + + {{else}} + + {{end}} +
-
-
-
Resolution:
-
Bandwidth:
-
-
-
-
+ + +
+
+

Video Stats

-
-
-
Buffer:
-
-
-
> Buffered Time:
-
> Chunks Requested:
-
> Requests Failed:
+
+
+
Resolution:
+
Bandwidth:
+
+
+
+
+
-
-
-
-
+
+
Buffer:
+
+
+
> Buffered Time:
+
> Chunks Requested:
+
> Requests Failed:
+
+
+
+
+
+
-
- - {{if and $course.ChatEnabled $stream.ChatEnabled}} -
-
- {{template "chat-component" .ChatData}} + + + {{if and $course.ChatEnabled $stream.ChatEnabled}} +
+
+ {{template "chat-component" .ChatData}} +
-
- {{end}} - + {{end}} +
From fff7704c1083b1e5e073aa0f38d9a6f404c39d55 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 11:10:33 +0100 Subject: [PATCH 07/42] Fixed chat and added Seek to Live Button --- .../admin/lecture-live-management.gohtml | 34 +++++++++++-------- web/ts/watch.ts | 9 +++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index bdd830121..213078318 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -28,7 +28,7 @@ {{template "header" .IndexData.TUMLiveContext}} -
+

{{$stream.Name}}

@@ -38,20 +38,27 @@
- -
- - {{if $stream.PlaylistUrl}} - - {{else}} - - {{end}} - +
+ +
+ + {{if $stream.PlaylistUrl}} + + {{else}} + + {{end}} + +
+
+ {{.Lecture.Description}} +
+
+
- {{if and $course.ChatEnabled $stream.ChatEnabled}}
{ + player.liveTracker.seekToLiveEdge(); + }); +} + export { repeatHeatMap } from "./repeat-heatmap"; export { seekbarHighlights, MarkerType } from "./seekbar-highlights"; export { seekbarOverlay, SeekbarHoverPosition } from "./seekbar-overlay"; From 94d51c96953203842c383e2065c09ee91954af14 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 11:19:50 +0100 Subject: [PATCH 08/42] Fixed layout of seek button --- web/template/admin/lecture-live-management.gohtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 213078318..684b95c06 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -55,7 +55,7 @@
{{.Lecture.Description}}
- +
From d9a9261646eb00d56eaac242ae96911a071c09da Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 11:39:46 +0100 Subject: [PATCH 09/42] Added function to use highest quality --- web/template/admin/lecture-live-management.gohtml | 6 +++++- web/ts/watch.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 684b95c06..64e688a40 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -53,7 +53,11 @@
- {{.Lecture.Description}} + {{if eq .Lecture.Description ""}} + No description available + {{else}} + {{.Lecture.Description}} + {{end}}
diff --git a/web/ts/watch.ts b/web/ts/watch.ts index 817f300cf..49896a92e 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -218,6 +218,21 @@ export function seekToLive() { }); } +export function setHighestQuality() { + const players = getPlayers(); + console.debug(players); + players.forEach((player) => { + let qualityLevels = player.qualityLevels(); + // Listen to change events for when the player selects a new quality level + qualityLevels.on('change', function() { + console.log('Quality Level changed!'); + console.log('New level:', qualityLevels[qualityLevels.selectedIndex]); + }); + qualityLevels.trigger({ type: 'change', selectedIndex: 0 }); + }); + +} + export { repeatHeatMap } from "./repeat-heatmap"; export { seekbarHighlights, MarkerType } from "./seekbar-highlights"; export { seekbarOverlay, SeekbarHoverPosition } from "./seekbar-overlay"; From 3eed1a9ee8552a4f0330c623fbfc3d50e9859be7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 11:54:50 +0100 Subject: [PATCH 10/42] Minor fixes --- web/template/admin/lecture-live-management.gohtml | 1 + web/ts/watch.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 64e688a40..7821bba5f 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -119,5 +119,6 @@ watch.initPlayer("videoPlayer", true, false, false, {{.IndexData.TUMLiveContext.User.GetEnabledPlaybackSpeeds}}, {{$stream.LiveNow}}, {{.IndexData.TUMLiveContext.User.GetSeekingTime}}); watch.videoStatListener.listen(); + diff --git a/web/ts/watch.ts b/web/ts/watch.ts index 49896a92e..d6db04db9 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -222,7 +222,7 @@ export function setHighestQuality() { const players = getPlayers(); console.debug(players); players.forEach((player) => { - let qualityLevels = player.qualityLevels(); + let qualityLevels = (player as any).qualityLevels(); // Listen to change events for when the player selects a new quality level qualityLevels.on('change', function() { console.log('Quality Level changed!'); From c06c2f84f465f1717201af5cc84799f3f029e6be Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 12:53:03 +0100 Subject: [PATCH 11/42] Added highest quality automatically and added Remaining live time --- .../admin/lecture-live-management.gohtml | 113 ++++++++++-------- web/ts/watch.ts | 22 +++- 2 files changed, 78 insertions(+), 57 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 7821bba5f..8d2d67089 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -37,65 +37,72 @@
-
-
- -
- - {{if $stream.PlaylistUrl}} - +
+
+
+ Live time left: {{.Lecture.End}} +
+
+
+ +
+ + {{if $stream.PlaylistUrl}} + + {{else}} + + {{end}} + +
+
+ {{if eq .Lecture.Description ""}} + No description available {{else}} - + {{.Lecture.Description}} {{end}} - -
-
- {{if eq .Lecture.Description ""}} - No description available - {{else}} - {{.Lecture.Description}} - {{end}} +
+ +
- -
- -
-
-

Video Stats

-
-
-
-
Resolution:
-
Bandwidth:
+ +
+
+

Video Stats

-
-
-
-
-
-
-
Buffer:
-
-
-
> Buffered Time:
-
> Chunks Requested:
-
> Requests Failed:
+
+
+
Resolution:
+
Bandwidth:
+
+
+
+
+
-
-
-
-
+
+
Buffer:
+
+
+
> Buffered Time:
+
> Chunks Requested:
+
> Requests Failed:
+
+
+
+
+
+
@@ -117,8 +124,8 @@ - diff --git a/web/ts/watch.ts b/web/ts/watch.ts index d6db04db9..91ceb142f 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -218,19 +218,33 @@ export function seekToLive() { }); } +function getHighestQualityLevel(qualityLevels : any[]): number { + let highestQuality = qualityLevels[0]; + for(let i = 1; i < qualityLevels.length; i++) { + if(qualityLevels[i].height > highestQuality.height) { + highestQuality = qualityLevels[i]; + } + } + return qualityLevels.indexOf(highestQuality); +} + export function setHighestQuality() { const players = getPlayers(); console.debug(players); players.forEach((player) => { let qualityLevels = (player as any).qualityLevels(); + let highestQuality = getHighestQualityLevel(qualityLevels.levels_); // Listen to change events for when the player selects a new quality level qualityLevels.on('change', function() { - console.log('Quality Level changed!'); - console.log('New level:', qualityLevels[qualityLevels.selectedIndex]); + console.debug('Quality Level changed!'); + console.debug('New level:', qualityLevels[qualityLevels.selectedIndex]); }); - qualityLevels.trigger({ type: 'change', selectedIndex: 0 }); + qualityLevels.trigger({ type: 'change', selectedIndex: highestQuality }); + qualityLevels.selectedIndex_ = highestQuality; + for(let i = 0; i < qualityLevels.length; i++) { + qualityLevels[i].enabled = i == highestQuality; + } }); - } export { repeatHeatMap } from "./repeat-heatmap"; From ec6e2a33adf5d0659e764999c3b2250865f4e4c4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 14:21:54 +0100 Subject: [PATCH 12/42] Added current time to lecture live page --- web/template/admin/lecture-live-management.gohtml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 8d2d67089..c8b44cd6e 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -38,9 +38,12 @@
-
-
- Live time left: {{.Lecture.End}} +
+
+
+ Time left: +
+
@@ -125,6 +128,7 @@ From e0d072261f2206d75b4050c3676d4e1731c76af0 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 19:42:32 +0100 Subject: [PATCH 13/42] Added reactions to model and added endpoints to add reactions --- api/stream.go | 92 ++++++++++++++++++++++++++++ cmd/tumlive/tumlive.go | 1 + config.yaml | 5 ++ dao/dao_base.go | 2 + dao/streamReaction.go | 81 ++++++++++++++++++++++++ mock_dao/streamReaction.go | 109 +++++++++++++++++++++++++++++++++ model/streamReaction.go | 18 ++++++ tools/config.go | 14 +++-- web/ts/api/stream-reactions.ts | 13 ++++ web/ts/entry/video.ts | 1 + 10 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 dao/streamReaction.go create mode 100644 mock_dao/streamReaction.go create mode 100644 model/streamReaction.go create mode 100644 web/ts/api/stream-reactions.ts diff --git a/api/stream.go b/api/stream.go index 4f880274e..09d9da277 100644 --- a/api/stream.go +++ b/api/stream.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "time" @@ -46,6 +47,9 @@ func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { streamById.GET("/playlist", routes.getStreamPlaylist) + streamById.POST("/reaction", routes.addReaction) + streamById.GET("/reaction/allowed", routes.allowedReactions) + thumbs := streamById.Group("/thumbs") { thumbs.GET(":fid", routes.getThumbs) @@ -426,6 +430,94 @@ func (r streamRoutes) getVideoSections(c *gin.Context) { c.JSON(http.StatusOK, sections) } +// TODO: This can be modified to allow different reactions for different streams +func (r streamRoutes) allowedReactions(c *gin.Context) { + c.JSON(http.StatusOK, tools.Cfg.AllowedReactions) +} + +func (r streamRoutes) addReaction(c *gin.Context) { + cooldownSeconds := 10 + + tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) + user := tumLiveContext.User + stream := tumLiveContext.Stream + + if stream == nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusNotFound, + CustomMessage: "stream not found", + }) + return + } + + course, err := r.DaoWrapper.CoursesDao.GetCourseById(c, stream.CourseID) + + if user == nil || err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "user or course not found", + }) + return + } + + if !user.IsEligibleToWatchCourse(course) { + _ = c.Error(tools.RequestError{ + Status: http.StatusForbidden, + CustomMessage: "user not eligible to watch course", + }) + return + } + + type reactionRequest struct { + Reaction string `json:"reaction"` + } + + var reaction reactionRequest + if err := c.ShouldBindJSON(&reaction); err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "can not bind body", + Err: err, + }) + return + } + + // TODO: This can be modified to allow different reactions for different streams + if !slices.Contains(tools.Cfg.AllowedReactions, reaction.Reaction) { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "reaction not allowed", + }) + return + } + + lastReaction, _ := r.DaoWrapper.StreamReactionDao.GetLastReactionOfUser(c, user.ID) + if lastReaction.Reaction != "" && lastReaction.CreatedAt.Add(time.Duration(cooldownSeconds)*time.Second).After(time.Now()) { + _ = c.Error(tools.RequestError{ + Status: http.StatusTooManyRequests, + CustomMessage: "cooldown not over", + }) + return + } + + reactionObj := model.StreamReaction{ + Reaction: reaction.Reaction, + StreamID: stream.ID, + UserID: user.ID, + } + + err = r.DaoWrapper.StreamReactionDao.Create(c, &reactionObj) + if err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "can not create reaction", + Err: err, + }) + return + } + c.JSON(http.StatusOK, "") +} + // RegenerateThumbs regenerates the thumbnails for a stream. func (r streamRoutes) RegenerateThumbs(c *gin.Context) { tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) diff --git a/cmd/tumlive/tumlive.go b/cmd/tumlive/tumlive.go index 1a014cd4b..24989c6c4 100755 --- a/cmd/tumlive/tumlive.go +++ b/cmd/tumlive/tumlive.go @@ -212,6 +212,7 @@ func main() { &model.Subtitles{}, &model.TranscodingFailure{}, &model.Email{}, + &model.StreamReaction{}, &model.Runner{}, ) if err != nil { diff --git a/config.yaml b/config.yaml index d89a8bdcf..6b730547b 100644 --- a/config.yaml +++ b/config.yaml @@ -104,3 +104,8 @@ meili: vodURLTemplate: https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/%s.mp4/playlist.m3u8 canonicalURL: https://tum.live rtmpProxyURL: https://proxy.example.com +allowedReactions: + - 😊 + - 👍 + - 👎 + - 😢 \ No newline at end of file diff --git a/dao/dao_base.go b/dao/dao_base.go index 6f1888853..35f90ba8c 100644 --- a/dao/dao_base.go +++ b/dao/dao_base.go @@ -36,6 +36,7 @@ type DaoWrapper struct { SubtitlesDao TranscodingFailureDao EmailDao + StreamReactionDao RunnerDao RunnerDao } @@ -64,6 +65,7 @@ func NewDaoWrapper() DaoWrapper { SubtitlesDao: NewSubtitlesDao(), TranscodingFailureDao: NewTranscodingFailureDao(), EmailDao: NewEmailDao(), + StreamReactionDao: NewStreamReactionDao(), RunnerDao: NewRunnerDao(), } } diff --git a/dao/streamReaction.go b/dao/streamReaction.go new file mode 100644 index 000000000..3d215a452 --- /dev/null +++ b/dao/streamReaction.go @@ -0,0 +1,81 @@ +package dao + +import ( + "context" + "github.com/TUM-Dev/gocast/model" + "gorm.io/gorm" +) + +//go:generate mockgen -source=streamReaction.go -destination ../mock_dao/streamReaction.go + +type StreamReactionDao interface { + // Get StreamReaction by ID + Get(context.Context, uint) (model.StreamReaction, error) + + // Create a new StreamReaction for the database + Create(context.Context, *model.StreamReaction) error + + // Delete a StreamReaction by id. + Delete(context.Context, uint) error + + GetByStream(context.Context, uint) ([]model.StreamReaction, error) + + GetNumbersOfReactions(context.Context, uint) (map[string]int, error) + + GetLastReactionOfUser(context.Context, uint) (model.StreamReaction, error) +} + +type streamReactionDao struct { + db *gorm.DB +} + +type reactionCount struct { + Reaction string + Count int +} + +func NewStreamReactionDao() StreamReactionDao { + return streamReactionDao{db: DB} +} + +// Get a StreamReaction by id. +func (d streamReactionDao) Get(c context.Context, id uint) (res model.StreamReaction, err error) { + return res, d.db.WithContext(c).First(&res, id).Error +} + +// Create a StreamReaction. +func (d streamReactionDao) Create(c context.Context, it *model.StreamReaction) error { + return d.db.WithContext(c).Create(it).Error +} + +// Delete a StreamReaction by id. +func (d streamReactionDao) Delete(c context.Context, id uint) error { + return d.db.WithContext(c).Delete(&model.StreamReaction{}, id).Error +} + +// GetByStream gets a StreamReaction by stream. +func (d streamReactionDao) GetByStream(c context.Context, streamID uint) (res []model.StreamReaction, err error) { + return res, d.db.WithContext(c).Where("stream_id = ?", streamID).Find(&res).Error +} + +// GetNumbersOfReactions gets the number of reactions grouped by reactions for a stream. +func (d streamReactionDao) GetNumbersOfReactions(c context.Context, streamID uint) (map[string]int, error) { + var reactionCounts []reactionCount + err := d.db.WithContext(c).Model(&model.StreamReaction{}).Where("stream_id = ?", streamID).Group("reaction").Select("reaction, count(reaction) as count").Scan(&reactionCounts).Error + + if err != nil { + return nil, err + } + + reactionMap := make(map[string]int) + for _, rc := range reactionCounts { + reactionMap[rc.Reaction] = rc.Count + } + + return reactionMap, err +} + +// GetLastReactionOfUser gets the last reaction of a user. +func (d streamReactionDao) GetLastReactionOfUser(c context.Context, userID uint) (res model.StreamReaction, err error) { + return res, d.db.WithContext(c).Where("user_id = ?", userID).Last(&res).Error +} diff --git a/mock_dao/streamReaction.go b/mock_dao/streamReaction.go new file mode 100644 index 000000000..6dbc0e973 --- /dev/null +++ b/mock_dao/streamReaction.go @@ -0,0 +1,109 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: streamReaction.go + +// Package mock_dao is a generated GoMock package. +package mock_dao + +import ( + context "context" + reflect "reflect" + + model "github.com/TUM-Dev/gocast/model" + gomock "github.com/golang/mock/gomock" +) + +// MockStreamReactionDao is a mock of StreamReactionDao interface. +type MockStreamReactionDao struct { + ctrl *gomock.Controller + recorder *MockStreamReactionDaoMockRecorder +} + +// MockStreamReactionDaoMockRecorder is the mock recorder for MockStreamReactionDao. +type MockStreamReactionDaoMockRecorder struct { + mock *MockStreamReactionDao +} + +// NewMockStreamReactionDao creates a new mock instance. +func NewMockStreamReactionDao(ctrl *gomock.Controller) *MockStreamReactionDao { + mock := &MockStreamReactionDao{ctrl: ctrl} + mock.recorder = &MockStreamReactionDaoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStreamReactionDao) EXPECT() *MockStreamReactionDaoMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockStreamReactionDao) Create(arg0 context.Context, arg1 *model.StreamReaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockStreamReactionDaoMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockStreamReactionDao)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockStreamReactionDao) Delete(arg0 context.Context, arg1 uint) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockStreamReactionDaoMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStreamReactionDao)(nil).Delete), arg0, arg1) +} + +// Get mocks base method. +func (m *MockStreamReactionDao) Get(arg0 context.Context, arg1 uint) (model.StreamReaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(model.StreamReaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStreamReactionDaoMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStreamReactionDao)(nil).Get), arg0, arg1) +} + +// GetByStream mocks base method. +func (m *MockStreamReactionDao) GetByStream(arg0 context.Context, arg1 uint) ([]model.StreamReaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByStream", arg0, arg1) + ret0, _ := ret[0].([]model.StreamReaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByStream indicates an expected call of GetByStream. +func (mr *MockStreamReactionDaoMockRecorder) GetByStream(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByStream", reflect.TypeOf((*MockStreamReactionDao)(nil).GetByStream), arg0, arg1) +} + +// GetNumbersOfReactions mocks base method. +func (m *MockStreamReactionDao) GetNumbersOfReactions(arg0 context.Context, arg1 uint) (map[string]int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNumbersOfReactions", arg0, arg1) + ret0, _ := ret[0].(map[string]int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNumbersOfReactions indicates an expected call of GetNumbersOfReactions. +func (mr *MockStreamReactionDaoMockRecorder) GetNumbersOfReactions(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNumbersOfReactions", reflect.TypeOf((*MockStreamReactionDao)(nil).GetNumbersOfReactions), arg0, arg1) +} diff --git a/model/streamReaction.go b/model/streamReaction.go new file mode 100644 index 000000000..604708900 --- /dev/null +++ b/model/streamReaction.go @@ -0,0 +1,18 @@ +package model + +import "gorm.io/gorm" + +// StreamReaction represents Reactions of users to a Stream. +type StreamReaction struct { + gorm.Model + + Reaction string `gorm:"not null" json:"reaction"` + StreamID uint `gorm:"not null" json:"streamID"` + UserID uint `gorm:"not null" json:"userID"` + // Name string `gorm:"column:name;type:text;not null;default:'unnamed'"` +} + +// TableName returns the name of the table for the StreamReaction model in the database. +func (*StreamReaction) TableName() string { + return "streamReaction" // todo +} diff --git a/tools/config.go b/tools/config.go index 77d0c7ab3..3561e03b8 100644 --- a/tools/config.go +++ b/tools/config.go @@ -95,6 +95,11 @@ func initConfig() { if os.Getenv("DBHOST") != "" { Cfg.Db.Host = os.Getenv("DBHOST") } + if len(Cfg.AllowedReactions) > 0 { + logger.Debug("Allowed reactions", "reactions", Cfg.AllowedReactions) + } else { + logger.Warn("No allowed reactions configured") + } } type Config struct { @@ -171,10 +176,11 @@ type Config struct { Host string `yaml:"host"` ApiKey string `yaml:"apiKey"` } `yaml:"meili"` - VodURLTemplate string `yaml:"vodURLTemplate"` - CanonicalURL string `yaml:"canonicalURL"` - WikiURL string `yaml:"wikiURL"` - RtmpProxyURL string `yaml:"rtmpProxyURL"` + VodURLTemplate string `yaml:"vodURLTemplate"` + CanonicalURL string `yaml:"canonicalURL"` + WikiURL string `yaml:"wikiURL"` + RtmpProxyURL string `yaml:"rtmpProxyURL"` + AllowedReactions []string `yaml:"allowedReactions"` } type MailConfig struct { diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts new file mode 100644 index 000000000..754c14c59 --- /dev/null +++ b/web/ts/api/stream-reactions.ts @@ -0,0 +1,13 @@ +import {getData, postData} from "../global"; + + +// Function to add a reaction to a stream +export function addReaction(reaction: string, streamID: number) { + return postData(`/api/${streamID}/reactions`, { reaction: reaction }); +} + +export function getAllowedReactions(streamID: number) { + return getData(`/api/${streamID}/reactions/allowed`).then((data) => { + return data; + }); +} \ No newline at end of file diff --git a/web/ts/entry/video.ts b/web/ts/entry/video.ts index 14cfca3a9..a5d59fa9e 100644 --- a/web/ts/entry/video.ts +++ b/web/ts/entry/video.ts @@ -9,3 +9,4 @@ export * from "../subtitle-search"; export * from "../components/video-sections"; // Lecture Units are currently not used, so we don't include them in the bundle at the moment export * from "../interval-updates"; +export * from "../api/stream-reactions"; From 878fec406204157095ddc4a837d8cf80f76d2864 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 12 Nov 2024 16:41:29 +0100 Subject: [PATCH 14/42] Added reactions to watch page --- .../admin/lecture-live-management.gohtml | 2 ++ .../components/stream-reactions.gohtml | 7 +++++ web/template/watch.gohtml | 31 +++++++++++++------ web/ts/api/stream-reactions.ts | 8 +++-- 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 web/template/components/stream-reactions.gohtml diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index c8b44cd6e..3ae0c242a 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -109,6 +109,8 @@
+ + {{template "stream-reactions" .Lecture}}
diff --git a/web/template/components/stream-reactions.gohtml b/web/template/components/stream-reactions.gohtml new file mode 100644 index 000000000..82be2f71c --- /dev/null +++ b/web/template/components/stream-reactions.gohtml @@ -0,0 +1,7 @@ +{{define "stream-reactions"}}{{- /*gotype: github.com/TUM-Dev/gocast/model.Stream*/ -}} +
+ +
+{{end}} \ No newline at end of file diff --git a/web/template/watch.gohtml b/web/template/watch.gohtml index a85eaee8e..c64232df9 100644 --- a/web/template/watch.gohtml +++ b/web/template/watch.gohtml @@ -478,7 +478,7 @@
-
+
{{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} + +
+
+ {{template "stream-reactions" $stream}} +
+
{{ if not $stream.LiveNow }} diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index 754c14c59..b929661d7 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -1,13 +1,15 @@ import {getData, postData} from "../global"; +import {get} from "../utilities/fetch-wrappers"; // Function to add a reaction to a stream export function addReaction(reaction: string, streamID: number) { - return postData(`/api/${streamID}/reactions`, { reaction: reaction }); + return postData(`/api/stream/${streamID}/reaction`, { reaction: reaction }); } -export function getAllowedReactions(streamID: number) { - return getData(`/api/${streamID}/reactions/allowed`).then((data) => { +// Function to get all possible reactions for a stream +export function getAllowedReactions(streamID: number): Promise { + return get(`/api/stream/${streamID}/reaction/allowed`).then((data) => { return data; }); } \ No newline at end of file From 1597514a6a6fd04493fefe1f75d60c99c37158f2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 12 Nov 2024 16:42:12 +0100 Subject: [PATCH 15/42] Removed reactions from lecture-management --- web/template/admin/lecture-live-management.gohtml | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 3ae0c242a..c8b44cd6e 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -109,8 +109,6 @@
- - {{template "stream-reactions" .Lecture}}
From e45b5c4c56b0b4b066eb5022dcfb0dd3d3d7889b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 12:54:53 +0100 Subject: [PATCH 16/42] Renamed stream reactions --- dao/{streamReaction.go => stream-reaction.go} | 0 mock_dao/{streamReaction.go => stream-reaction.go} | 0 model/{streamReaction.go => stream-reaction.go} | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename dao/{streamReaction.go => stream-reaction.go} (100%) rename mock_dao/{streamReaction.go => stream-reaction.go} (100%) rename model/{streamReaction.go => stream-reaction.go} (93%) diff --git a/dao/streamReaction.go b/dao/stream-reaction.go similarity index 100% rename from dao/streamReaction.go rename to dao/stream-reaction.go diff --git a/mock_dao/streamReaction.go b/mock_dao/stream-reaction.go similarity index 100% rename from mock_dao/streamReaction.go rename to mock_dao/stream-reaction.go diff --git a/model/streamReaction.go b/model/stream-reaction.go similarity index 93% rename from model/streamReaction.go rename to model/stream-reaction.go index 604708900..1567594b0 100644 --- a/model/streamReaction.go +++ b/model/stream-reaction.go @@ -14,5 +14,5 @@ type StreamReaction struct { // TableName returns the name of the table for the StreamReaction model in the database. func (*StreamReaction) TableName() string { - return "streamReaction" // todo + return "stream_reaction" // todo } From c92b34773858e2cd3686b118bf00026e411ce945 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 14:02:15 +0100 Subject: [PATCH 17/42] Added stream reaction websocket --- api/router.go | 1 + api/stream.go | 95 +----------------- api/stream_reactions.go | 205 +++++++++++++++++++++++++++++++++++++++ model/stream-reaction.go | 2 +- 4 files changed, 211 insertions(+), 92 deletions(-) create mode 100644 api/stream_reactions.go diff --git a/api/router.go b/api/router.go index 8eabec6b5..e0d99e483 100755 --- a/api/router.go +++ b/api/router.go @@ -20,6 +20,7 @@ func ConfigRealtimeRouter(router *gin.RouterGroup) { // Register Channels RegisterLiveUpdateRealtimeChannel() + RegisterReactionUpdateRealtimeChannel() RegisterLiveRunnerPageUpdateRealtimeChannel(daoWrapper) RegisterRealtimeChatChannel() } diff --git a/api/stream.go b/api/stream.go index 09d9da277..9a72aa1a8 100644 --- a/api/stream.go +++ b/api/stream.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "path/filepath" - "slices" "strconv" "strings" "time" @@ -32,6 +31,7 @@ const ( func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { routes := streamRoutes{daoWrapper} + reactionRoutes := StreamReactionRoutes{daoWrapper} stream := router.Group("/api/stream") { @@ -47,8 +47,9 @@ func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { streamById.GET("/playlist", routes.getStreamPlaylist) - streamById.POST("/reaction", routes.addReaction) - streamById.GET("/reaction/allowed", routes.allowedReactions) + streamById.POST("/reaction", reactionRoutes.addReaction) + streamById.GET("/reaction/allowed", reactionRoutes.allowedReactions) + streamById.GET("/reaction/ws", reactionRoutes.upgradeWebsocket) thumbs := streamById.Group("/thumbs") { @@ -430,94 +431,6 @@ func (r streamRoutes) getVideoSections(c *gin.Context) { c.JSON(http.StatusOK, sections) } -// TODO: This can be modified to allow different reactions for different streams -func (r streamRoutes) allowedReactions(c *gin.Context) { - c.JSON(http.StatusOK, tools.Cfg.AllowedReactions) -} - -func (r streamRoutes) addReaction(c *gin.Context) { - cooldownSeconds := 10 - - tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) - user := tumLiveContext.User - stream := tumLiveContext.Stream - - if stream == nil { - _ = c.Error(tools.RequestError{ - Status: http.StatusNotFound, - CustomMessage: "stream not found", - }) - return - } - - course, err := r.DaoWrapper.CoursesDao.GetCourseById(c, stream.CourseID) - - if user == nil || err != nil { - _ = c.Error(tools.RequestError{ - Status: http.StatusInternalServerError, - CustomMessage: "user or course not found", - }) - return - } - - if !user.IsEligibleToWatchCourse(course) { - _ = c.Error(tools.RequestError{ - Status: http.StatusForbidden, - CustomMessage: "user not eligible to watch course", - }) - return - } - - type reactionRequest struct { - Reaction string `json:"reaction"` - } - - var reaction reactionRequest - if err := c.ShouldBindJSON(&reaction); err != nil { - _ = c.Error(tools.RequestError{ - Status: http.StatusBadRequest, - CustomMessage: "can not bind body", - Err: err, - }) - return - } - - // TODO: This can be modified to allow different reactions for different streams - if !slices.Contains(tools.Cfg.AllowedReactions, reaction.Reaction) { - _ = c.Error(tools.RequestError{ - Status: http.StatusBadRequest, - CustomMessage: "reaction not allowed", - }) - return - } - - lastReaction, _ := r.DaoWrapper.StreamReactionDao.GetLastReactionOfUser(c, user.ID) - if lastReaction.Reaction != "" && lastReaction.CreatedAt.Add(time.Duration(cooldownSeconds)*time.Second).After(time.Now()) { - _ = c.Error(tools.RequestError{ - Status: http.StatusTooManyRequests, - CustomMessage: "cooldown not over", - }) - return - } - - reactionObj := model.StreamReaction{ - Reaction: reaction.Reaction, - StreamID: stream.ID, - UserID: user.ID, - } - - err = r.DaoWrapper.StreamReactionDao.Create(c, &reactionObj) - if err != nil { - _ = c.Error(tools.RequestError{ - Status: http.StatusInternalServerError, - CustomMessage: "can not create reaction", - Err: err, - }) - return - } - c.JSON(http.StatusOK, "") -} - // RegenerateThumbs regenerates the thumbnails for a stream. func (r streamRoutes) RegenerateThumbs(c *gin.Context) { tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) diff --git a/api/stream_reactions.go b/api/stream_reactions.go new file mode 100644 index 000000000..7f183bbed --- /dev/null +++ b/api/stream_reactions.go @@ -0,0 +1,205 @@ +package api + +import ( + "errors" + "github.com/TUM-Dev/gocast/dao" + "github.com/TUM-Dev/gocast/model" + "github.com/TUM-Dev/gocast/tools" + "github.com/TUM-Dev/gocast/tools/realtime" + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" + "net/http" + "slices" + "sync" + "time" +) + +type StreamReactionRoutes struct { + dao.DaoWrapper +} + +// TODO: This can be modified to allow different reactions for different streams +func (r StreamReactionRoutes) allowedReactions(c *gin.Context) { + c.JSON(http.StatusOK, tools.Cfg.AllowedReactions) +} + +func (r StreamReactionRoutes) addReaction(c *gin.Context) { + cooldownSeconds := 10 + + tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) + user := tumLiveContext.User + stream := tumLiveContext.Stream + + if stream == nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusNotFound, + CustomMessage: "stream not found", + }) + return + } + + course, err := r.DaoWrapper.CoursesDao.GetCourseById(c, stream.CourseID) + + if user == nil || err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "user or course not found", + }) + return + } + + if !user.IsEligibleToWatchCourse(course) { + _ = c.Error(tools.RequestError{ + Status: http.StatusForbidden, + CustomMessage: "user not eligible to watch course", + }) + return + } + + type reactionRequest struct { + Reaction string `json:"reaction"` + } + + var reaction reactionRequest + if err := c.ShouldBindJSON(&reaction); err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "can not bind body", + Err: err, + }) + return + } + + // TODO: This can be modified to allow different reactions for different streams + if !slices.Contains(tools.Cfg.AllowedReactions, reaction.Reaction) { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "reaction not allowed", + }) + return + } + + lastReaction, _ := r.DaoWrapper.StreamReactionDao.GetLastReactionOfUser(c, user.ID) + if lastReaction.Reaction != "" && lastReaction.CreatedAt.Add(time.Duration(cooldownSeconds)*time.Second).After(time.Now()) { + _ = c.Error(tools.RequestError{ + Status: http.StatusTooManyRequests, + CustomMessage: "cooldown not over", + }) + return + } + + reactionObj := model.StreamReaction{ + Reaction: reaction.Reaction, + StreamID: stream.ID, + UserID: user.ID, + } + + err = r.DaoWrapper.StreamReactionDao.Create(c, &reactionObj) + if err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "can not create reaction", + Err: err, + }) + return + } + c.JSON(http.StatusOK, "") +} + +const ( + ReactionUpdateRoomName = "reaction-update" +) + +var ( + liveReactionListenerMutex sync.RWMutex + liveReactionListener = map[uint]*liveReactionAdminSessionsWrapper{} +) + +type liveReactionAdminSessionsWrapper struct { + sessions []*realtime.Context + stream uint +} + +func RegisterReactionUpdateRealtimeChannel() { + RealtimeInstance.RegisterChannel(ReactionUpdateRoomName, realtime.ChannelHandlers{ + OnSubscribe: reactionUpdateOnSubscribe, + OnUnsubscribe: reactionUpdateOnUnsubscribe, + }) +} + +func reactionUpdateOnUnsubscribe(psc *realtime.Context) { + ctx, _ := psc.Client.Get("ctx") // get gin context + foundContext, exists := ctx.(*gin.Context).Get("TUMLiveContext") + if !exists { + sentry.CaptureException(errors.New("context should exist but doesn't")) + return + } + + tumLiveContext := foundContext.(tools.TUMLiveContext) + + var userId uint = 0 + if tumLiveContext.User != nil { + userId = tumLiveContext.User.ID + } + + liveReactionListenerMutex.Lock() + defer liveReactionListenerMutex.Unlock() + var newSessions []*realtime.Context + for _, session := range liveReactionListener[userId].sessions { + if session != psc { + newSessions = append(newSessions, session) + } + } + if len(newSessions) == 0 { + delete(liveReactionListener, userId) + } else { + liveReactionListener[userId].sessions = newSessions + } +} + +func reactionUpdateOnSubscribe(psc *realtime.Context) { + ctx, _ := psc.Client.Get("ctx") // get gin context + + foundContext, exists := ctx.(*gin.Context).Get("TUMLiveContext") + if !exists { + sentry.CaptureException(errors.New("context should exist but doesn't")) + return + } + + tumLiveContext := foundContext.(tools.TUMLiveContext) + + var userId uint = 0 + var err error + + if tumLiveContext.User != nil { + userId = tumLiveContext.User.ID + } else { + logger.Error("could not fetch public courses", "err", err) + return + + } + + stream := tumLiveContext.Stream + + liveReactionListenerMutex.Lock() + if liveReactionListener[userId] != nil { + liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(liveUpdateListener[userId].sessions, psc), stream.Model.ID} + } else { + liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{[]*realtime.Context{psc}, stream.Model.ID} + } + liveReactionListenerMutex.Unlock() +} + +func NotifyAdminsOnReaction(streamID uint, reaction string) { + liveReactionListenerMutex.Lock() + for _, session := range liveReactionListener { + if session.stream == streamID { + for _, s := range session.sessions { + err := s.Send([]byte(reaction)) + if err != nil { + logger.Error("can't write reaction to session", "err", err) + } + } + } + } +} diff --git a/model/stream-reaction.go b/model/stream-reaction.go index 1567594b0..a5e876a2b 100644 --- a/model/stream-reaction.go +++ b/model/stream-reaction.go @@ -14,5 +14,5 @@ type StreamReaction struct { // TableName returns the name of the table for the StreamReaction model in the database. func (*StreamReaction) TableName() string { - return "stream_reaction" // todo + return "stream_reaction" } From e69c80edb18a4d16299d7279667a422eccdd0620 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 15:01:36 +0100 Subject: [PATCH 18/42] Added functionality to send reaction to admin --- api/stream.go | 1 - api/stream_reactions.go | 70 +++++++++++++++++-- .../admin/lecture-live-management.gohtml | 4 +- web/ts/api/stream-reactions.ts | 16 ++++- 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/api/stream.go b/api/stream.go index 9a72aa1a8..2552dec98 100644 --- a/api/stream.go +++ b/api/stream.go @@ -49,7 +49,6 @@ func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { streamById.POST("/reaction", reactionRoutes.addReaction) streamById.GET("/reaction/allowed", reactionRoutes.allowedReactions) - streamById.GET("/reaction/ws", reactionRoutes.upgradeWebsocket) thumbs := streamById.Group("/thumbs") { diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 7f183bbed..4257e45cf 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" @@ -10,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "net/http" "slices" + "strconv" "sync" "time" ) @@ -103,9 +105,12 @@ func (r StreamReactionRoutes) addReaction(c *gin.Context) { }) return } + NotifyAdminsOnReaction(stream.ID, reaction.Reaction) c.JSON(http.StatusOK, "") } +// The part below is used for Realtime Connection to the client + const ( ReactionUpdateRoomName = "reaction-update" ) @@ -124,6 +129,7 @@ func RegisterReactionUpdateRealtimeChannel() { RealtimeInstance.RegisterChannel(ReactionUpdateRoomName, realtime.ChannelHandlers{ OnSubscribe: reactionUpdateOnSubscribe, OnUnsubscribe: reactionUpdateOnUnsubscribe, + OnMessage: reactionUpdateSetStream, }) } @@ -179,23 +185,79 @@ func reactionUpdateOnSubscribe(psc *realtime.Context) { } - stream := tumLiveContext.Stream + liveReactionListenerMutex.Lock() + if liveReactionListener[userId] != nil { + liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(liveUpdateListener[userId].sessions, psc), liveReactionListener[userId].stream} + } else { + liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{[]*realtime.Context{psc}, 0} + } + liveReactionListenerMutex.Unlock() +} + +func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { + logger.Info("reactionUpdateSetStream", "message", string(message.Payload)) + ctx, _ := psc.Client.Get("ctx") // get gin context + + foundContext, exists := ctx.(*gin.Context).Get("TUMLiveContext") + if !exists { + sentry.CaptureException(errors.New("context should exist but doesn't")) + return + } + + tumLiveContext := foundContext.(tools.TUMLiveContext) + + var userId uint = 0 + var err error + + if tumLiveContext.User != nil { + userId = tumLiveContext.User.ID + } else { + logger.Error("could not get user from request", "err", err) + return + } + + type Message struct { + StreamID string `json:"streamId"` + } + + var messageObj Message + err = json.Unmarshal(message.Payload, &messageObj) + + if err != nil { + logger.Error("could not unmarshal message", "err", err) + return + } liveReactionListenerMutex.Lock() if liveReactionListener[userId] != nil { - liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(liveUpdateListener[userId].sessions, psc), stream.Model.ID} + uId, err := strconv.Atoi(messageObj.StreamID) + if err != nil { + logger.Error("could not convert streamID to int", "err", err) + return + } + liveReactionListener[userId].stream = uint(uId) } else { - liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{[]*realtime.Context{psc}, stream.Model.ID} + logger.Error("User has no live reaction listener") } liveReactionListenerMutex.Unlock() } func NotifyAdminsOnReaction(streamID uint, reaction string) { liveReactionListenerMutex.Lock() + reactionStruct := struct { + Reaction string `json:"reaction"` + }{ + Reaction: reaction, + } + reactionMarshaled, err := json.Marshal(reactionStruct) + if err != nil { + logger.Error("could not marshal reaction", "err", err) + return + } for _, session := range liveReactionListener { if session.stream == streamID { for _, s := range session.sessions { - err := s.Send([]byte(reaction)) + err := s.Send([]byte(reactionMarshaled)) if err != nil { logger.Error("can't write reaction to session", "err", err) } diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index c8b44cd6e..1c280f051 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -28,7 +28,7 @@ {{template "header" .IndexData.TUMLiveContext}} -
+

{{$stream.Name}}

@@ -111,6 +111,8 @@
+
+ {{if and $course.ChatEnabled $stream.ChatEnabled}}
{ return get(`/api/stream/${streamID}/reaction/allowed`).then((data) => { return data; }); -} \ No newline at end of file +} + +export const liveReactionListener = { + async init(streamId: string) { + await Realtime.get().subscribeChannel("reaction-update", this.handle); + setTimeout(async () => { + await Realtime.get().send("reaction-update", {type: RealtimeMessageTypes.RealtimeMessageTypeChannelMessage, payload: {"streamID": streamId}}); + }, 2000); + }, + + handle(payload: object) { + window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); + }, +}; From 07da05bbb4219f7482bc48f83ce4b8b0de0fb3c5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 15:07:33 +0100 Subject: [PATCH 19/42] Added admin check --- api/stream_reactions.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 4257e45cf..df83fb395 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -222,12 +222,28 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { var messageObj Message err = json.Unmarshal(message.Payload, &messageObj) - if err != nil { logger.Error("could not unmarshal message", "err", err) return } + daoWrapper := ctx.(dao.DaoWrapper) + stream, err := daoWrapper.StreamsDao.GetStreamByID(nil, messageObj.StreamID) + if err != nil { + logger.Error("Cant get stream by id", "err", err) + return + } + course, err := daoWrapper.CoursesDao.GetCourseById(nil, stream.CourseID) + if err != nil { + logger.Error("Cant get course by id", "err", err) + return + } + if !tumLiveContext.User.IsAdminOfCourse(course) { + logger.Error("User is not admin of course") + reactionUpdateOnUnsubscribe(psc) + return + } + liveReactionListenerMutex.Lock() if liveReactionListener[userId] != nil { uId, err := strconv.Atoi(messageObj.StreamID) From d2e10c90d2f78882d83d1ab65a4083659fac68e6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 15:19:14 +0100 Subject: [PATCH 20/42] Fixed kernel panic --- api/router.go | 2 +- api/stream_reactions.go | 5 +++-- web/ts/api/stream-reactions.ts | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/api/router.go b/api/router.go index e0d99e483..06e73386a 100755 --- a/api/router.go +++ b/api/router.go @@ -20,7 +20,7 @@ func ConfigRealtimeRouter(router *gin.RouterGroup) { // Register Channels RegisterLiveUpdateRealtimeChannel() - RegisterReactionUpdateRealtimeChannel() + RegisterReactionUpdateRealtimeChannel(daoWrapper) RegisterLiveRunnerPageUpdateRealtimeChannel(daoWrapper) RegisterRealtimeChatChannel() } diff --git a/api/stream_reactions.go b/api/stream_reactions.go index df83fb395..0a6c6bd0e 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -118,6 +118,7 @@ const ( var ( liveReactionListenerMutex sync.RWMutex liveReactionListener = map[uint]*liveReactionAdminSessionsWrapper{} + daoWrapper dao.DaoWrapper ) type liveReactionAdminSessionsWrapper struct { @@ -125,12 +126,13 @@ type liveReactionAdminSessionsWrapper struct { stream uint } -func RegisterReactionUpdateRealtimeChannel() { +func RegisterReactionUpdateRealtimeChannel(wrapper dao.DaoWrapper) { RealtimeInstance.RegisterChannel(ReactionUpdateRoomName, realtime.ChannelHandlers{ OnSubscribe: reactionUpdateOnSubscribe, OnUnsubscribe: reactionUpdateOnUnsubscribe, OnMessage: reactionUpdateSetStream, }) + daoWrapper = wrapper } func reactionUpdateOnUnsubscribe(psc *realtime.Context) { @@ -227,7 +229,6 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { return } - daoWrapper := ctx.(dao.DaoWrapper) stream, err := daoWrapper.StreamsDao.GetStreamByID(nil, messageObj.StreamID) if err != nil { logger.Error("Cant get stream by id", "err", err) diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index 65afbf2af..b96b7c1e1 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -18,9 +18,7 @@ export function getAllowedReactions(streamID: number): Promise { export const liveReactionListener = { async init(streamId: string) { await Realtime.get().subscribeChannel("reaction-update", this.handle); - setTimeout(async () => { - await Realtime.get().send("reaction-update", {type: RealtimeMessageTypes.RealtimeMessageTypeChannelMessage, payload: {"streamID": streamId}}); - }, 2000); + await Realtime.get().send("reaction-update", {type: RealtimeMessageTypes.RealtimeMessageTypeChannelMessage, payload: {"streamID": streamId}}); }, handle(payload: object) { From 7408bbdb9c531c09094633896572ee18efcb85e5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 15:46:14 +0100 Subject: [PATCH 21/42] Added reactions to typescript --- web/ts/api/stream-reactions.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index b96b7c1e1..01b51f73d 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -25,3 +25,13 @@ export const liveReactionListener = { window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); }, }; + +export function startReactionAnimation(reaction: string) { + const reactionElement = document.getElementById(`reaction-${reaction}`); + if (reactionElement) { + reactionElement.classList.add("opacity-0"); + setTimeout(() => { + reactionElement.classList.remove("opacity-0"); + }, 500); + } +} From dabe7120797d3a6cf41fb54a2c77ce31b644093f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 16:23:24 +0100 Subject: [PATCH 22/42] Reactions now show up on admin panel --- api/stream_reactions.go | 7 +++++-- web/template/admin/lecture-live-management.gohtml | 9 ++++++++- web/ts/api/stream-reactions.ts | 10 ---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 0a6c6bd0e..9692ea039 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -136,6 +136,7 @@ func RegisterReactionUpdateRealtimeChannel(wrapper dao.DaoWrapper) { } func reactionUpdateOnUnsubscribe(psc *realtime.Context) { + logger.Debug("Unsubscribing from reaction Update") ctx, _ := psc.Client.Get("ctx") // get gin context foundContext, exists := ctx.(*gin.Context).Get("TUMLiveContext") if !exists { @@ -163,6 +164,7 @@ func reactionUpdateOnUnsubscribe(psc *realtime.Context) { } else { liveReactionListener[userId].sessions = newSessions } + logger.Debug("Successfully unsubscribed from reaction Update") } func reactionUpdateOnSubscribe(psc *realtime.Context) { @@ -188,12 +190,12 @@ func reactionUpdateOnSubscribe(psc *realtime.Context) { } liveReactionListenerMutex.Lock() + defer liveReactionListenerMutex.Unlock() if liveReactionListener[userId] != nil { liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(liveUpdateListener[userId].sessions, psc), liveReactionListener[userId].stream} } else { liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{[]*realtime.Context{psc}, 0} } - liveReactionListenerMutex.Unlock() } func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { @@ -246,6 +248,7 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { } liveReactionListenerMutex.Lock() + defer liveReactionListenerMutex.Unlock() if liveReactionListener[userId] != nil { uId, err := strconv.Atoi(messageObj.StreamID) if err != nil { @@ -256,11 +259,11 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { } else { logger.Error("User has no live reaction listener") } - liveReactionListenerMutex.Unlock() } func NotifyAdminsOnReaction(streamID uint, reaction string) { liveReactionListenerMutex.Lock() + defer liveReactionListenerMutex.Unlock() reactionStruct := struct { Reaction string `json:"reaction"` }{ diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 1c280f051..f8f31f839 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -111,7 +111,14 @@
-
+
+ + + +
{{if and $course.ChatEnabled $stream.ChatEnabled}} diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index 01b51f73d..b96b7c1e1 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -25,13 +25,3 @@ export const liveReactionListener = { window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); }, }; - -export function startReactionAnimation(reaction: string) { - const reactionElement = document.getElementById(`reaction-${reaction}`); - if (reactionElement) { - reactionElement.classList.add("opacity-0"); - setTimeout(() => { - reactionElement.classList.remove("opacity-0"); - }, 500); - } -} From d803fb23f53c407e67d37e8a9192c332dabadda7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 14 Nov 2024 16:50:00 +0100 Subject: [PATCH 23/42] Fixed opacity bug --- web/template/admin/lecture-live-management.gohtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index f8f31f839..a2a615a1f 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -114,9 +114,9 @@
- +
From bec3075fd0fd085e9cd9ee270e04bb8fe94796f4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 8 Apr 2025 20:52:36 +0200 Subject: [PATCH 24/42] Added percentage to the reactions in admin screen --- api/stream_reactions.go | 70 +++++++++++++++++++ dao/stream-reaction.go | 13 +++- .../admin/lecture-live-management.gohtml | 10 ++- web/ts/api/stream-reactions.ts | 9 ++- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 9692ea039..5558e6bbe 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "errors" "github.com/TUM-Dev/gocast/dao" @@ -133,6 +134,15 @@ func RegisterReactionUpdateRealtimeChannel(wrapper dao.DaoWrapper) { OnMessage: reactionUpdateSetStream, }) daoWrapper = wrapper + + go func() { + // Notify admins every 5 seconds + logger.Info("Starting periodic notification of reaction percentages") + for { + time.Sleep(5 * time.Second) + NotifyAdminsOnReactionPercentages(context.Background()) + } + }() } func reactionUpdateOnUnsubscribe(psc *realtime.Context) { @@ -285,3 +295,63 @@ func NotifyAdminsOnReaction(streamID uint, reaction string) { } } } + +func NotifyAdminsOnReactionPercentages(context context.Context) { + liveReactionListenerMutex.Lock() + defer liveReactionListenerMutex.Unlock() + streams := make([]uint, 0) + for _, session := range liveReactionListener { + streams = append(streams, session.stream) + } + liveReactionListenerMutex.Unlock() + + streamReactionPercentages := map[uint]map[string]float64{} + + for _, stream := range streams { + reactionsRaw, err := daoWrapper.StreamReactionDao.GetByStreamWithinMinutes(context, stream, 2) + if err != nil { + logger.Error("could not get reactions for stream", "stream", stream, "err", err) + return + } + + reactions := make(map[string]int) + for _, reaction := range reactionsRaw { + reactions[reaction.Reaction]++ + } + + totalReactions := 0 + for _, count := range reactions { + totalReactions += count + } + if totalReactions == 0 { + logger.Debug("no reactions for stream", "stream", stream) + continue + } + + streamReactionPercentages[stream] = make(map[string]float64) + for reaction, count := range reactions { + streamReactionPercentages[stream][reaction] = float64(count) / float64(totalReactions) + } + } + + // Send the percentages to the admin sessions + liveReactionListenerMutex.Lock() + + for _, session := range liveReactionListener { + if session.stream == 0 { + continue + } + reactionPercentages := streamReactionPercentages[session.stream] + reactionPercentagesMarshaled, err := json.Marshal(reactionPercentages) + if err != nil { + logger.Error("could not marshal reaction percentages", "err", err) + return + } + for _, s := range session.sessions { + err := s.Send([]byte("{\"percentages\": " + string(reactionPercentagesMarshaled) + "}")) + if err != nil { + logger.Error("can't write reaction percentages to session", "err", err) + } + } + } +} diff --git a/dao/stream-reaction.go b/dao/stream-reaction.go index 3d215a452..fea9d0b7c 100644 --- a/dao/stream-reaction.go +++ b/dao/stream-reaction.go @@ -4,6 +4,7 @@ import ( "context" "github.com/TUM-Dev/gocast/model" "gorm.io/gorm" + "time" ) //go:generate mockgen -source=streamReaction.go -destination ../mock_dao/streamReaction.go @@ -20,6 +21,8 @@ type StreamReactionDao interface { GetByStream(context.Context, uint) ([]model.StreamReaction, error) + GetByStreamWithinMinutes(context.Context, uint, uint) ([]model.StreamReaction, error) + GetNumbersOfReactions(context.Context, uint) (map[string]int, error) GetLastReactionOfUser(context.Context, uint) (model.StreamReaction, error) @@ -30,8 +33,8 @@ type streamReactionDao struct { } type reactionCount struct { - Reaction string - Count int + Reaction string `json:"reaction"` + Count int `json:"count"` } func NewStreamReactionDao() StreamReactionDao { @@ -58,6 +61,12 @@ func (d streamReactionDao) GetByStream(c context.Context, streamID uint) (res [] return res, d.db.WithContext(c).Where("stream_id = ?", streamID).Find(&res).Error } +// GetByStream gets a StreamReaction by stream within the last ... minutes. +func (d streamReactionDao) GetByStreamWithinMinutes(c context.Context, streamID uint, minutes uint) (res []model.StreamReaction, err error) { + time_specified := time.Now().Add(-time.Duration(minutes) * time.Minute) + return res, d.db.WithContext(c).Where("stream_id = ? AND created_at > ?", streamID, time_specified.String()).Find(&res).Error +} + // GetNumbersOfReactions gets the number of reactions grouped by reactions for a stream. func (d streamReactionDao) GetNumbersOfReactions(c context.Context, streamID uint) (map[string]int, error) { var reactionCounts []reactionCount diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index a2a615a1f..d0717ee5d 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -111,12 +111,16 @@
-
+
diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index b96b7c1e1..211a1771f 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -22,6 +22,13 @@ export const liveReactionListener = { }, handle(payload: object) { - window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); + if(payload["reaction"]) { + // TODO: Handle multiple parallel reactions + window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); + } else if(payload["percentages"]) { + window.dispatchEvent(new CustomEvent("reactionupdatepercentages", { detail: { data: payload["percentages"] } })); + }else { + console.log(payload); + } }, }; From 7dcd7deff5726113a01efe7ceb272f24166f301b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 8 Apr 2025 20:53:29 +0200 Subject: [PATCH 25/42] Added todo --- api/stream_reactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 5558e6bbe..149eaacce 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -308,7 +308,7 @@ func NotifyAdminsOnReactionPercentages(context context.Context) { streamReactionPercentages := map[uint]map[string]float64{} for _, stream := range streams { - reactionsRaw, err := daoWrapper.StreamReactionDao.GetByStreamWithinMinutes(context, stream, 2) + reactionsRaw, err := daoWrapper.StreamReactionDao.GetByStreamWithinMinutes(context, stream, 2) // TODO: Make this variable for the lecturer if err != nil { logger.Error("could not get reactions for stream", "stream", stream, "err", err) return From 03faf0c8a45ee1ab60c8ed6c7a89409e14c6a7a9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 13:10:54 +0200 Subject: [PATCH 26/42] Added button to go to live management --- web/template/watch.gohtml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/template/watch.gohtml b/web/template/watch.gohtml index c64232df9..b8aecb2a4 100644 --- a/web/template/watch.gohtml +++ b/web/template/watch.gohtml @@ -490,6 +490,15 @@ href="/admin/stats/{{$course.Model.ID}}/{{$stream.Model.ID}}" title="Watch lecture stats"> + + {{if .IndexData.TUMLiveContext.Stream.LiveNow }} + + + + {{ end }} +
{{end}} From e602b32793d64271642ef72cd73ee9cbce0dd894 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:05:10 +0200 Subject: [PATCH 27/42] Some minor design changes --- .../admin/lecture-live-management.gohtml | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index d0717ee5d..2b4b0ce30 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -37,7 +37,7 @@
-
+
@@ -45,7 +45,7 @@
-
+
@@ -111,17 +111,19 @@
-
- - - +
+
+ + + +
From 5a25136870d48320595c968713dcb47ac32e7e16 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:07:23 +0200 Subject: [PATCH 28/42] Removed debugging information --- api/stream_reactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 149eaacce..f3599a7ad 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -324,7 +324,7 @@ func NotifyAdminsOnReactionPercentages(context context.Context) { totalReactions += count } if totalReactions == 0 { - logger.Debug("no reactions for stream", "stream", stream) + //logger.Debug("no reactions for stream", "stream", stream) continue } From 1d39bf090e10c173df691d4d01cfdbd3473675fb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 19:50:41 +0100 Subject: [PATCH 29/42] Live lecture management page is only usable if stream is live --- web/admin.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/admin.go b/web/admin.go index a9d27ee13..f03d78b88 100644 --- a/web/admin.go +++ b/web/admin.go @@ -251,6 +251,18 @@ func (r mainRoutes) LectureLiveManagementPage(c *gin.Context) { tumLiveContext := foundContext.(tools.TUMLiveContext) indexData := NewIndexData() indexData.TUMLiveContext = tumLiveContext + stream := tumLiveContext.Stream + + if stream == nil { + tools.RenderErrorPage(c, http.StatusNotFound, "Lecture not found") + return + } + + if !stream.LiveNow { + tools.RenderErrorPage(c, http.StatusNotFound, "Lecture is not live") + return + } + if err := templateExecutor.ExecuteTemplate(c.Writer, "lecture-live-management.gohtml", LiveLectureManagementData{ IndexData: indexData, Lecture: *tumLiveContext.Stream, From 3f52e9504d71112f356a12d2486c25357e0e8963 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Nov 2024 19:54:26 +0100 Subject: [PATCH 30/42] Changed chat layout --- web/template/admin/lecture-live-management.gohtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 2b4b0ce30..06eb883b4 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -129,7 +129,7 @@ {{if and $course.ChatEnabled $stream.ChatEnabled}}
+ style="height: 80vh; width: 40vw" class="pr-5">
{{template "chat-component" .ChatData}}
From 4c025575bd4609ecde339f3ba4220d99cbea901e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 12 Nov 2024 14:59:19 +0100 Subject: [PATCH 31/42] Added Restart and Stop button for stream --- .../admin/lecture-live-management.gohtml | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/web/template/admin/lecture-live-management.gohtml b/web/template/admin/lecture-live-management.gohtml index 06eb883b4..5991ca1de 100644 --- a/web/template/admin/lecture-live-management.gohtml +++ b/web/template/admin/lecture-live-management.gohtml @@ -67,8 +67,20 @@ {{.Lecture.Description}} {{end}}
- - + + +
+ + +
@@ -140,6 +152,39 @@
+ +
+
+
+
+ +
+

Keep the recording after ending the stream?

+
+ +
+ + + +
+
+
+
+
+ + @@ -81,6 +82,15 @@ Stop
+ +
@@ -138,6 +148,8 @@
+ + {{if and $course.ChatEnabled $stream.ChatEnabled}}
watch.setHighestQuality(), 2000); watch.periodicCurrentTime("time"); watch.videoStatListener.listen(); + + admin.loadLectureStats("lecture", "lectureLiveStats", "{{.Lecture.Model.ID}}"); + admin.initLectureStatsPage("{{.Lecture.Model.ID}}"); From b3b6957189075d12a5f69f7a45cba05eb9e1b575 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:24:08 +0200 Subject: [PATCH 33/42] eslint fix --- web/ts/watch.ts | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/web/ts/watch.ts b/web/ts/watch.ts index 91ceb142f..f81cda8e5 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -204,49 +204,6 @@ export function setHighestQuality() { }); } -export function pauseVideo() { - const player = getPlayers()[0]; - player.pause(); -} - -export function seekToLive() { - const players = getPlayers(); - console.log("Seeking to live edge"); - console.debug(players); - players.forEach((player) => { - player.liveTracker.seekToLiveEdge(); - }); -} - -function getHighestQualityLevel(qualityLevels : any[]): number { - let highestQuality = qualityLevels[0]; - for(let i = 1; i < qualityLevels.length; i++) { - if(qualityLevels[i].height > highestQuality.height) { - highestQuality = qualityLevels[i]; - } - } - return qualityLevels.indexOf(highestQuality); -} - -export function setHighestQuality() { - const players = getPlayers(); - console.debug(players); - players.forEach((player) => { - let qualityLevels = (player as any).qualityLevels(); - let highestQuality = getHighestQualityLevel(qualityLevels.levels_); - // Listen to change events for when the player selects a new quality level - qualityLevels.on('change', function() { - console.debug('Quality Level changed!'); - console.debug('New level:', qualityLevels[qualityLevels.selectedIndex]); - }); - qualityLevels.trigger({ type: 'change', selectedIndex: highestQuality }); - qualityLevels.selectedIndex_ = highestQuality; - for(let i = 0; i < qualityLevels.length; i++) { - qualityLevels[i].enabled = i == highestQuality; - } - }); -} - export { repeatHeatMap } from "./repeat-heatmap"; export { seekbarHighlights, MarkerType } from "./seekbar-highlights"; export { seekbarOverlay, SeekbarHoverPosition } from "./seekbar-overlay"; From f84d02ab4b3154178c44d3d9c0a729eac0768922 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:25:47 +0200 Subject: [PATCH 34/42] Fixed watch.ts --- web/ts/watch.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/ts/watch.ts b/web/ts/watch.ts index f81cda8e5..c7c96f8bb 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -196,7 +196,7 @@ export function setHighestQuality() { console.debug("Quality Level changed!"); console.debug("New level:", qualityLevels[qualityLevels.selectedIndex]); }); - qualityLevels.trigger({ type: "change", selectedIndex: highestQuality }); + qualityLevels.trigger({type: "change", selectedIndex: highestQuality}); qualityLevels.selectedIndex_ = highestQuality; for (let i = 0; i < qualityLevels.length; i++) { qualityLevels[i].enabled = i == highestQuality; @@ -204,6 +204,11 @@ export function setHighestQuality() { }); } +export function pauseVideo() { + const player = getPlayers()[0]; + player.pause(); +} + export { repeatHeatMap } from "./repeat-heatmap"; export { seekbarHighlights, MarkerType } from "./seekbar-highlights"; export { seekbarOverlay, SeekbarHoverPosition } from "./seekbar-overlay"; From 297bd096599c6f286c2579cb40438cf59b6cf9a3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:27:14 +0200 Subject: [PATCH 35/42] eslint fix 2 --- web/ts/watch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ts/watch.ts b/web/ts/watch.ts index c7c96f8bb..650832823 100644 --- a/web/ts/watch.ts +++ b/web/ts/watch.ts @@ -196,7 +196,7 @@ export function setHighestQuality() { console.debug("Quality Level changed!"); console.debug("New level:", qualityLevels[qualityLevels.selectedIndex]); }); - qualityLevels.trigger({type: "change", selectedIndex: highestQuality}); + qualityLevels.trigger({ type: "change", selectedIndex: highestQuality }); qualityLevels.selectedIndex_ = highestQuality; for (let i = 0; i < qualityLevels.length; i++) { qualityLevels[i].enabled = i == highestQuality; From 09733ab52862d368a6a61801c5003dcc1e99ef8a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:36:14 +0200 Subject: [PATCH 36/42] eslint fix --- web/ts/api/stream-reactions.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/web/ts/api/stream-reactions.ts b/web/ts/api/stream-reactions.ts index 211a1771f..d2e0bae5c 100644 --- a/web/ts/api/stream-reactions.ts +++ b/web/ts/api/stream-reactions.ts @@ -1,7 +1,6 @@ -import {getData, postData} from "../global"; -import {get} from "../utilities/fetch-wrappers"; -import {Realtime, RealtimeMessageTypes} from "../socket"; - +import { getData, postData } from "../global"; +import { get } from "../utilities/fetch-wrappers"; +import { Realtime, RealtimeMessageTypes } from "../socket"; // Function to add a reaction to a stream export function addReaction(reaction: string, streamID: number) { @@ -18,16 +17,21 @@ export function getAllowedReactions(streamID: number): Promise { export const liveReactionListener = { async init(streamId: string) { await Realtime.get().subscribeChannel("reaction-update", this.handle); - await Realtime.get().send("reaction-update", {type: RealtimeMessageTypes.RealtimeMessageTypeChannelMessage, payload: {"streamID": streamId}}); + await Realtime.get().send("reaction-update", { + type: RealtimeMessageTypes.RealtimeMessageTypeChannelMessage, + payload: { streamID: streamId }, + }); }, handle(payload: object) { - if(payload["reaction"]) { + if (payload["reaction"]) { // TODO: Handle multiple parallel reactions window.dispatchEvent(new CustomEvent("reactionupdate", { detail: { data: payload } })); - } else if(payload["percentages"]) { - window.dispatchEvent(new CustomEvent("reactionupdatepercentages", { detail: { data: payload["percentages"] } })); - }else { + } else if (payload["percentages"]) { + window.dispatchEvent( + new CustomEvent("reactionupdatepercentages", { detail: { data: payload["percentages"] } }), + ); + } else { console.log(payload); } }, From ddaab5b9b198cc34be64db698b1fb18af0c2630f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:41:00 +0200 Subject: [PATCH 37/42] Gofumpted --- api/stream_reactions.go | 13 +++++++------ dao/stream-reaction.go | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index f3599a7ad..7d68e17c8 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -4,17 +4,18 @@ import ( "context" "encoding/json" "errors" + "net/http" + "slices" + "strconv" + "sync" + "time" + "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" "github.com/TUM-Dev/gocast/tools" "github.com/TUM-Dev/gocast/tools/realtime" "github.com/getsentry/sentry-go" "github.com/gin-gonic/gin" - "net/http" - "slices" - "strconv" - "sync" - "time" ) type StreamReactionRoutes struct { @@ -324,7 +325,7 @@ func NotifyAdminsOnReactionPercentages(context context.Context) { totalReactions += count } if totalReactions == 0 { - //logger.Debug("no reactions for stream", "stream", stream) + // logger.Debug("no reactions for stream", "stream", stream) continue } diff --git a/dao/stream-reaction.go b/dao/stream-reaction.go index fea9d0b7c..ec73b81b3 100644 --- a/dao/stream-reaction.go +++ b/dao/stream-reaction.go @@ -2,9 +2,10 @@ package dao import ( "context" + "time" + "github.com/TUM-Dev/gocast/model" "gorm.io/gorm" - "time" ) //go:generate mockgen -source=streamReaction.go -destination ../mock_dao/streamReaction.go @@ -71,7 +72,6 @@ func (d streamReactionDao) GetByStreamWithinMinutes(c context.Context, streamID func (d streamReactionDao) GetNumbersOfReactions(c context.Context, streamID uint) (map[string]int, error) { var reactionCounts []reactionCount err := d.db.WithContext(c).Model(&model.StreamReaction{}).Where("stream_id = ?", streamID).Group("reaction").Select("reaction, count(reaction) as count").Scan(&reactionCounts).Error - if err != nil { return nil, err } From 377084f66d736e023ba53de21edad9fa461241b9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 17 Apr 2025 16:45:06 +0200 Subject: [PATCH 38/42] Some lint fixes --- api/stream_reactions.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 7d68e17c8..fca847c39 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -157,7 +157,7 @@ func reactionUpdateOnUnsubscribe(psc *realtime.Context) { tumLiveContext := foundContext.(tools.TUMLiveContext) - var userId uint = 0 + var userId uint if tumLiveContext.User != nil { userId = tumLiveContext.User.ID } @@ -189,7 +189,7 @@ func reactionUpdateOnSubscribe(psc *realtime.Context) { tumLiveContext := foundContext.(tools.TUMLiveContext) - var userId uint = 0 + var userId uint var err error if tumLiveContext.User != nil { @@ -221,7 +221,7 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { tumLiveContext := foundContext.(tools.TUMLiveContext) - var userId uint = 0 + var userId uint var err error if tumLiveContext.User != nil { @@ -242,12 +242,12 @@ func reactionUpdateSetStream(psc *realtime.Context, message *realtime.Message) { return } - stream, err := daoWrapper.StreamsDao.GetStreamByID(nil, messageObj.StreamID) + stream, err := daoWrapper.StreamsDao.GetStreamByID(context.TODO(), messageObj.StreamID) if err != nil { logger.Error("Cant get stream by id", "err", err) return } - course, err := daoWrapper.CoursesDao.GetCourseById(nil, stream.CourseID) + course, err := daoWrapper.CoursesDao.GetCourseById(context.TODO(), stream.CourseID) if err != nil { logger.Error("Cant get course by id", "err", err) return @@ -288,7 +288,7 @@ func NotifyAdminsOnReaction(streamID uint, reaction string) { for _, session := range liveReactionListener { if session.stream == streamID { for _, s := range session.sessions { - err := s.Send([]byte(reactionMarshaled)) + err := s.Send(reactionMarshaled) if err != nil { logger.Error("can't write reaction to session", "err", err) } From a6c16d7a5cef3e53c5e2bebf9f8f68240a904327 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 11 May 2025 15:31:34 +0200 Subject: [PATCH 39/42] Small api change so gocast does not crash --- api/stream_reactions.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index fca847c39..82c9bef34 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -202,8 +202,9 @@ func reactionUpdateOnSubscribe(psc *realtime.Context) { liveReactionListenerMutex.Lock() defer liveReactionListenerMutex.Unlock() - if liveReactionListener[userId] != nil { - liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(liveUpdateListener[userId].sessions, psc), liveReactionListener[userId].stream} + existing := liveReactionListener[userId] + if existing != nil { + liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{append(existing.sessions, psc), liveReactionListener[userId].stream} } else { liveReactionListener[userId] = &liveReactionAdminSessionsWrapper{[]*realtime.Context{psc}, 0} } From c4d58b1f3802062ffe8b47fea931467cb9dfcfcd Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 18 Jun 2025 13:43:24 +0200 Subject: [PATCH 40/42] Some fixes --- api/stream_reactions.go | 7 +++---- config.yaml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/stream_reactions.go b/api/stream_reactions.go index 82c9bef34..e0a1a7e60 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -28,8 +28,6 @@ func (r StreamReactionRoutes) allowedReactions(c *gin.Context) { } func (r StreamReactionRoutes) addReaction(c *gin.Context) { - cooldownSeconds := 10 - tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) user := tumLiveContext.User stream := tumLiveContext.Stream @@ -74,7 +72,7 @@ func (r StreamReactionRoutes) addReaction(c *gin.Context) { return } - // TODO: This can be modified to allow different reactions for different streams + // This can be modified to allow different reactions for different streams if !slices.Contains(tools.Cfg.AllowedReactions, reaction.Reaction) { _ = c.Error(tools.RequestError{ Status: http.StatusBadRequest, @@ -84,7 +82,8 @@ func (r StreamReactionRoutes) addReaction(c *gin.Context) { } lastReaction, _ := r.DaoWrapper.StreamReactionDao.GetLastReactionOfUser(c, user.ID) - if lastReaction.Reaction != "" && lastReaction.CreatedAt.Add(time.Duration(cooldownSeconds)*time.Second).After(time.Now()) { + // This contains the cooldown logic, to change this value change the time.Duration(10) to the desired cooldown time + if lastReaction.Reaction != "" && lastReaction.CreatedAt.Add(time.Duration(10)*time.Second).After(time.Now()) { _ = c.Error(tools.RequestError{ Status: http.StatusTooManyRequests, CustomMessage: "cooldown not over", diff --git a/config.yaml b/config.yaml index 6b730547b..e02c8dfb0 100644 --- a/config.yaml +++ b/config.yaml @@ -108,4 +108,4 @@ allowedReactions: - 😊 - 👍 - 👎 - - 😢 \ No newline at end of file + - 😢 From 9d4356f844a60b4a4d6a340c6c55248b6a5f6e47 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 16 Oct 2025 09:23:37 +0200 Subject: [PATCH 41/42] Minor fixes after merge --- api/router.go | 2 +- api/stream_reactions.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api/router.go b/api/router.go index 06e73386a..4c0651c04 100755 --- a/api/router.go +++ b/api/router.go @@ -20,8 +20,8 @@ func ConfigRealtimeRouter(router *gin.RouterGroup) { // Register Channels RegisterLiveUpdateRealtimeChannel() - RegisterReactionUpdateRealtimeChannel(daoWrapper) RegisterLiveRunnerPageUpdateRealtimeChannel(daoWrapper) + RegisterReactionUpdateRealtimeChannel() RegisterRealtimeChatChannel() } diff --git a/api/stream_reactions.go b/api/stream_reactions.go index e0a1a7e60..c09750f56 100644 --- a/api/stream_reactions.go +++ b/api/stream_reactions.go @@ -119,7 +119,6 @@ const ( var ( liveReactionListenerMutex sync.RWMutex liveReactionListener = map[uint]*liveReactionAdminSessionsWrapper{} - daoWrapper dao.DaoWrapper ) type liveReactionAdminSessionsWrapper struct { @@ -127,13 +126,12 @@ type liveReactionAdminSessionsWrapper struct { stream uint } -func RegisterReactionUpdateRealtimeChannel(wrapper dao.DaoWrapper) { +func RegisterReactionUpdateRealtimeChannel() { RealtimeInstance.RegisterChannel(ReactionUpdateRoomName, realtime.ChannelHandlers{ OnSubscribe: reactionUpdateOnSubscribe, OnUnsubscribe: reactionUpdateOnUnsubscribe, OnMessage: reactionUpdateSetStream, }) - daoWrapper = wrapper go func() { // Notify admins every 5 seconds From 8fbd965a741a8d604c043e2d1e2005a1a875e1b6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 16 Oct 2025 09:28:53 +0200 Subject: [PATCH 42/42] golangci-lint --- mock_dao/stream-reaction.go | 2 +- web/admin.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mock_dao/stream-reaction.go b/mock_dao/stream-reaction.go index 6dbc0e973..1ca568889 100644 --- a/mock_dao/stream-reaction.go +++ b/mock_dao/stream-reaction.go @@ -9,7 +9,7 @@ import ( reflect "reflect" model "github.com/TUM-Dev/gocast/model" - gomock "github.com/golang/mock/gomock" + "go.uber.org/mock/gomock" ) // MockStreamReactionDao is a mock of StreamReactionDao interface. diff --git a/web/admin.go b/web/admin.go index a5383f685..4a4afc537 100644 --- a/web/admin.go +++ b/web/admin.go @@ -5,9 +5,9 @@ import ( "encoding/json" "errors" "fmt" - "strings" "net/http" "regexp" + "strings" "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model"