From a21dcb581808fdd03382e9b7accd39f4946b86d6 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Wed, 3 Jul 2024 10:12:28 -0300 Subject: [PATCH] Introduces /api/feedback endpoint (to replace Meteor /feedback) --- .../api/model/request/Feedback.java | 44 +++++++++++ .../api/service/ValidationService.java | 2 + .../ui/components/meeting-ended/component.tsx | 13 +++- .../private/config/settings.yml | 2 +- .../org/bigbluebutton/web/UrlMappings.groovy | 4 + .../web/controllers/ApiController.groovy | 75 +++++++++++++++++++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/Feedback.java diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/Feedback.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/Feedback.java new file mode 100755 index 0000000000..65a61f369c --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/Feedback.java @@ -0,0 +1,44 @@ +package org.bigbluebutton.api.model.request; + +import org.bigbluebutton.api.model.constraint.UserSessionConstraint; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +public class Feedback extends RequestWithSession { + + public enum Params implements RequestParameters { + SESSION_TOKEN("sessionToken"); + + private final String value; + + Params(String value) { this.value = value; } + + public String getValue() { return value; } + } + + @UserSessionConstraint + private String sessionToken; + + public Feedback(HttpServletRequest servletRequest) { + super(servletRequest); + } + + public String getSessionToken() { + return sessionToken; + } + + public void setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + } + + @Override + public void populateFromParamsMap(Map params) { + if(params.containsKey(Feedback.Params.SESSION_TOKEN.getValue())) setSessionToken(params.get(Feedback.Params.SESSION_TOKEN.getValue())[0]); + } + + @Override + public void convertParamsFromString() { + + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java index ade48b8dc6..1b82d6233b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java @@ -44,6 +44,7 @@ public class ValidationService { SIGN_OUT("signOut", RequestType.GET), LEARNING_DASHBOARD("learningDashboard", RequestType.GET), GET_JOIN_URL("getJoinUrl", RequestType.GET), + FEEDBACK("feedback", RequestType.GET), INSERT_DOCUMENT("insertDocument", RequestType.GET), SEND_CHAT_MESSAGE("sendChatMessage", RequestType.GET); @@ -130,6 +131,7 @@ public class ValidationService { case SIGN_OUT -> new SignOut(servletRequest); case LEARNING_DASHBOARD -> new LearningDashboard(servletRequest); case GET_JOIN_URL -> new GetJoinUrl(servletRequest); + case FEEDBACK -> new Feedback(servletRequest); }; } diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.tsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.tsx index 8c53ac907b..c0866a4a81 100644 --- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.tsx @@ -227,7 +227,16 @@ const MeetingEnded: React.FC = ({ comment, isModerator, }; - const url = './feedback'; + + const pathMatch = window.location.pathname.match('^(.*)/html5client/join$'); + if (pathMatch == null) { + throw new Error('Failed to match BBB client URI'); + } + const serverPathPrefix = pathMatch[1]; + + const sessionToken = sessionStorage.getItem('sessionToken'); + + const url = `https://${window.location.hostname}${serverPathPrefix}/bigbluebutton/api/feedback?sessionToken=${sessionToken}`; const options = { method: 'POST', body: JSON.stringify(message), @@ -351,7 +360,7 @@ const MeetingEnded: React.FC = ({ ) : null} ); - }, []); + }, [askForFeedbackOnLogout, dispatched, selectedStars]); useEffect(() => { // Sets Loading to falsed and removes loading splash screen diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 87e4b4fa5b..eac80947ed 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -56,7 +56,7 @@ public: customStyleUrl: null darkTheme: enabled: true - askForFeedbackOnLogout: false + askForFeedbackOnLogout: true # the default logoutUrl matches window.location.origin i.e. bigbluebutton.org for demo.bigbluebutton.org # in some cases we want only custom logoutUrl to be used when provided on meeting create. Default value: true askForConfirmationOnLeave: true diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy index 3e4d293983..7c4f5a2ca2 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy @@ -111,6 +111,10 @@ class UrlMappings { action = [GET: 'getJoinUrl', POST: 'getJoinUrl'] } + "/bigbluebutton/api/feedback"(controller: "api") { + action = [POST: 'feedback'] + } + "/bigbluebutton/api/learningDashboard"(controller: "api") { action = [GET: 'learningDashboard', POST: 'learningDashboard'] } diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 2f8d37c1a2..775824b55c 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -19,6 +19,7 @@ package org.bigbluebutton.web.controllers import com.google.gson.Gson +import com.google.gson.JsonObject import grails.web.context.ServletContextHolder import groovy.json.JsonBuilder import groovy.xml.MarkupBuilder @@ -31,6 +32,7 @@ import org.bigbluebutton.api.* import org.bigbluebutton.api.domain.GuestPolicy import org.bigbluebutton.api.domain.Meeting import org.bigbluebutton.api.domain.UserSession +import org.bigbluebutton.api.domain.UserSessionBasicData import org.bigbluebutton.api.service.ValidationService import org.bigbluebutton.api.service.ServiceUtils import org.bigbluebutton.api.util.ParamsUtil @@ -1217,6 +1219,79 @@ class ApiController { } } + def feedback = { + String API_CALL = 'feedback' + log.debug CONTROLLER_NAME + "#${API_CALL}" + + if (!params.sessionToken) { + invalid("missingSession", "Invalid session token") + } + + String requestBody = request.inputStream == null ? null : request.inputStream.text + Gson gson = new Gson() + JsonObject body = gson.fromJson(requestBody, JsonObject.class) + + if (!body + || !body.has("userName") + || !body.has("authToken") + || !body.has("comment") + || !body.has("rating")) { + invalid("missingParameters", "One or more required parameters are missing") + return + } + + String userName = "[unconfirmed] " + body.get("userName").getAsString() + String meetingId = "" + String userId = "" + String authToken = body.get("authToken").getAsString() + String comment = body.get("comment").getAsString() + int rating = body.get("rating").getAsInt() + + String sessionToken = sanitizeSessionToken(params.sessionToken) + UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken) + + if(userSession) { + userName = userSession.fullname + userId = userSession.internalUserId + meetingId = userSession.meetingID + } else { + //Usually the session was already removed when the user send the feedback + UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) + if(removedUserSession) { + userId = removedUserSession.userId + meetingId = removedUserSession.meetingId + } + } + + if(userId == "") { + invalid("invalidSession", "Invalid Session") + } + + response.contentType = 'application/json' + response.setStatus(200) + withFormat { + json { + def builder = new JsonBuilder() + builder { + "status" "ok" + } + render(contentType: "application/json", text: builder.toPrettyString()) + } + } + + def feedback = [ + meetingId: meetingId, + userId: userId, + authToken: authToken, + userName: userName, + comment: comment, + rating: rating + ] + + log.info("FEEDBACK LOG: ${feedback}") + } + + /*********************************************** * LEARNING DASHBOARD DATA ***********************************************/