Page MenuHomePhorge

D1912.1775315073.diff
No OneTemporary

Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None

D1912.1775315073.diff

diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
--- a/src/app/Http/Controllers/API/V4/OpenViduController.php
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -11,6 +11,68 @@
class OpenViduController extends Controller
{
/**
+ * Accepting the room join request.
+ *
+ * @param string $id Room identifier (name)
+ * @param string $reqid Request identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function acceptJoinRequest($id, $reqid)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->requestAccept($reqid)) {
+ return $this->errorResponse(500, \trans('meet.session-request-accept-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
+ /**
+ * Denying the room join request.
+ *
+ * @param string $id Room identifier (name)
+ * @param string $reqid Request identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function denyJoinRequest($id, $reqid)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->requestDeny($reqid)) {
+ return $this->errorResponse(500, \trans('meet.session-request-deny-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
+ /**
* Close the room session.
*
* @param string $id Room identifier (name)
@@ -44,6 +106,37 @@
}
/**
+ * Accepting the room join request.
+ *
+ * @param string $id Room identifier (name)
+ * @param string $conn Connection identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function dismissConnection($id, $conn)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->closeOVConnection($conn)) {
+ return $this->errorResponse(500, \trans('meet.session-dismiss-connection-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
+ /**
* Listing of rooms that belong to the current user.
*
* @return \Illuminate\Http\JsonResponse
@@ -136,19 +229,53 @@
if (!$isOwner && strlen($password)) {
$request_password = request()->input('password');
if ($request_password !== $password) {
- // Note: We send the config to the client so it knows to display the password field
- $response = [
- 'config' => $config,
- 'message' => \trans('meet.session-password-error'),
- 'status' => 'error',
- ];
-
- return response()->json($response, 425);
+ return $this->errorResponse(425, \trans('meet.session-password-error'), ['config' => $config]);
+ }
+ }
+
+ // Handle locked room
+ if (!$isOwner && $config['locked']) {
+ $nickname = request()->input('nickname');
+ $picture = request()->input('picture');
+ $requestId = request()->input('requestId');
+
+ $request = $requestId ? $room->requestGet($requestId) : null;
+
+ $error = \trans('meet.session-room-locked-error');
+
+ // Request already has been processed (not accepted yet, but it could be denied)
+ if (empty($request['status']) || $request['status'] != Room::REQUEST_ACCEPTED) {
+ if (!$request) {
+ if (empty($nickname) || empty($requestId) || !preg_match('/^[a-z0-9]{8,32}$/i', $requestId)) {
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ if (empty($picture)) {
+ $svg = file_get_contents(resource_path('images/user.svg'));
+ $picture = 'data:image/svg+xml;base64,' . base64_encode($svg);
+ } elseif (!preg_match('|^data:image/png;base64,[a-zA-Z0-9=+/]+$|', $picture)) {
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ // TODO: Resize when big/make safe the user picture?
+
+ $request = ['nickname' => $nickname, 'requestId' => $requestId, 'picture' => $picture];
+
+ if (!$room->requestSave($requestId, $request)) {
+ // FIXME: should we use error code 500?
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ // Send the request (signal) to the owner
+ $result = $room->signal('joinRequest', $request, Room::ROLE_MODERATOR);
+ }
+
+ return $this->errorResponse(427, $error, ['config' => $config]);
}
}
// Create session token for the current user/connection
- $response = $room->getSessionToken('PUBLISHER');
+ $response = $room->getSessionToken($isOwner ? Room::ROLE_MODERATOR : Room::ROLE_PUBLISHER);
if (empty($response)) {
return $this->errorResponse(500, \trans('meet.session-join-error'));
@@ -156,7 +283,7 @@
// Create session token for screen sharing connection
if (!empty(request()->input('screenShare'))) {
- $add_token = $room->getSessionToken('PUBLISHER');
+ $add_token = $room->getSessionToken(Room::ROLE_PUBLISHER);
$response['shareToken'] = $add_token['token'];
}
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -19,10 +19,11 @@
*
* @param int $code Error code
* @param string $message Error message
+ * @param array $data Additional response data
*
* @return \Illuminate\Http\JsonResponse
*/
- protected function errorResponse(int $code, string $message = null)
+ protected function errorResponse(int $code, string $message = null, array $data = [])
{
$errors = [
400 => "Bad request",
@@ -39,6 +40,10 @@
'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"),
];
+ if (!empty($data)) {
+ $response = $response + $data;
+ }
+
return response()->json($response, $code);
}
}
diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php
--- a/src/app/OpenVidu/Room.php
+++ b/src/app/OpenVidu/Room.php
@@ -4,6 +4,7 @@
use App\Traits\SettingsTrait;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Cache;
/**
* The eloquent definition of a Room.
@@ -17,6 +18,13 @@
{
use SettingsTrait;
+ public const ROLE_MODERATOR = 'MODERATOR';
+ public const ROLE_PUBLISHER = 'PUBLISHER';
+ public const ROLE_SUBSCRIBER = 'SUBSCRIBER';
+
+ public const REQUEST_ACCEPTED = 'accepted';
+ public const REQUEST_DENIED = 'denied';
+
protected $fillable = [
'user_id',
'name'
@@ -53,6 +61,54 @@
}
/**
+ * Destroy a OpenVidu connection
+ *
+ * @param string $conn Connection identifier
+ *
+ * @return bool True on success, False otherwise
+ * @throws \Exception if session does not exist
+ */
+ public function closeOVConnection($conn): bool
+ {
+ if (!$this->session_id) {
+ throw new \Exception("The room session does not exist");
+ }
+
+ $url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn);
+
+ $response = $this->client()->request('DELETE', $url);
+
+ return $response->getStatusCode() == 204;
+ }
+
+ /**
+ * Fetch a OpenVidu connection information.
+ *
+ * @param string $conn Connection identifier
+ *
+ * @return ?array Connection data on success, Null otherwise
+ * @throws \Exception if session does not exist
+ */
+ public function getOVConnection($conn): ?array
+ {
+ // Note: getOVConnection() not getConnection() because Eloquent\Model::getConnection() exists
+ // TODO: Maybe use some other name? getParticipant?
+ if (!$this->session_id) {
+ throw new \Exception("The room session does not exist");
+ }
+
+ $url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn);
+
+ $response = $this->client()->request('GET', $url);
+
+ if ($response->getStatusCode() == 200) {
+ return json_decode($response->getBody(), true);
+ }
+
+ return null;
+ }
+
+ /**
* Create a OpenVidu session
*
* @return array|null Session data on success, NULL otherwise
@@ -73,6 +129,7 @@
if ($response->getStatusCode() !== 200) {
$this->session_id = null;
$this->save();
+ return null;
}
$session = json_decode($response->getBody(), true);
@@ -114,7 +171,7 @@
*
* @return array|null Token data on success, NULL otherwise
*/
- public function getSessionToken($role = 'PUBLISHER'): ?array
+ public function getSessionToken($role = self::ROLE_PUBLISHER): ?array
{
$response = $this->client()->request(
'POST',
@@ -163,6 +220,75 @@
}
/**
+ * Accept the join request.
+ *
+ * @param string $id Request identifier
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestAccept(string $id): bool
+ {
+ $request = Cache::get($this->session_id . '-' . $id);
+
+ if ($request) {
+ $request['status'] = self::REQUEST_ACCEPTED;
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
+ return false;
+ }
+
+ /**
+ * Deny the join request.
+ *
+ * @param string $id Request identifier
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestDeny(string $id): bool
+ {
+ $request = Cache::get($this->session_id . '-' . $id);
+
+ if ($request) {
+ $request['status'] = self::REQUEST_DENIED;
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the join request data.
+ *
+ * @param string $id Request identifier
+ *
+ * @return array|null Request data (e.g. nickname, status, picture?)
+ */
+ public function requestGet(string $id): ?array
+ {
+ return Cache::get($this->session_id . '-' . $id);
+ }
+
+ /**
+ * Save the join request.
+ *
+ * @param string $id Request identifier
+ * @param array $request Request data
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestSave(string $id, array $request): bool
+ {
+ // We don't really need the picture in the cache
+ // As we use this cache for the request status only
+ unset($request['picture']);
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
+ /**
* Any (additional) properties of this room.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@@ -171,4 +297,65 @@
{
return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id');
}
+
+ /**
+ * Send a OpenVidu signal to the session participants (connections)
+ *
+ * @param string $name Signal name (type)
+ * @param array $data Signal data array
+ * @param array|string $target List of target connections, Null for all connections.
+ * It can be also a participant role.
+ *
+ * @return bool True on success, False on failure
+ * @throws \Exception if session does not exist
+ */
+ public function signal(string $name, array $data = [], $target = null): bool
+ {
+ if (!$this->session_id) {
+ throw new \Exception("The room session does not exist");
+ }
+
+ $post = [
+ 'session' => $this->session_id,
+ 'type' => $name,
+ 'data' => $data ? json_encode($data) : '',
+ ];
+
+ // Get connection IDs by participant role
+ if (is_string($target)) {
+ // TODO: We should probably store this in our database/redis. I foresee a use-case
+ // for such a connections store on our side, e.g. keeping participant
+ // metadata, e.g. selected language, extra roles like a "language interpreter", etc.
+
+ $response = $this->client()->request('GET', 'sessions/' . $this->session_id);
+
+ if ($response->getStatusCode() !== 200) {
+ return false;
+ }
+
+ $json = json_decode($response->getBody(), true);
+ $connections = [];
+
+ foreach ($json['connections']['content'] as $connection) {
+ if ($connection['role'] === $target) {
+ $connections[] = $connection['id'];
+ break;
+ }
+ }
+
+ if (empty($connections)) {
+ return false;
+ }
+
+ $target = $connections;
+ }
+
+ if (!empty($target)) {
+ $post['to'] = $target;
+ }
+
+ $response = $this->client()->request('POST', 'signal', ['json' => $post]);
+
+ return $response->getStatusCode() == 200;
+ }
}
diff --git a/src/resources/images/user.svg b/src/resources/images/user.svg
new file mode 100644
--- /dev/null
+++ b/src/resources/images/user.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 96c48.6 0 88 39.4 88 88s-39.4 88-88 88-88-39.4-88-88 39.4-88 88-88zm0 344c-58.7 0-111.3-26.6-146.5-68.2 18.8-35.4 55.6-59.8 98.5-59.8 2.4 0 4.8.4 7.1 1.1 13 4.2 26.6 6.9 40.9 6.9 14.3 0 28-2.7 40.9-6.9 2.3-.7 4.7-1.1 7.1-1.1 42.9 0 79.7 24.4 98.5 59.8C359.3 421.4 306.7 448 248 448z"/></svg>
\ No newline at end of file
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
--- a/src/resources/js/meet/app.js
+++ b/src/resources/js/meet/app.js
@@ -14,6 +14,7 @@
let audioSource = '' // Currently selected microphone
let videoSource = '' // Currently selected camera
let sessionData // Room session metadata
+ let role // Current user role
let screenOV // OpenVidu object to initialize a screen sharing session
let screenSession // Session object where the user will connect for screen sharing
@@ -70,7 +71,7 @@
* Join the room session
*
* @param data Session metadata and event handlers (session, token, shareToken, nickname,
- * chatElement, menuElement, onDestroy)
+ * chatElement, menuElement, onDestroy, onJoinRequest)
*/
function joinRoom(data) {
resize();
@@ -90,6 +91,7 @@
session.on('connectionCreated', event => {
// Ignore the current user connection
if (event.connection.role) {
+ role = event.connection.role
return
}
@@ -105,6 +107,7 @@
let connectionId = event.connection.connectionId
let metadata = JSON.parse(event.connection.data)
+ metadata.connId = connectionId
let wrapper = videoWrapperCreate(container, metadata)
connections[connectionId] = {
@@ -414,7 +417,7 @@
*/
function signalEventHandler(signal) {
let conn, data
- let connId = signal.from.connectionId
+ let connId = signal.from ? signal.from.connectionId : null
switch (signal.type) {
case 'signal:userChanged':
@@ -431,6 +434,12 @@
data.id = connId
pushChatMessage(data)
break
+
+ case 'signal:joinRequest':
+ if (sessionData.onJoinRequest) {
+ sessionData.onJoinRequest(JSON.parse(signal.data))
+ }
+ break;
}
}
@@ -645,26 +654,31 @@
function videoWrapperCreate(container, params) {
// Create the element
let wrapper = $('<div class="meet-video">').html(
- `${svgIcon("user", 'fas', 'watermark')}
- <div class="nickname" title="Nickname">
- <span></span>
- <button type="button" class="btn btn-link">${svgIcon('user')}</button>
- </div>
- <div class="controls">
- <button type="button" class="btn btn-link link-audio hidden" title="Mute audio">${svgIcon('volume-mute')}</button>
- <button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">${svgIcon('expand')}</button>
- <button type="button" class="btn btn-link link-fullscreen open hidden" title="Full screen">${svgIcon('compress')}</button>
- </div>
- <div class="status">
- <span class="bg-danger status-audio hidden">${svgIcon('microphone')}</span>
- <span class="bg-danger status-video hidden">${svgIcon('video')}</span>
- </div>`
+ svgIcon('user', 'fas', 'watermark')
+ + '<div class="dropdown">'
+ + '<a href="#" class="nickname btn btn-link" title="Nickname" aria-haspopup="true" aria-expanded="false" role="button">'
+ + '<span class="content"></span>'
+ + '<span class="icon">' + svgIcon('user') + '</span>'
+ + '</a>'
+ + '<div class="dropdown-menu">'
+ + '<a class="dropdown-item action-dismiss" href="#">Dismiss</a>'
+ + '</div>'
+ + '</div>'
+ + '<div class="controls">'
+ + '<button type="button" class="btn btn-link link-audio hidden" title="Mute audio">' + svgIcon('volume-mute') + '</button>'
+ + '<button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">' + svgIcon('expand') + '</button>'
+ + '<button type="button" class="btn btn-link link-fullscreen open hidden" title="Full screen">' + svgIcon('compress') + '</button>'
+ + '</div>'
+ + '<div class="status">'
+ + '<span class="bg-danger status-audio hidden">' + svgIcon('microphone') + '</span>'
+ + '<span class="bg-danger status-video hidden">' + svgIcon('video') + '</span>'
+ + '</div>'
)
if (params.publisher) {
// Add events for nickname change
let nickname = wrapper.addClass('publisher').find('.nickname')
- let editable = nickname.find('span').get(0)
+ let editable = nickname.find('.content')[0]
let editableEnable = () => {
editable.contentEditable = true
editable.focus()
@@ -688,14 +702,24 @@
}
})
} else {
- wrapper.find('.nickname > svg').addClass('hidden')
-
wrapper.find('.link-audio').removeClass('hidden')
.on('click', e => {
let video = wrapper.find('video')[0]
video.muted = !video.muted
wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger')
})
+
+ if (role == 'MODERATOR') {
+ wrapper.addClass('moderated')
+
+ wrapper.find('.nickname').attr({title: 'Options', 'data-toggle': 'dropdown'}).dropdown()
+
+ wrapper.find('.action-dismiss').on('click', e => {
+ if (sessionData.onDismiss) {
+ sessionData.onDismiss(params.connId)
+ }
+ })
+ }
}
videoWrapperUpdate(wrapper, params)
@@ -741,7 +765,7 @@
}
if ('nickname' in params) {
- $(wrapper).find('.nickname > span').text(params.nickname)
+ $(wrapper).find('.nickname > .content').text(params.nickname)
}
}
diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php
--- a/src/resources/lang/en/meet.php
+++ b/src/resources/lang/en/meet.php
@@ -21,6 +21,9 @@
'session-join-error' => 'Failed to join the session.',
'session-close-error' => 'Failed to close the session.',
'session-close-success' => 'The session has been closed successfully.',
+ 'session-dismiss-connection-error' => 'Failed to dismiss the connection.',
'session-password-error' => 'Failed to join the session. Invalid password.',
-
+ 'session-request-accept-error' => 'Failed to accept the join request.',
+ 'session-request-deny-error' => 'Failed to deny the join request.',
+ 'session-room-locked-error' => 'Failed to join the session. Room locked.',
];
diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss
--- a/src/resources/themes/meet.scss
+++ b/src/resources/themes/meet.scss
@@ -6,12 +6,6 @@
align-items: center;
justify-content: center;
- .watermark {
- color: darken($menu-bg-color, 20%);
- width: 50%;
- height: 50%;
- }
-
video {
// To make object-fit:cover working we have to set the height in pixels
// on the wrapper element. This is what javascript method will do.
@@ -33,6 +27,19 @@
}
}
+ .watermark {
+ color: darken($menu-bg-color, 20%);
+ width: 50%;
+ height: 50%;
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
+
.controls {
position: absolute;
bottom: 0;
@@ -78,41 +85,53 @@
border-radius: 1em;
max-width: calc(100% - 1em);
background: rgba(#fff, 0.8);
+ color: $body-color;
+ text-decoration: none !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ display: flex;
- button {
+ .icon {
display: none;
+ width: 2em;
+ margin: 0 -1em;
}
- span {
+ .content {
outline: none;
}
}
+ &.moderated .nickname {
+ display: flex;
+
+ .content {
+ order: 1;
+
+ &:not(:empty) + .icon {
+ margin-right: 0;
+ }
+ }
+
+ .icon {
+ display: inline-block;
+ }
+ }
+
&.publisher .nickname {
- cursor: pointer;
background: rgba($main-color, 0.9);
&:focus-within {
box-shadow: $btn-focus-box-shadow;
}
- span:empty {
+ .content:empty {
display: block;
height: 2em;
- &:not(:focus) + button {
- display: block;
- position: absolute;
- top: 0;
- left: 0;
- width: 2em;
- height: 2em;
- border-radius: 50%;
- padding: 0;
- color: $menu-gray;
+ &:not(:focus) + .icon {
+ display: inline-block;
}
}
}
@@ -253,3 +272,29 @@
}
}
}
+
+.toast.join-request {
+ .toast-header {
+ color: #eee;
+ }
+
+ .toast-body {
+ display: flex;
+ }
+
+ .picture {
+ margin-right: 1em;
+
+ img {
+ width: 64px;
+ height: 64px;
+ border: 1px solid #555;
+ border-radius: 50%;
+ object-fit: cover;
+ }
+ }
+
+ .content {
+ flex: 1;
+ }
+}
diff --git a/src/resources/themes/toast.scss b/src/resources/themes/toast.scss
--- a/src/resources/themes/toast.scss
+++ b/src/resources/themes/toast.scss
@@ -4,6 +4,10 @@
right: 0;
margin: 0.5rem;
width: 320px;
+ max-height: calc(100% - 1rem);
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(52, 58, 64, 0.95) transparent;
z-index: 1055; // above Bootstrap's modal backdrop and dialogs
@media (max-width: 375px) {
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -70,16 +70,15 @@
<input type="password" class="form-control" id="setup-password" v-model="password" placeholder="Password">
</div>
<div class="mt-3">
- <button v-if="roomState == 'ready' || roomState == 424 || roomState == 425"
- type="button"
+ <button type="button"
@click="joinSession"
- :class="'btn w-100 btn-' + (roomState == 'ready' ? 'success' : 'primary')"
- >JOIN</button>
- <button v-if="roomState == 423"
- type="button"
- @click="joinSession"
- class="btn btn-primary w-100"
- >I'm the owner</button>
+ :disabled="roomState == 'init' || roomState == 427 || roomState == 404"
+ :class="'btn w-100 btn-' + (isRoomReady() ? 'success' : 'primary')"
+ >
+ <span v-if="isRoomReady()">JOIN NOW</span>
+ <span v-else-if="roomState == 423">I'm the owner</span>
+ <span v-else>JOIN</span>
+ </button>
</div>
</div>
<div class="mt-4 col-sm-12">
@@ -157,6 +156,8 @@
423: 'The room is closed. Please, wait for the owner to start the session.',
424: 'The room is closed. It will be open for others after you join.',
425: 'The room is ready. Please, provide a valid password.',
+ 426: 'The room is locked. Please, enter your name and try again.',
+ 427: 'Waiting for permission to join the room.',
500: 'Failed to create a session. Server error.'
},
session: {}
@@ -184,8 +185,7 @@
},
methods: {
authSuccess() {
- // The user (owner) authentication succeeded
- this.roomState = 'init'
+ // The user authentication succeeded, we still don't know it's really the room owner
this.initSession()
$('#meet-setup').removeClass('hidden')
@@ -194,15 +194,20 @@
configUpdate(config) {
this.session.config = Object.assign({}, this.session.config, config)
},
+ dismissParticipant(id) {
+ axios.post('/api/v4/openvidu/rooms/' + this.room + '/connections/' + id + '/dismiss')
+ },
initSession(init) {
this.post = {
password: this.password,
nickname: this.nickname,
screenShare: this.canShareScreen ? 1 : 0,
- init: init ? 1 : 0
+ init: init ? 1 : 0,
+ picture: init ? this.makePicture() : '',
+ requestId: this.requestId()
}
- $('#setup-password').removeClass('is-invalid')
+ $('#setup-password,#setup-nickname').removeClass('is-invalid')
axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
.then(response => {
@@ -234,6 +239,19 @@
$('#setup-password').addClass('is-invalid').focus()
}
break;
+
+ case '426':
+ // Locked room prerequisites error
+ if (init && !$('#setup-nickname').val()) {
+ $('#setup-nickname').addClass('is-invalid').focus()
+ }
+ break;
+
+ case '427':
+ // Waiting for the owner's approval to join
+ // Update room state every 10 seconds
+ window.roomRequest = setTimeout(() => { this.initSession(true) }, 10000)
+ break;
}
})
@@ -241,6 +259,55 @@
$('#meet-session-menu').find('.link-fullscreen.closed').removeClass('hidden')
}
},
+ isRoomReady() {
+ return ['ready', '424', '425', '426', '427'].includes(this.roomState)
+ },
+ // An event received by the room owner when a participant is asking for a permission to join the room
+ joinRequest(data) {
+ // The toast for this user request already exists, ignore
+ // It's not really needed as we do this on server-side already
+ if ($('#i' + data.requestId).length) {
+ return
+ }
+
+ // FIXME: Should the message close button act as the Deny button? Do we need the Deny button?
+
+ let body = $(
+ `<div>`
+ + `<div class="picture"><img src="${data.picture}"></div>`
+ + `<div class="content">`
+ + `<p class="mb-2"></p>`
+ + `<div class="text-right">`
+ + `<button type="button" class="btn btn-sm btn-success accept">Accept</button>`
+ + `<button type="button" class="btn btn-sm btn-danger deny ml-2">Deny</button>`
+ )
+
+ this.$toast.message({
+ className: 'join-request',
+ icon: 'user',
+ timeout: 0,
+ title: 'Join request',
+ // titleClassName: '',
+ body: body.html(),
+ onShow: element => {
+ const id = data.requestId
+
+ $(element).find('p').text((data.nickname || '') + ' requested to join.')
+
+ // add id attribute, so we can identify it
+ $(element).attr('id', 'i' + id)
+ // add action to the buttons
+ .find('button.accept,button.deny').on('click', e => {
+ const action = $(e.target).is('.accept') ? 'accept' : 'deny'
+ axios.post('/api/v4/openvidu/rooms/' + this.room + '/request/' + id + '/' + action)
+ .then(response => {
+ $('#i' + id).remove()
+ })
+ })
+ }
+ })
+ },
+ // Entering the room
joinSession() {
if (this.roomState == 423) {
$('#meet-setup').addClass('hidden')
@@ -248,7 +315,7 @@
return
}
- if (this.roomState == 424 || this.roomState == 425) {
+ if (this.roomState != 'ready') {
this.initSession(true)
return
}
@@ -267,9 +334,9 @@
this.session.menuElement = $('#meet-session-menu')[0]
this.session.chatElement = $('#meet-chat')[0]
this.session.onDestroy = event => {
- // TODO: Handle nicely other reasons: disconnect, forceDisconnectByUser,
- // forceDisconnectByServer, networkDisconnect?
- if (event.reason == 'sessionClosedByServer' && !this.session.owner) {
+ // TODO: Display different message for each reason: forceDisconnectByUser,
+ // forceDisconnectByServer, sessionClosedByServer?
+ if (event.reason != 'disconnect' && event.reason != 'networkDisconnect' && !this.session.owner) {
$('#leave-dialog').on('hide.bs.modal', () => {
// FIXME: Where exactly the user should land? Currently he'll land
// on dashboard (if he's logged in) or login form (if he's not).
@@ -279,21 +346,72 @@
}
}
+ this.session.onDismiss = connId => { this.dismissParticipant(connId) }
+
+ if (this.session.owner) {
+ this.session.onJoinRequest = data => { this.joinRequest(data) }
+ }
+
this.meet.joinRoom(this.session)
},
logout() {
- if (this.session.owner) {
- axios.post('/api/v4/openvidu/rooms/' + this.room + '/close')
- .then(response => {
- this.meet.leaveRoom()
- this.meet = null
- window.location = window.config['app.url']
- })
- } else {
+ const logout = () => {
this.meet.leaveRoom()
this.meet = null
window.location = window.config['app.url']
}
+
+ if (this.session.owner) {
+ axios.post('/api/v4/openvidu/rooms/' + this.room + '/close').then(logout)
+ } else {
+ logout()
+ }
+ },
+ makePicture() {
+ const video = $("#setup-preview video")[0];
+
+ // Skip if video is not "playing"
+ if (!video.videoWidth || !this.camera) {
+ return ''
+ }
+
+ // we're going to crop a square from the video and resize it
+ const maxSize = 64
+
+ // Calculate sizing
+ let sh = Math.floor(video.videoHeight / 1.5)
+ let sw = sh
+ let sx = (video.videoWidth - sw) / 2
+ let sy = (video.videoHeight - sh) / 2
+
+ let dh = Math.min(sh, maxSize)
+ let dw = sh < maxSize ? sw : Math.floor(sw * dh/sh)
+
+ const canvas = $("<canvas>")[0];
+ canvas.width = dw;
+ canvas.height = dh;
+
+ // draw the image on the canvas (square cropped and resized)
+ canvas.getContext('2d').drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
+
+ // convert it to a usable data URL (png format)
+ return canvas.toDataURL();
+ },
+ requestId() {
+ if (!this.reqId) {
+ // FIXME: Shall we use some UUID generator? Or better something that identifies the
+ // user/browser so we could deny the join request for a longer time.
+ // I'm thinking about e.g. a bad actor knocking again and again and again,
+ // we don't want the room owner to be bothered every few seconds.
+ // Maybe a solution would be to store the identifier in the browser storage
+ // This would not prevent hackers from sending the new identifier on every request,
+ // but could make sure that it is kept after page refresh for the avg user.
+
+ // This will create max. 24-char numeric string
+ this.reqId = (String(Date.now()) + String(Math.random()).substring(2)).substring(0, 24)
+ }
+
+ return this.reqId
},
securityOptions() {
$('#security-options-dialog').modal()
diff --git a/src/resources/vue/Meet/SessionSecurityOptions.vue b/src/resources/vue/Meet/SessionSecurityOptions.vue
--- a/src/resources/vue/Meet/SessionSecurityOptions.vue
+++ b/src/resources/vue/Meet/SessionSecurityOptions.vue
@@ -27,11 +27,11 @@
the password before they are allowed to join the meeting.
</small>
</form>
- <hr v-if="false">
- <form v-if="false" id="security-options-lock">
- <div id="room-lock" class="">
- <span class="">Locked room:</span>
- <input type="checkbox" name="lock" value="1" :checked="config.locked" @click="lockSave">
+ <hr>
+ <form id="security-options-lock">
+ <div id="room-lock">
+ <label for="room-lock-input">Locked room:</label>
+ <input type="checkbox" id="room-lock-input" name="lock" value="1" :checked="config.locked" @click="lockSave">
</div>
<small class="form-text text-muted">
When the room is locked participants have to be approved by you
diff --git a/src/resources/vue/Widgets/Toast.vue b/src/resources/vue/Widgets/Toast.vue
--- a/src/resources/vue/Widgets/Toast.vue
+++ b/src/resources/vue/Widgets/Toast.vue
@@ -82,6 +82,16 @@
}
return this.addToast(data)
+ },
+ message(data) {
+ if (data.type === undefined) {
+ data.type = 'custom'
+ }
+ if (data.timeout === undefined) {
+ data.timeout = this.defaultTimeout
+ }
+
+ return this.addToast(data)
}
},
// Plugin installer method
diff --git a/src/resources/vue/Widgets/ToastMessage.vue b/src/resources/vue/Widgets/ToastMessage.vue
--- a/src/resources/vue/Widgets/ToastMessage.vue
+++ b/src/resources/vue/Widgets/ToastMessage.vue
@@ -1,18 +1,18 @@
<template>
- <div :class="'toast hide toast-' + data.type" role="alert" aria-live="assertive" aria-atomic="true">
- <div class="toast-header">
- <svg-icon icon="info-circle" :class="className()" v-if="data.type == 'info'"></svg-icon>
- <svg-icon icon="check-circle" :class="className()" v-else-if="data.type == 'success'"></svg-icon>
- <svg-icon icon="exclamation-circle" :class="className()" v-else-if="data.type == 'error'"></svg-icon>
- <svg-icon icon="exclamation-circle" :class="className()" v-else-if="data.type == 'warning'"></svg-icon>
- <strong :class="className()">{{ data.title || title() }}</strong>
+ <div :class="toastClassName()" role="alert" aria-live="assertive" aria-atomic="true">
+ <div class="toast-header" :class="className()">
+ <svg-icon icon="info-circle" v-if="data.type == 'info'"></svg-icon>
+ <svg-icon icon="check-circle" v-else-if="data.type == 'success'"></svg-icon>
+ <svg-icon icon="exclamation-circle" v-else-if="data.type == 'error'"></svg-icon>
+ <svg-icon icon="exclamation-circle" v-else-if="data.type == 'warning'"></svg-icon>
+ <svg-icon :icon="data.icon" v-else-if="data.type == 'custom' && data.icon"></svg-icon>
+ <strong>{{ data.title || title() }}</strong>
<button type="button" class="close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
- <div class="toast-body">
- {{ data.msg }}
- </div>
+ <div v-if="data.body" v-html="data.body" class="toast-body"></div>
+ <div v-else class="toast-body">{{ data.msg }}</div>
</div>
</template>
@@ -22,13 +22,19 @@
data: { type: Object, default: () => {} }
},
mounted() {
- $(this.$el).on('hidden.bs.toast', () => {
+ $(this.$el)
+ .on('hidden.bs.toast', () => {
(this.$el).remove()
this.$destroy()
})
+ .on('shown.bs.toast', () => {
+ if (this.data.onShow) {
+ this.data.onShow(this.$el)
+ }
+ })
.toast({
animation: true,
- autohide: true,
+ autohide: this.data.timeout > 0,
delay: this.data.timeout
})
.toast('show')
@@ -42,6 +48,8 @@
case 'info':
case 'success':
return 'text-' + this.data.type
+ case 'custom':
+ return this.data.titleClassName || ''
}
},
title() {
@@ -54,6 +62,12 @@
case 'success':
return type.charAt(0).toUpperCase() + type.slice(1)
}
+
+ return ''
+ },
+ toastClassName() {
+ return 'toast hide toast-' + this.data.type
+ + (this.data.className ? ' ' + this.data.className : '')
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -84,6 +84,10 @@
Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
+ // FIXME: I'm not sure about this one, should we use DELETE request maybe?
+ Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
+ Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
+ Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -13,6 +13,19 @@
class Browser extends \Laravel\Dusk\Browser
{
/**
+ * Assert that the given element attribute contains specified text.
+ */
+ public function assertAttributeRegExp($selector, $attribute, $regexp)
+ {
+ $element = $this->resolver->findOrFail($selector);
+ $value = (string) $element->getAttribute($attribute);
+
+ Assert::assertRegExp($regexp, $value, "No expected text in [$selector][$attribute]. Found: $value");
+
+ return $this;
+ }
+
+ /**
* Assert number of (visible) elements
*/
public function assertElementsCount($selector, $expected_count, $visible = true)
diff --git a/src/tests/Browser/Components/Toast.php b/src/tests/Browser/Components/Toast.php
--- a/src/tests/Browser/Components/Toast.php
+++ b/src/tests/Browser/Components/Toast.php
@@ -12,6 +12,7 @@
public const TYPE_SUCCESS = 'success';
public const TYPE_WARNING = 'warning';
public const TYPE_INFO = 'info';
+ public const TYPE_CUSTOM = 'custom';
protected $type;
protected $element;
diff --git a/src/tests/Browser/Meet/RoomSecurityTest.php b/src/tests/Browser/Meet/RoomSecurityTest.php
--- a/src/tests/Browser/Meet/RoomSecurityTest.php
+++ b/src/tests/Browser/Meet/RoomSecurityTest.php
@@ -115,7 +115,7 @@
->assertSeeIn('#password-clear-btn.btn-outline-danger', 'Clear password')
->assertElementsCount('#password-input button', 1)
->click('#password-clear-btn')
- ->assertToast(Toast::TYPE_SUCCESS, 'Room configuration updated successfully.')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.")
->assertMissing('#password-input input')
->assertSeeIn('#password-input-text.text-muted', 'none')
->assertSeeIn('#password-set-btn', 'Set password')
@@ -126,4 +126,123 @@
});
});
}
+
+ /**
+ * Test locked room
+ *
+ * @group openvidu
+ */
+ public function testLockedRoom(): void
+ {
+ $this->browse(function (Browser $owner, Browser $guest) {
+ // Make sure there's no session yet
+ $room = Room::where('name', 'john')->first();
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ // Join the room as an owner (authenticate)
+ $owner->visit(new RoomPage('john'))
+ // ->click('@setup-button')
+ // ->submitLogon('john@kolab.org', 'simple123')
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->type('@setup-nickname-input', 'John')
+ ->click('@setup-button')
+ ->waitFor('@session')
+ // Enter Security option dialog
+ ->click('@menu button.link-security')
+ ->with(new Dialog('#security-options-dialog'), function (Browser $browser) use ($room) {
+ $browser->assertSeeIn('@title', 'Security options')
+ ->assertSeeIn('#room-lock label', 'Locked room:')
+ ->assertVisible('#room-lock input[type=checkbox]:not(:checked)')
+ ->assertVisible('#room-lock + small')
+ // Test setting the lock
+ ->click('#room-lock input')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.")
+ ->click('@button-action');
+
+ $this->assertSame('true', $room->fresh()->getSetting('locked'));
+ });
+
+ // In another browser act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertSeeIn('@setup-button:not([disabled]).btn-success', 'JOIN NOW')
+ // try without the nickname
+ ->click('@setup-button')
+ ->waitFor('@setup-nickname-input.is-invalid')
+ ->assertSeeIn(
+ '@setup-status-message.text-danger',
+ "The room is locked. Please, enter your name and try again."
+ )
+ ->assertMissing('@setup-password-input')
+ ->assertSeeIn('@setup-button:not([disabled]).btn-success', 'JOIN NOW')
+ ->type('@setup-nickname-input', 'Guest<p>')
+ ->click('@setup-button')
+ ->assertMissing('@setup-nickname-input.is-invalid')
+ ->waitFor('@setup-button[disabled]')
+ ->assertSeeIn('@setup-status-message.text-danger', "Waiting for permission to join the room.");
+
+ // Test denying the request (this will also test custom toasts)
+ $owner
+ ->whenAvailable(new Toast(Toast::TYPE_CUSTOM), function ($browser) {
+ $browser->assertToastTitle('Join request')
+ ->assertVisible('.toast-header svg.fa-user')
+ ->assertSeeIn('@message', 'Guest<p> requested to join.')
+ ->assertAttributeRegExp('@message img', 'src', '|^data:image|')
+ ->assertSeeIn('@message button.accept.btn-success', 'Accept')
+ ->assertSeeIn('@message button.deny.btn-danger', 'Deny')
+ ->click('@message button.deny');
+ })
+ ->waitUntilMissing('.toast')
+ // wait 10 seconds to make sure the request message does not show up again
+ ->pause(10 * 1000)
+ ->assertMissing('.toast');
+
+ // Test accepting the request
+ $guest->refresh()
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->type('@setup-nickname-input', 'guest')
+ ->click('@setup-button')
+ ->waitFor('@setup-button[disabled]')
+ ->assertSeeIn('@setup-status-message.text-danger', "Waiting for permission to join the room.");
+
+ $owner
+ ->whenAvailable(new Toast(Toast::TYPE_CUSTOM), function ($browser) {
+ $browser->assertToastTitle('Join request')
+ ->assertSeeIn('@message', 'guest requested to join.')
+ ->click('@message button.accept');
+ });
+
+ // Guest automatically anters the room
+ $guest->waitFor('@session', 12)
+ // make sure he has no access to the Options menu
+ ->waitFor('@session .meet-video:not(.publisher)')
+ ->assertSeeIn('@session .meet-video:not(.publisher) a.nickname', 'John')
+ // TODO: Assert title and icon
+ ->click('@session .meet-video:not(.publisher) a.nickname')
+ ->pause(100)
+ ->assertMissing('.dropdown-menu');
+
+ // Test dismissing the participant
+ $owner->click('@session .meet-video:not(.publisher) a.nickname')
+ ->waitFor('@session .meet-video:not(.publisher) .dropdown-menu')
+ ->assertSeeIn('@session .meet-video:not(.publisher) .dropdown-menu > .action-dismiss', 'Dismiss')
+ ->click('@session .meet-video:not(.publisher) .dropdown-menu > .action-dismiss')
+ ->waitUntilMissing('.dropdown-menu')
+ ->waitUntilMissing('@session .meet-video:not(.publisher)');
+
+ // Expect a "end of session" dialog on the participant side
+ $guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Room closed')
+ ->assertSeeIn('@body', "The session has been closed by the room owner.")
+ ->assertMissing('@button-cancel')
+ ->assertSeeIn('@button-action', 'Close');
+ });
+ });
+ }
}
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -47,6 +47,8 @@
$browser->assertMissing('#footer-menu .navbar-nav');
}
+ // FIXME: Maybe it would be better to just display the usual 404 Not Found error page?
+
$browser->assertMissing('@toolbar')
->assertMissing('@menu')
->assertMissing('@session')
@@ -54,7 +56,7 @@
->assertMissing('@login-form')
->assertVisible('@setup-form')
->assertSeeIn('@setup-status-message', "The room does not exist.")
- ->assertMissing('@setup-button');
+ ->assertVisible('@setup-button[disabled]');
});
}
diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php
--- a/src/tests/Feature/Controller/OpenViduTest.php
+++ b/src/tests/Feature/Controller/OpenViduTest.php
@@ -88,6 +88,8 @@
$response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing");
$response->assertStatus(404);
+ // TODO: Test accessing an existing room of deleted owner
+
// Non-owner, no session yet
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}");
$response->assertStatus(423);
@@ -103,7 +105,7 @@
$session_id = $room->fresh()->session_id;
- $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame(Room::ROLE_MODERATOR, $json['role']);
$this->assertSame($session_id, $json['session']);
$this->assertTrue(is_string($session_id) && !empty($session_id));
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
@@ -117,7 +119,7 @@
$json = $response->json();
- $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
$this->assertSame($session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue($json['token'] != $john_token);
@@ -153,8 +155,129 @@
// Make sure the room owner can access the password protected room w/o password
$response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}");
$response->assertStatus(200);
+ }
- // TODO: Test accessing an existing room of deleted owner
+ /**
+ * Test locked room and join requests
+ *
+ * @group openvidu
+ */
+ public function testJoinRequests(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+ $room->session_id = null;
+ $room->save();
+ $room->setSettings(['password' => null, 'locked' => 'true']);
+
+ $this->assignBetaEntitlement($john, 'meet');
+
+ // Create the session (also makes sure the owner can access a locked room)
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ // Non-owner, locked room, invalid/missing input
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}");
+ $response->assertStatus(426);
+
+ $json = $response->json();
+
+ $this->assertCount(3, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to join the session. Room locked.', $json['message']);
+ $this->assertTrue($json['config']['locked']);
+
+ // Non-owner, locked room, invalid requestId
+ $post = ['nickname' => 'name', 'requestId' => '-----'];
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(426);
+
+ // Non-owner, locked room, invalid requestId
+ $post = ['nickname' => 'name', 'picture' => '-----'];
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(426);
+
+ // Non-owner, locked room, valid input
+ $reqId = '12345678';
+ $post = ['nickname' => 'name', 'requestId' => $reqId, 'picture' => 'data:image/png;base64,01234'];
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(427);
+
+ $json = $response->json();
+
+ $this->assertCount(3, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to join the session. Room locked.', $json['message']);
+ $this->assertTrue($json['config']['locked']);
+
+ // TODO: How do we assert that a signal has been sent to the owner?
+
+ // Test denying a request
+
+ // Unknown room
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/unknown/request/unknown/deny");
+ $response->assertStatus(404);
+
+ // Unknown request Id
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/unknown/deny");
+ $response->assertStatus(500);
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to deny the join request.', $json['message']);
+
+ // Non-owner access forbidden
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/deny");
+ $response->assertStatus(403);
+
+ // Valid request
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/deny");
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+
+ // Non-owner, locked room, join request denied
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(427);
+
+ // Test accepting a request
+
+ // Unknown room
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/unknown/request/unknown/accept");
+ $response->assertStatus(404);
+
+ // Unknown request Id
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/unknown/accept");
+ $response->assertStatus(500);
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to accept the join request.', $json['message']);
+
+ // Non-owner access forbidden
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/accept");
+ $response->assertStatus(403);
+
+ // Valid request
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/accept");
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+
+ // Non-owner, locked room, join request accepted
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
+ $this->assertTrue(strpos($json['token'], 'wss://') === 0);
+
+ // TODO: Test a scenario where both password and lock are enabled
}
/**
@@ -177,7 +300,7 @@
$json = $response->json();
- $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
$this->assertSame($room->session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue(strpos($json['shareToken'], 'wss://') === 0);
@@ -238,6 +361,69 @@
}
/**
+ * Test dismissing a participant (closing a connection)
+ *
+ * @group openvidu
+ */
+ public function testDismissConnection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+ $room->session_id = null;
+ $room->save();
+
+ $this->assignBetaEntitlement($john, 'meet');
+
+ // First we create the session
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ // And the other user connection
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $conn_id = $json['connectionId'];
+ $room->refresh();
+ $conn_data = $room->getOVConnection($conn_id);
+
+ $this->assertSame($conn_id, $conn_data['connectionId']);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/connections/{$conn_id}/dismiss");
+ $response->assertStatus(404);
+
+ // TODO: Test accessing an existing room of deleted owner
+
+ // Non-existing connection
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/connections/123/dismiss");
+ $response->assertStatus(500);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to dismiss the connection.', $json['message']);
+
+ // Non-owner access
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss");
+ $response->assertStatus(403);
+
+ // Expected success
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertNull($room->getOVConnection($conn_id));
+ }
+
+ /**
* Test configuring the room (session)
*
* @group openvidu

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 3:04 PM (17 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830092
Default Alt Text
D1912.1775315073.diff (61 KB)

Event Timeline