diff --git a/appinfo/info.xml b/appinfo/info.xml
index 94d28890025..20b0c552c0c 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
]]>
- 7.0.0-dev.2
+ 7.0.0-dev.3
agpl
Daniel Calviño Sánchez
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 57f47e5eb5f..b88b28112e0 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -392,6 +392,19 @@
'id' => '^\d+$',
],
],
+
+ /**
+ * Webinary
+ */
+ [
+ 'name' => 'Webinary#setLobby',
+ 'url' => '/api/{apiVersion}/room/{token}/webinary/lobby',
+ 'verb' => 'PUT',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ 'token' => '^[a-z0-9]{4,30}$',
+ ],
+ ],
],
];
diff --git a/css/style.scss b/css/style.scss
index 42b69ed5039..299b04b50c7 100644
--- a/css/style.scss
+++ b/css/style.scss
@@ -502,6 +502,7 @@ input[type="password"] {
padding-left: 1px;
}
+.room-moderation-button .menu li div.separator,
#app-navigation .app-navigation-entry-menu li div.separator {
border-bottom: 1px solid var(--color-border-dark);
margin: 0 10px;
@@ -1049,23 +1050,48 @@ body:not(#body-public) .participantWithList > li > span:not(.currentUser):not(.g
}
}
- .menuitem.password-option {
+ li {
+ > .separator {
+
+ margin-top: $outter-margin - 2px; // minus the input margin
+ }
+ }
+
+ .menuitem.caption,
+ .menuitem.caption > span {
+ /* Override rule for menu items from server, as in this case the
+ * caption is not clickable. */
+ cursor: default;
+ }
+
+ .menuitem.caption {
+ font-weight: bold;
+
+ &:hover,
+ &:focus,
+ &:active {
+ opacity: 0.7 !important;
+ }
+ }
+
+ .menuitem.password-option,
+ .menuitem.lobby-timer-option {
/* Override rule for menu items from server, as in this case
* only the button in the password field is clickable, so the
* pointer cursor should not be used for the whole item. */
cursor: default;
- .password-form {
+ form {
position: relative;
- .password-confirm,
- .password-loading {
+ .icon-confirm,
+ .icon-loading-small {
/* Inputs in menu items do not have a right margin, so
* it does not need to be compensated. */
right: 0;
}
- .password-confirm {
+ .icon-confirm {
/* Needed to override an important rule set in the
* server. */
background-color: transparent !important;
@@ -1076,7 +1102,8 @@ body:not(#body-public) .participantWithList > li > span:not(.currentUser):not(.g
/* The specific locator is needed to have higher priority than other
* important rules set in the server. */
- input.checkbox + label.link-checkbox-label {
+ input.checkbox + label.link-checkbox-label,
+ input.radio + label {
/* If ".icon-loading-small" is set hide the checkbox and show a
* loading icon instead. */
&.icon-loading-small:before {
diff --git a/docs/capabilities.md b/docs/capabilities.md
index fef80fb0610..7f3fa080e39 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -33,6 +33,6 @@ title: Capabilities
* `locked-one-to-one-rooms` - One-to-one conversations are now locked to the users. Neither guests nor other participants can be added, so the options to do that should be hidden as well. Also a user can only leave a one-to-one conversation (not delete). It will be deleted when the other participant left too. If the other participant posts a new chat message or starts a call, the left-participant will be re-added.
* `read-only-rooms` - Conversations can be in `read-only` mode which means people can not do calls or write chat messages.
-
## 7.0
* `chat-read-marker` - The chat can be optionally marked read by clients manually, independent from the loading of the chat messages.
+* `webinary-lobby` - See [Webinary management](webinary.md) for technical details.
diff --git a/docs/constants.md b/docs/constants.md
index a0bab164b08..08ed22562a0 100644
--- a/docs/constants.md
+++ b/docs/constants.md
@@ -24,3 +24,7 @@ title: Constants
* `guests` - guest users
* `users` - logged-in users
* `bots` - used by commands (actor-id is the used `/command`) and the changelog conversation (actor-id is `changelog`)
+
+## Webinary lobby states
+* `0` no lobby
+* `1` lobby for non moderators
diff --git a/docs/conversation.md b/docs/conversation.md
index b9d8cd6762e..c9c18ab8b22 100644
--- a/docs/conversation.md
+++ b/docs/conversation.md
@@ -62,6 +62,8 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`lastActivity` | int | Timestamp of the last activity in the conversation, in seconds and UTC time zone
`isFavorite` | bool | Flag if the conversation is favorited by the user
`notificationLevel` | int | The notification level for the user (one of `Participant::NOTIFY_*` (1-3))
+ `lobbyState` | int | Webinary lobby restriction (0-1), if the participant is a moderator they can always join the conversation (only available with `webinary-lobby` capability)
+ `lobbyTimer` | int | Timestamp when the lobby will be automatically disabled (only available with `webinary-lobby` capability)
`unreadMessages` | int | Number of unread chat messages in the conversation (only available with `chat-v2` capability)
`unreadMention` | bool | Flag if the user was mentioned since their last visit
`lastReadMessage` | int | ID of the last read message in a room (only available with `chat-read-marker` capability)
diff --git a/docs/webinary.md b/docs/webinary.md
new file mode 100644
index 00000000000..008df47591a
--- /dev/null
+++ b/docs/webinary.md
@@ -0,0 +1,28 @@
+# Webinary management
+
+Group and public conversations can be used to host webinaries. Those online meetings can have a lobby, which come with the following restrictions:
+* Only moderators can start/join a call
+* Only moderators can read and write chat messages
+* Normal users can only join the room. They then pull the room endpoint regularly for an update and should start the chat and signaling as well as allowing to join the call, once the lobby got disabled.
+
+
+Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
+
+## Set lobby for a conversation
+
+* Required capability: `webinary-lobby`
+* Method: `PUT`
+* Endpoint: `/room/{token}/webinary/lobby`
+* Data:
+
+ field | type | Description
+ ------|------|------------
+ `state` | int | New state for the conversation
+ `timer` | int/null | Timestamp when the lobby state is reset to no lobby
+
+* Response:
+ - Header:
+ + `200 OK`
+ + `400 Bad Request` When the conversation type does not support lobby (only group and public conversation atm)
+ + `403 Forbidden` When the current user is not a moderator/owner
+ + `404 Not Found` When the conversation could not be found for the participant
diff --git a/js/app.js b/js/app.js
index 5a096c3b4ce..72352de2481 100644
--- a/js/app.js
+++ b/js/app.js
@@ -55,6 +55,10 @@
ROOM_TYPE_PUBLIC: 3,
ROOM_TYPE_CHANGELOG: 4,
+ /* Must stay in sync with values in "lib/Webinary.php". */
+ LOBBY_NONE: 0,
+ LOBBY_NON_MODERATORS: 1,
+
/** @property {OCA.SpreedMe.Models.Room} activeRoom */
activeRoom: null,
@@ -355,7 +359,9 @@
this.signaling.syncRooms()
.then(function() {
self.stopListening(self.activeRoom, 'change:displayName');
+ self.stopListening(self.activeRoom, 'change:participantType');
self.stopListening(self.activeRoom, 'change:participantFlags');
+ self.stopListening(self.activeRoom, 'change:lobbyState');
if (OC.getCurrentUser().uid) {
roomChannel.trigger('active', token);
@@ -376,6 +382,10 @@
self.updateContentsLayout();
self.listenTo(self.activeRoom, 'change:participantFlags', self.updateContentsLayout);
+ self.listenTo(self.activeRoom, 'change:participantType', self.updateContentsLayout);
+ self.listenTo(self.activeRoom, 'change:participantType', self._updateSidebar);
+ self.listenTo(self.activeRoom, 'change:lobbyState', self.updateContentsLayout);
+ self.listenTo(self.activeRoom, 'change:lobbyState', self._updateSidebar);
self.updateSidebarWithActiveRoom();
});
@@ -386,16 +396,22 @@
return;
}
+ if (this.activeRoom.isCurrentParticipantInLobby()) {
+ this._showEmptyContentViewInMainView();
+
+ return;
+ }
+
var flags = this.activeRoom.get('participantFlags') || 0;
var inCall = flags & OCA.SpreedMe.app.FLAG_IN_CALL !== 0;
- if (inCall && this._chatViewInMainView === true) {
- this._chatView.$el.detach();
- this._sidebarView.addTab('chat', { label: t('spreed', 'Chat'), icon: 'icon-comment', priority: 100 }, this._chatView);
- this._sidebarView.selectTab('chat');
- this._chatView.reloadMessageList();
- this._chatView.setTooltipContainer(this._chatView.$el);
- this._chatViewInMainView = false;
- } else if (!inCall && !this._chatViewInMainView) {
+ if (inCall) {
+ this._showCallViewInMainView();
+ } else if (!inCall) {
+ this._showChatViewInMainView();
+ }
+ },
+ _showChatViewInMainView: function() {
+ if (!this._chatViewInMainView) {
this._sidebarView.removeTab('chat');
this._chatView.$el.prependTo('#app-content-wrapper');
this._chatView.reloadMessageList();
@@ -404,18 +420,38 @@
this._chatViewInMainView = true;
}
- if (inCall) {
- $('#videos').show();
- $('#screens').show();
- $('#emptycontent').hide();
- } else {
- $('#videos').hide();
- $('#screens').hide();
- $('#emptycontent').show();
+ $('#videos').hide();
+ $('#screens').hide();
+ $('#emptycontent').show();
+ },
+ _showCallViewInMainView: function() {
+ if (this._chatViewInMainView) {
+ this._chatView.$el.detach();
+ this._sidebarView.addTab('chat', { label: t('spreed', 'Chat'), icon: 'icon-comment', priority: 100 }, this._chatView);
+ this._sidebarView.selectTab('chat');
+ this._chatView.reloadMessageList();
+ this._chatView.setTooltipContainer(this._chatView.$el);
+ this._chatViewInMainView = false;
}
+
+ $('#videos').show();
+ $('#screens').show();
+ $('#emptycontent').hide();
},
- updateSidebarWithActiveRoom: function() {
- this._sidebarView.enable();
+ _showEmptyContentViewInMainView: function() {
+ this._chatView.$el.detach();
+ this._chatViewInMainView = false;
+
+ $('#videos').hide();
+ $('#screens').hide();
+ $('#emptycontent').show();
+ },
+ _updateSidebar: function() {
+ if (!this.activeRoom.isCurrentParticipantInLobby()) {
+ this._sidebarView.enable();
+ } else {
+ this._sidebarView.disable();
+ }
// The sidebar has a width of 27% of the window width and a minimum
// width of 300px. Therefore, when the window is 1111px wide or
@@ -429,6 +465,13 @@
this._sidebarView.open();
}
+ if (this.activeRoom.isCurrentParticipantInLobby()) {
+ this._messageCollection.stopReceivingMessages();
+ } else {
+ this._messageCollection.receiveMessages();
+ }
+ },
+ updateSidebarWithActiveRoom: function() {
var callInfoView = new OCA.SpreedMe.Views.CallInfoView({
model: this.activeRoom,
guestNameModel: this._localStorageModel
@@ -437,7 +480,8 @@
this._chatView.setRoom(this.activeRoom);
this._messageCollection.setRoomToken(this.activeRoom.get('token'));
- this._messageCollection.receiveMessages();
+
+ this._updateSidebar();
},
setPageTitle: function(title){
if (title) {
@@ -574,12 +618,7 @@
});
this.listenTo(roomChannel, 'leaveCurrentRoom', function() {
- this._chatView.$el.detach();
- this._chatViewInMainView = false;
-
- $('#videos').hide();
- $('#screens').hide();
- $('#emptycontent').show();
+ this._showEmptyContentViewInMainView();
});
this.listenTo(roomChannel, 'joinRoom', function(token) {
diff --git a/js/models/room.js b/js/models/room.js
index 60b57a4a60e..e760cf0d24a 100644
--- a/js/models/room.js
+++ b/js/models/room.js
@@ -63,6 +63,8 @@
unreadMention: false,
isFavorite: false,
notificationLevel: 0,
+ lobbyState: 0,
+ lobbyTimer: 0,
lastPing: 0,
sessionId: '0',
participants: [],
@@ -106,6 +108,10 @@
return 'Public room type can only be changed to group';
}
}
+
+ if (attributes.lobbyTimer && this.attributes.lobbyState !== OCA.SpreedMe.app.LOBBY_NON_MODERATORS) {
+ return 'Lobby timer can be set only when lobby state is non moderators';
+ }
},
save: function(key, value, options) {
if (typeof key !== 'string') {
@@ -113,6 +119,8 @@
}
var supportedKeys = [
+ 'lobbyState',
+ 'lobbyTimer',
'name',
'password',
'type',
@@ -200,6 +208,31 @@
options.url = this.url() + '/password';
}
+ if (method === 'patch' && options.attrs.lobbyState !== undefined) {
+ method = 'update';
+
+ options.url = this.url() + '/webinary/lobby';
+
+ // The endpoint to set the lobby state expects the state to be
+ // provided in a "state" attribute instead of a "lobbyState"
+ // attribute.
+ options.attrs.state = options.attrs.lobbyState;
+ delete options.attrs.lobbyState;
+ }
+
+ if (method === 'patch' && options.attrs.lobbyTimer !== undefined) {
+ method = 'update';
+
+ options.url = this.url() + '/webinary/lobby';
+
+ // The endpoint to set the lobby state expects the state and
+ // timer to be provided in "state" and "timer" attribute instead
+ // of "lobbyState" and "lobbyTimer" attributes.
+ options.attrs.state = this.attributes.lobbyState;
+ options.attrs.timer = options.attrs.lobbyTimer;
+ delete options.attrs.lobbyTimer;
+ }
+
return Backbone.Model.prototype.sync.call(this, method, model, options);
},
setPublic: function(isPublic, options) {
@@ -210,6 +243,12 @@
setPassword: function(password, options) {
this.save('password', password, options);
},
+ setLobbyState: function(lobbyState, options) {
+ this.save('lobbyState', lobbyState, options);
+ },
+ setLobbyTimer: function(lobbyTimer, options) {
+ this.save('lobbyTimer', lobbyTimer, options);
+ },
join: function() {
OCA.SpreedMe.app.connection.joinRoom(this.get('token'));
},
@@ -247,6 +286,17 @@
return Backbone.Model.prototype.destroy.call(this, options);
},
+ isCurrentParticipantInLobby: function() {
+ var isModerator = this.get('participantType') !== OCA.SpreedMe.app.USER &&
+ this.get('participantType') !== OCA.SpreedMe.app.USERSELFJOINED &&
+ this.get('participantType') !== OCA.SpreedMe.app.GUEST;
+
+ if (this.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NON_MODERATORS && !isModerator) {
+ return true;
+ }
+
+ return false;
+ },
});
OCA.SpreedMe.Models.Room = Room;
diff --git a/js/views/callinfoview.js b/js/views/callinfoview.js
index 886390af6f7..e5a295cba35 100644
--- a/js/views/callinfoview.js
+++ b/js/views/callinfoview.js
@@ -55,6 +55,17 @@
canFullModerate: canFullModerate,
linkCheckboxLabel: t('spreed', 'Share link'),
copyLinkLabel: t('spreed', 'Copy link'),
+ enableForLabel: t('spreed', 'Enable for'),
+ allParticipantsLabel: t('spreed', 'All participants'),
+ moderatorsOnlyLabel: t('spreed', 'Moderators only'),
+ allParticipantsValue: OCA.SpreedMe.app.LOBBY_NONE,
+ moderatorsOnlyValue: OCA.SpreedMe.app.LOBBY_NON_MODERATORS,
+ lobbyStateAllParticipants: this.model.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NONE,
+ lobbyStateModeratorsOnly: this.model.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NON_MODERATORS,
+ lobbyTimerPlaceholder: t('spreed', 'Start time (optional)'),
+ // PHP timestamp is second-based; JavaScript timestamp is
+ // millisecond based.
+ lobbyTimerValue: this.model.get('lobbyTimer')? OC.Util.formatDate(this.model.get('lobbyTimer') * 1000, 'YYYY-MM-DD HH:mm:ss') : '',
isPublic: isPublic,
passwordInputPlaceholder: this.model.get('hasPassword')? t('spreed', 'Change password'): t('spreed', 'Set password'),
isDeletable: canModerate && (Object.keys(this.model.get('participants')).length > 2 || this.model.get('numGuests') > 0)
@@ -77,6 +88,16 @@
'roomModerationButton': '.room-moderation-button .button',
'roomModerationMenu': '.room-moderation-button .menu',
+
+ 'allParticipantsRadio': '.all-participants-radio',
+ 'moderatorsOnlyRadio': '.moderators-only-radio',
+ 'allParticipantsLabel': '.all-participants-label',
+ 'moderatorsOnlyLabel': '.moderators-only-label',
+
+ 'lobbyTimerForm': '.lobby-timer-form',
+ 'lobbyTimerInput': '.lobby-timer-input',
+ 'lobbyTimerConfirm': '.lobby-timer-confirm',
+ 'lobbyTimerLoading': '.lobby-timer-loading',
},
regions: {
@@ -89,12 +110,24 @@
'click @ui.passwordConfirm': 'confirmPassword',
'submit @ui.passwordForm': 'confirmPassword',
+
+ 'click @ui.allParticipantsRadio': 'setLobbyStateAllParticipants',
+ 'click @ui.moderatorsOnlyRadio': 'setLobbyStateModeratorsOnly',
+
+ 'click @ui.lobbyTimerConfirm': 'confirmLobbyTimer',
+ 'submit @ui.lobbyTimerForm': 'confirmLobbyTimer',
},
modelEvents: {
'change:hasPassword': function() {
this.render();
},
+ 'change:lobbyState': function() {
+ this.render();
+ },
+ 'change:lobbyTimer': function() {
+ this.render();
+ },
'change:participantType': function() {
this._updateNameEditability();
@@ -284,6 +317,92 @@
});
},
+ setLobbyStateAllParticipants: function() {
+ this.ui.allParticipantsRadio.prop('disabled', true);
+ this.ui.allParticipantsLabel.addClass('icon-loading-small');
+
+ this.model.setLobbyState(OCA.SpreedMe.app.LOBBY_NONE, {
+ wait: true,
+ error: function() {
+ if (this.model.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NONE) {
+ this.ui.allParticipantsRadio.prop('checked', true);
+ } else {
+ this.ui.moderatorsOnlyRadio.prop('checked', true);
+ }
+ this.ui.allParticipantsRadio.prop('disabled', false);
+ this.ui.allParticipantsLabel.removeClass('icon-loading-small');
+
+ OC.Notification.show(t('spreed', 'Error occurred while enabling for all participants'), {type: 'error'});
+ }.bind(this)
+ });
+ },
+
+ setLobbyStateModeratorsOnly: function() {
+ this.ui.moderatorsOnlyRadio.prop('disabled', true);
+ this.ui.moderatorsOnlyLabel.addClass('icon-loading-small');
+
+ this.model.setLobbyState(OCA.SpreedMe.app.LOBBY_NON_MODERATORS, {
+ wait: true,
+ error: function() {
+ if (this.model.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NONE) {
+ this.ui.allParticipantsRadio.prop('checked', true);
+ } else {
+ this.ui.moderatorsOnlyRadio.prop('checked', true);
+ }
+ this.ui.moderatorsOnlyRadio.prop('disabled', false);
+ this.ui.moderatorsOnlyLabel.removeClass('icon-loading-small');
+
+ OC.Notification.show(t('spreed', 'Error occurred while enabling only for moderators'), {type: 'error'});
+ }.bind(this)
+ });
+ },
+
+ confirmLobbyTimer: function(e) {
+ e.preventDefault();
+
+ var lobbyTimerTimestamp = 0;
+
+ var lobbyTimerInputValue = this.ui.lobbyTimerInput.val().trim();
+ if (lobbyTimerInputValue) {
+ // PHP timestamp is second-based; JavaScript timestamp is
+ // millisecond based.
+ lobbyTimerTimestamp = Date.parse(lobbyTimerInputValue) / 1000;
+ }
+
+ if (isNaN(lobbyTimerTimestamp)) {
+ OC.Notification.show(t('spreed', 'Invalid start time format'), {type: 'error'});
+
+ return;
+ }
+
+ this.ui.lobbyTimerInput.prop('disabled', true);
+ this.ui.lobbyTimerConfirm.addClass('hidden');
+ this.ui.lobbyTimerLoading.removeClass('hidden');
+
+ var restoreState = function() {
+ this.ui.lobbyTimerInput.prop('disabled', false);
+ this.ui.lobbyTimerConfirm.removeClass('hidden');
+ this.ui.lobbyTimerLoading.addClass('hidden');
+ }.bind(this);
+
+ this.model.setLobbyTimer(lobbyTimerTimestamp, {
+ wait: true,
+ // The same lobby timer can be successfully set, which would not
+ // trigger a change event, so the state needs to be restored
+ // instead of relying on the view to be rendered again.
+ success: function() {
+ restoreState();
+ OC.hideMenus();
+ this.ui.roomModerationButton.focus();
+ }.bind(this),
+ error: function() {
+ restoreState();
+
+ OC.Notification.show(t('spreed', 'Error occurred while setting the lobby start time'), {type: 'error'});
+ }.bind(this)
+ });
+ },
+
/**
* Clipboard
*/
diff --git a/js/views/emptycontentview.js b/js/views/emptycontentview.js
index 19575096e8c..338a1800552 100644
--- a/js/views/emptycontentview.js
+++ b/js/views/emptycontentview.js
@@ -90,7 +90,11 @@
this._activeRoom = activeRoom;
- this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall();
+ if (!this._activeRoom.isCurrentParticipantInLobby()) {
+ this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall();
+ } else {
+ this.setEmptyContentMessageWhenWaitingInLobby();
+ }
this.listenTo(this._activeRoom, 'destroy', function() {
this.stopListening(this._activeRoom, 'destroy', this.setInitialEmptyContentMessage);
@@ -111,6 +115,10 @@
this.stopListening(this._activeRoom, 'change:numGuests', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
this.stopListening(this._activeRoom, 'change:participantType', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
this.stopListening(this._activeRoom, 'change:type', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
+
+ this.stopListening(this._activeRoom, 'change:lobbyState', this.setEmptyContentMessageWhenWaitingInLobby);
+ this.stopListening(this._activeRoom, 'change:lobbyTimer', this.setEmptyContentMessageWhenWaitingInLobby);
+ this.stopListening(this._activeRoom, 'change:participantType', this.setEmptyContentMessageWhenWaitingInLobby);
},
_enableUpdatesOnActiveRoomChanges: function() {
@@ -118,6 +126,10 @@
this.listenTo(this._activeRoom, 'change:numGuests', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
this.listenTo(this._activeRoom, 'change:participantType', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
this.listenTo(this._activeRoom, 'change:type', this.setEmptyContentMessageWhenWaitingForOthersToJoinTheCall);
+
+ this.listenTo(this._activeRoom, 'change:lobbyState', this.setEmptyContentMessageWhenWaitingInLobby);
+ this.listenTo(this._activeRoom, 'change:lobbyTimer', this.setEmptyContentMessageWhenWaitingInLobby);
+ this.listenTo(this._activeRoom, 'change:participantType', this.setEmptyContentMessageWhenWaitingInLobby);
},
/**
@@ -167,7 +179,39 @@
);
},
+ setEmptyContentMessageWhenWaitingInLobby: function() {
+ if (!this._activeRoom.isCurrentParticipantInLobby()) {
+ return;
+ }
+
+ var icon = '';
+
+ if (this._activeRoom.get('type') === OCA.SpreedMe.app.ROOM_TYPE_PUBLIC) {
+ icon = 'icon-public';
+ } else {
+ icon = 'icon-contacts-dark';
+ }
+
+ var messageAdditional = t('spreed', 'Waiting for the conversation to be opened');
+ if (this._activeRoom.get('lobbyTimer')) {
+ // PHP timestamp is second-based; JavaScript timestamp is
+ // millisecond based.
+ var startTime = OC.Util.formatDate(this._activeRoom.get('lobbyTimer') * 1000);
+ messageAdditional = t('spreed', 'Waiting for the conversation to be opened. This meeting is scheduled for {startTime}', {startTime: startTime});
+ }
+
+ this.setEmptyContentMessage(
+ icon,
+ this._activeRoom.get('name'),
+ messageAdditional
+ );
+ },
+
setEmptyContentMessageWhenWaitingForOthersToJoinTheCall: function() {
+ if (this._activeRoom.isCurrentParticipantInLobby()) {
+ return;
+ }
+
var icon = '';
var message = '';
var messageAdditional = '';
diff --git a/js/views/templates.js b/js/views/templates.js
index 51541e45a58..d522f6aca4a 100644
--- a/js/views/templates.js
+++ b/js/views/templates.js
@@ -59,6 +59,7 @@ templates['callinfoview'] = template({"1":function(container,depth0,helpers,part
return "
\n \n \n
\n";
},"6":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
@@ -82,6 +83,33 @@ templates['callinfoview'] = template({"1":function(container,depth0,helpers,part
return "icon-password";
},"12":function(container,depth0,helpers,partials,data) {
return "icon-no-password";
+},"14":function(container,depth0,helpers,partials,data) {
+ var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+ return " \n \n \n \n \n \n \n \n \n \n \n \n"
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.lobbyStateModeratorsOnly : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"15":function(container,depth0,helpers,partials,data) {
+ var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+ return " \n \n \n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
diff --git a/js/views/templates/callinfoview.handlebars b/js/views/templates/callinfoview.handlebars
index 4a3e52dbe74..e78847e2847 100644
--- a/js/views/templates/callinfoview.handlebars
+++ b/js/views/templates/callinfoview.handlebars
@@ -38,6 +38,39 @@
{{/if}}
+ {{#if canFullModerate}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#if lobbyStateModeratorsOnly}}
+
+
+
+ {{/if}}
+ {{/if}}
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 2c0bc5f8d08..3f3cb48183b 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -79,6 +79,7 @@ public function getCapabilities(): array {
'locked-one-to-one-rooms',
'read-only-rooms',
'chat-read-marker',
+ 'webinary-lobby',
],
'config' => [
'chat' => [
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index 57ba2c8ec27..21536b10753 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -138,6 +138,18 @@ public function parseMessage(Message $chatMessage): void {
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You locked the conversation');
}
+ } else if ($message === 'lobby_timer_reached') {
+ $parsedMessage = $this->l->t('The conversation is now open to everyone');
+ } else if ($message === 'lobby_none') {
+ $parsedMessage = $this->l->t('{actor} opened the conversation to everyone');
+ if ($currentUserIsActor) {
+ $parsedMessage = $this->l->t('You opened the conversation to everyone');
+ }
+ } else if ($message === 'lobby_non_moderators') {
+ $parsedMessage = $this->l->t('{actor} restricted the conversation to moderators');
+ if ($currentUserIsActor) {
+ $parsedMessage = $this->l->t('You restricted the conversation to moderators');
+ }
} else if ($message === 'guests_allowed') {
$parsedMessage = $this->l->t('{actor} allowed guests');
if ($currentUserIsActor) {
diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php
index 6ee376a1838..c18cb016fb3 100644
--- a/lib/Chat/SystemMessage/Listener.php
+++ b/lib/Chat/SystemMessage/Listener.php
@@ -32,6 +32,7 @@
use OCA\Spreed\Room;
use OCA\Spreed\Share\RoomShareProvider;
use OCA\Spreed\TalkSession;
+use OCA\Spreed\Webinary;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\IUser;
@@ -157,6 +158,27 @@ public static function register(EventDispatcherInterface $dispatcher): void {
$listener->sendSystemMessage($room, 'read_only_off', $event->getArguments());
}
});
+ $dispatcher->addListener(Room::class . '::postSetLobbyState', function(GenericEvent $event) {
+ $arguments = $event->getArguments();
+
+ if ($arguments['newState'] === $arguments['oldState']) {
+ return;
+ }
+
+ /** @var Room $room */
+ $room = $event->getSubject();
+
+ /** @var self $listener */
+ $listener = \OC::$server->query(self::class);
+
+ if ($arguments['timerReached']) {
+ $listener->sendSystemMessage($room, 'lobby_timer_reached', $event->getArguments());
+ } else if ($arguments['newState'] === Webinary::LOBBY_NONE) {
+ $listener->sendSystemMessage($room, 'lobby_none', $event->getArguments());
+ } else if ($arguments['newState'] === Webinary::LOBBY_NON_MODERATORS) {
+ $listener->sendSystemMessage($room, 'lobby_non_moderators', $event->getArguments());
+ }
+ });
$dispatcher->addListener(Room::class . '::postAddUsers', function(GenericEvent $event) {
$participants = $event->getArgument('users');
diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php
index 73e9b44185f..1c4ee3e1352 100644
--- a/lib/Controller/CallController.php
+++ b/lib/Controller/CallController.php
@@ -48,6 +48,7 @@ public function __construct(string $appName,
* @PublicPage
* @RequireParticipant
* @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
*
* @return DataResponse
*/
@@ -75,6 +76,7 @@ public function getPeersForCall(): DataResponse {
* @PublicPage
* @RequireParticipant
* @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
*
* @param int|null $flags
* @return DataResponse
@@ -100,6 +102,7 @@ public function joinCall(?int $flags): DataResponse {
/**
* @PublicPage
* @RequireParticipant
+ * @RequireModeratorOrNoLobby
*
* @return DataResponse
*/
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index cf8ebeca420..4a38d0277b9 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -112,6 +112,7 @@ public function __construct(string $appName,
* @PublicPage
* @RequireParticipant
* @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
*
* Sends a new chat message to the given room.
*
@@ -193,6 +194,7 @@ public function sendMessage(string $message, string $actorDisplayName = '', int
/**
* @PublicPage
* @RequireParticipant
+ * @RequireModeratorOrNoLobby
*
* Receives chat messages from the given room.
*
@@ -369,6 +371,7 @@ public function setReadMarker(int $lastReadMessage): DataResponse {
* @PublicPage
* @RequireParticipant
* @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
*
* @param string $search
* @param int $limit
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index bef74ff4681..45f85b50f9a 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -37,6 +37,7 @@
use OCA\Spreed\Participant;
use OCA\Spreed\Room;
use OCA\Spreed\TalkSession;
+use OCA\Spreed\Webinary;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -201,6 +202,13 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant): arr
$lastActivity = 0;
}
+ $lobbyTimer = $room->getLobbyTimer();
+ if ($lobbyTimer instanceof \DateTimeInterface) {
+ $lobbyTimer = $lobbyTimer->getTimestamp();
+ } else {
+ $lobbyTimer = 0;
+ }
+
$roomData = array_merge($roomData, [
'name' => $room->getName(),
'displayName' => $room->getDisplayName($currentParticipant->getUser()),
@@ -216,6 +224,10 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant): arr
'lastActivity' => $lastActivity,
'isFavorite' => $currentParticipant->isFavorite(),
'notificationLevel' => $currentParticipant->getNotificationLevel(),
+ 'lobbyState' => $room->getLobbyState(),
+ 'lobbyTimer' => $lobbyTimer,
+ 'lastPing' => $currentParticipant->getLastPing(),
+ 'sessionId' => $currentParticipant->getSessionId(),
]);
if ($roomData['notificationLevel'] === Participant::NOTIFY_DEFAULT) {
@@ -226,6 +238,12 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant): arr
}
}
+ if ($room->getLobbyState() === Webinary::LOBBY_NON_MODERATORS &&
+ !$currentParticipant->hasModeratorPermissions()) {
+ // No participants and chat messages for users in the lobby.
+ return $roomData;
+ }
+
$currentUser = $this->userManager->get($currentParticipant->getUser());
if ($currentUser instanceof IUser) {
$lastReadMessage = $currentParticipant->getLastReadMessage();
@@ -295,8 +313,6 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant): arr
}
$roomData = array_merge($roomData, [
- 'lastPing' => $currentParticipant->getLastPing(),
- 'sessionId' => $currentParticipant->getSessionId(),
'participants' => $participantList,
'numGuests' => $numActiveGuests,
'lastMessage' => $lastMessage,
@@ -569,6 +585,7 @@ public function deleteRoom(): DataResponse {
/**
* @PublicPage
* @RequireParticipant
+ * @RequireModeratorOrNoLobby
*
* @return DataResponse
*/
@@ -894,10 +911,9 @@ public function joinRoom(string $token, string $password = ''): DataResponse {
$this->session->removePasswordForRoom($token);
$this->session->setSessionForRoom($token, $newSessionId);
$room->ping($this->userId, $newSessionId, $this->timeFactory->getTime());
+ $currentParticipant = $room->getParticipantBySession($newSessionId);
- return new DataResponse([
- 'sessionId' => $newSessionId,
- ]);
+ return new DataResponse($this->formatRoom($room, $currentParticipant));
}
/**
diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index 8a154158923..60b3f4437e2 100644
--- a/lib/Controller/SignalingController.php
+++ b/lib/Controller/SignalingController.php
@@ -417,8 +417,6 @@ private function backendRoom(array $roomRequest): DataResponse {
}
if ($action === 'join') {
- // Rooms get sorted by last ping time for users, so make sure to
- // update when a user joins a room.
$room->ping($userId, $sessionId, $this->timeFactory->getTime());
} else if ($action === 'leave') {
if (!empty($userId)) {
diff --git a/lib/Controller/WebinaryController.php b/lib/Controller/WebinaryController.php
new file mode 100644
index 00000000000..176e3052907
--- /dev/null
+++ b/lib/Controller/WebinaryController.php
@@ -0,0 +1,69 @@
+
+ *
+ * @author Joas Schilling
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Spreed\Controller;
+
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IRequest;
+
+class WebinaryController extends AEnvironmentAwareController {
+
+ /** @var ITimeFactory */
+ protected $timeFactory;
+
+ public function __construct(string $appName,
+ IRequest $request,
+ ITimeFactory $timeFactory) {
+ parent::__construct($appName, $request);
+ $this->timeFactory = $timeFactory;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @RequireModeratorParticipant
+ *
+ * @param int $state
+ * @param int|null $timer
+ * @return DataResponse
+ */
+ public function setLobby(int $state, ?int $timer = null): DataResponse {
+ $timerDateTime = null;
+ if ($timer !== null && $timer > 0) {
+ try {
+ $timerDateTime = $this->timeFactory->getDateTime('@' . $timer);
+ $timerDateTime->setTimezone(new \DateTimeZone('UTC'));
+ } catch (\Exception $e) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+ }
+
+ if (!$this->room->setLobby($state, $timerDateTime)) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ return new DataResponse();
+ }
+}
diff --git a/lib/Manager.php b/lib/Manager.php
index 8933f317008..44b3e50616f 100644
--- a/lib/Manager.php
+++ b/lib/Manager.php
@@ -117,6 +117,11 @@ public function createRoomObject(array $row): Room {
$lastActivity = $this->timeFactory->getDateTime($row['last_activity']);
}
+ $lobbyTimer = null;
+ if (!empty($row['lobby_timer'])) {
+ $lobbyTimer = $this->timeFactory->getDateTime($row['lobby_timer']);
+ }
+
$lastMessage = null;
if (!empty($row['comment_id'])) {
$lastMessage = $this->commentsManager->getCommentFromData(array_merge($row, [
@@ -140,6 +145,7 @@ public function createRoomObject(array $row): Room {
(int) $row['id'],
(int) $row['type'],
(int) $row['read_only'],
+ (int) $row['lobby_state'],
$row['token'],
$row['name'],
$row['password'],
@@ -147,6 +153,7 @@ public function createRoomObject(array $row): Room {
$activeSince,
$lastActivity,
$lastMessage,
+ $lobbyTimer,
(string) $row['object_type'],
(string) $row['object_id']
);
diff --git a/lib/Middleware/Exceptions/LobbyException.php b/lib/Middleware/Exceptions/LobbyException.php
new file mode 100644
index 00000000000..12ed483cead
--- /dev/null
+++ b/lib/Middleware/Exceptions/LobbyException.php
@@ -0,0 +1,31 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Spreed\Middleware\Exceptions;
+
+use OCP\AppFramework\Http;
+
+class LobbyException extends \Exception {
+ public function __construct() {
+ parent::__construct('The conversation is not open to join right now', Http::STATUS_FORBIDDEN);
+ }
+}
diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php
index 738da0ebaf3..86be2469186 100644
--- a/lib/Middleware/InjectionMiddleware.php
+++ b/lib/Middleware/InjectionMiddleware.php
@@ -28,10 +28,12 @@
use OCA\Spreed\Exceptions\ParticipantNotFoundException;
use OCA\Spreed\Exceptions\RoomNotFoundException;
use OCA\Spreed\Manager;
+use OCA\Spreed\Middleware\Exceptions\LobbyException;
use OCA\Spreed\Middleware\Exceptions\NotAModeratorException;
use OCA\Spreed\Middleware\Exceptions\ReadOnlyException;
use OCA\Spreed\Room;
use OCA\Spreed\TalkSession;
+use OCA\Spreed\Webinary;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Response;
@@ -74,6 +76,7 @@ public function __construct(IRequest $request,
* @throws ParticipantNotFoundException
* @throws NotAModeratorException
* @throws ReadOnlyException
+ * @throws LobbyException
*/
public function beforeController($controller, $methodName): void {
if (!$controller instanceof AEnvironmentAwareController) {
@@ -99,6 +102,10 @@ public function beforeController($controller, $methodName): void {
if ($this->reflector->hasAnnotation('RequireReadWriteConversation')) {
$this->checkReadOnlyState($controller);
}
+
+ if ($this->reflector->hasAnnotation('RequireModeratorOrNoLobby')) {
+ $this->checkLobbyState($controller);
+ }
}
/**
@@ -109,37 +116,38 @@ public function beforeController($controller, $methodName): void {
protected function getLoggedIn(AEnvironmentAwareController $controller, bool $moderatorRequired): void {
$token = $this->request->getParam('token');
$room = $this->manager->getRoomForParticipantByToken($token, $this->userId);
+ $controller->setRoom($room);
+
$participant = $room->getParticipant($this->userId);
+ $controller->setParticipant($participant);
if ($moderatorRequired && !$participant->hasModeratorPermissions(false)) {
throw new NotAModeratorException();
}
-
- $controller->setRoom($room);
- $controller->setParticipant($participant);
}
/**
* @param AEnvironmentAwareController $controller
* @param bool $moderatorRequired
* @throws NotAModeratorException
+ * @throws ParticipantNotFoundException
*/
protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired): void {
$token = $this->request->getParam('token');
$room = $this->manager->getRoomForParticipantByToken($token, $this->userId);
+ $controller->setRoom($room);
+
if ($this->userId !== null) {
$participant = $room->getParticipant($this->userId);
} else {
$sessionId = $this->talkSession->getSessionForRoom($token);
$participant = $room->getParticipantBySession($sessionId);
}
+ $controller->setParticipant($participant);
if ($moderatorRequired && !$participant->hasModeratorPermissions()) {
throw new NotAModeratorException();
}
-
- $controller->setRoom($room);
- $controller->setParticipant($participant);
}
/**
@@ -153,6 +161,24 @@ protected function checkReadOnlyState(AEnvironmentAwareController $controller):
}
}
+ /**
+ * @param AEnvironmentAwareController $controller
+ * @throws LobbyException
+ */
+ protected function checkLobbyState(AEnvironmentAwareController $controller): void {
+ try {
+ $this->getLoggedInOrGuest($controller, true);
+ return;
+ } catch (NotAModeratorException $e) {
+ } catch (ParticipantNotFoundException $e) {
+ }
+
+ $room = $controller->getRoom();
+ if (!$room instanceof Room || $room->getLobbyState() !== Webinary::LOBBY_NONE) {
+ throw new LobbyException();
+ }
+ }
+
/**
* @param Controller $controller
* @param string $methodName
@@ -170,6 +196,14 @@ public function afterException($controller, $methodName, \Exception $exception):
return new RedirectToDefaultAppResponse();
}
+ if ($exception instanceof LobbyException) {
+ if ($controller instanceof OCSController) {
+ throw new OCSException('', Http::STATUS_PRECONDITION_FAILED);
+ }
+
+ return new RedirectToDefaultAppResponse();
+ }
+
if ($exception instanceof NotAModeratorException ||
$exception instanceof ReadOnlyException) {
if ($controller instanceof OCSController) {
diff --git a/lib/Migration/Version6099Date20190627172429.php b/lib/Migration/Version6099Date20190627172429.php
new file mode 100644
index 00000000000..6e8328d43ad
--- /dev/null
+++ b/lib/Migration/Version6099Date20190627172429.php
@@ -0,0 +1,61 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Spreed\Migration;
+
+use Closure;
+use Doctrine\DBAL\Types\Type;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Migration\IOutput;
+
+class Version6099Date20190627172429 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable('talk_rooms')) {
+ $table = $schema->getTable('talk_rooms');
+
+ if (!$table->hasColumn('lobby_state')) {
+ $table->addColumn('lobby_state', Type::INTEGER, [
+ 'notnull' => true,
+ 'length' => 6,
+ 'default' => 0,
+ ]);
+ $table->addColumn('lobby_timer', Type::DATETIME, [
+ 'notnull' => false,
+ ]);
+ }
+ }
+
+ return $schema;
+ }
+}
diff --git a/lib/Room.php b/lib/Room.php
index 3e6f1ab2425..b6022ca4fbd 100644
--- a/lib/Room.php
+++ b/lib/Room.php
@@ -71,6 +71,10 @@ class Room {
private $type;
/** @var int */
private $readOnly;
+ /** @var int */
+ private $lobbyState;
+ /** @var \DateTime|null */
+ private $lobbyTimer;
/** @var string */
private $token;
/** @var string */
@@ -104,6 +108,7 @@ public function __construct(Manager $manager,
int $id,
int $type,
int $readOnly,
+ int $lobbyState,
string $token,
string $name,
string $password,
@@ -111,6 +116,7 @@ public function __construct(Manager $manager,
\DateTime $activeSince = null,
\DateTime $lastActivity = null,
IComment $lastMessage = null,
+ \DateTime $lobbyTimer = null,
string $objectType = '',
string $objectId = '') {
$this->manager = $manager;
@@ -122,6 +128,7 @@ public function __construct(Manager $manager,
$this->id = $id;
$this->type = $type;
$this->readOnly = $readOnly;
+ $this->lobbyState = $lobbyState;
$this->token = $token;
$this->name = $name;
$this->password = $password;
@@ -129,6 +136,7 @@ public function __construct(Manager $manager,
$this->activeSince = $activeSince;
$this->lastActivity = $lastActivity;
$this->lastMessage = $lastMessage;
+ $this->lobbyTimer = $lobbyTimer;
$this->objectType = $objectType;
$this->objectId = $objectId;
}
@@ -145,6 +153,22 @@ public function getReadOnly(): int {
return $this->readOnly;
}
+ public function getLobbyState(): int {
+ $this->validateTimer();
+ return $this->lobbyState;
+ }
+
+ public function getLobbyTimer(): ?\DateTime {
+ $this->validateTimer();
+ return $this->lobbyTimer;
+ }
+
+ protected function validateTimer(): void {
+ if ($this->lobbyTimer !== null && $this->lobbyTimer < $this->timeFactory->getDateTime()) {
+ $this->setLobby(Webinary::LOBBY_NONE, null, true);
+ }
+ }
+
public function getToken(): string {
return $this->token;
}
@@ -497,6 +521,56 @@ public function setReadOnly(int $newState): bool {
return true;
}
+ /**
+ * @param int $newState Currently it is only allowed to change between
+ * `Webinary::LOBBY_NON_MODERATORS` and `Webinary::LOBBY_NONE`
+ * Also it's not allowed in one-to-one conversations,
+ * file conversations and password request conversations.
+ * @param \DateTime|null $dateTime
+ * @param bool $timerReached
+ * @return bool True when the change was valid, false otherwise
+ */
+ public function setLobby(int $newState, ?\DateTime $dateTime, bool $timerReached = false): bool {
+ $oldState = $this->lobbyState;
+
+ if (!in_array($this->getType(), [self::GROUP_CALL, self::PUBLIC_CALL], true)) {
+ return false;
+ }
+
+ if ($this->getObjectType() !== '') {
+ return false;
+ }
+
+ if (!in_array($newState, [Webinary::LOBBY_NON_MODERATORS, Webinary::LOBBY_NONE], true)) {
+ return false;
+ }
+
+ $this->dispatcher->dispatch(self::class . '::preSetLobbyState', new GenericEvent($this, [
+ 'newState' => $newState,
+ 'oldState' => $oldState,
+ 'lobbyTimer' => $dateTime,
+ 'timerReached' => $timerReached,
+ ]));
+
+ $query = $this->db->getQueryBuilder();
+ $query->update('talk_rooms')
+ ->set('lobby_state', $query->createNamedParameter($newState, IQueryBuilder::PARAM_INT))
+ ->set('lobby_timer', $query->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
+ $query->execute();
+
+ $this->lobbyState = $newState;
+
+ $this->dispatcher->dispatch(self::class . '::postSetLobbyState', new GenericEvent($this, [
+ 'newState' => $newState,
+ 'oldState' => $oldState,
+ 'lobbyTimer' => $dateTime,
+ 'timerReached' => $timerReached,
+ ]));
+
+ return true;
+ }
+
public function ensureOneToOneRoomIsFilled(): void {
if ($this->getType() !== self::ONE_TO_ONE_CALL) {
return;
diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php
index 7bc25e29b07..636644b95ed 100644
--- a/lib/Signaling/Listener.php
+++ b/lib/Signaling/Listener.php
@@ -163,6 +163,17 @@ protected static function registerExternalSignaling(EventDispatcherInterface $di
$room = $event->getSubject();
$notifier->roomModified($room);
});
+ $dispatcher->addListener(Room::class . '::postSetLobbyState', function(GenericEvent $event) {
+ if (self::isUsingInternalSignaling()) {
+ return;
+ }
+
+ /** @var BackendNotifier $notifier */
+ $notifier = \OC::$server->query(BackendNotifier::class);
+
+ $room = $event->getSubject();
+ $notifier->roomModified($room);
+ });
$dispatcher->addListener(Room::class . '::postSetParticipantType', function(GenericEvent $event) {
if (self::isUsingInternalSignaling()) {
return;
diff --git a/lib/Webinary.php b/lib/Webinary.php
new file mode 100644
index 00000000000..869140db436
--- /dev/null
+++ b/lib/Webinary.php
@@ -0,0 +1,31 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Spreed;
+
+
+class Webinary {
+
+ public const LOBBY_NONE = 0;
+ public const LOBBY_NON_MODERATORS = 1;
+
+}
diff --git a/tests/acceptance/features/bootstrap/ConversationInfoContext.php b/tests/acceptance/features/bootstrap/ConversationInfoContext.php
index 3186bc7de4e..39157975b35 100644
--- a/tests/acceptance/features/bootstrap/ConversationInfoContext.php
+++ b/tests/acceptance/features/bootstrap/ConversationInfoContext.php
@@ -126,6 +126,48 @@ public static function passwordField() {
describedAs("Password field in room moderation menu in conversation info");
}
+ /**
+ * @return Locator
+ */
+ public static function enableForAllParticipantsRadioButton() {
+ // forThe()->radioButton("All participants") can not be used here; that
+ // would return the radio button itself, but the element that the user
+ // interacts with is the label.
+ return Locator::forThe()->css(".all-participants-label")->
+ descendantOf(self::roomModerationMenu())->
+ describedAs("Enable for all participants button in room moderation menu in conversation info");
+ }
+
+ /**
+ * @return Locator
+ */
+ public static function enableForAllParticipantsRadioButtonInput() {
+ return Locator::forThe()->radioButton("All participants")->
+ descendantOf(self::roomModerationMenu())->
+ describedAs("Enable for all participants button input in room moderation menu in conversation info");
+ }
+
+ /**
+ * @return Locator
+ */
+ public static function enableForModeratorsOnlyRadioButton() {
+ // forThe()->radioButton("Moderators only") can not be used here; that
+ // would return the radio button itself, but the element that the user
+ // interacts with is the label.
+ return Locator::forThe()->css(".moderators-only-label")->
+ descendantOf(self::roomModerationMenu())->
+ describedAs("Enable for moderators only button in room moderation menu in conversation info");
+ }
+
+ /**
+ * @return Locator
+ */
+ public static function enableForModeratorsOnlyRadioButtonInput() {
+ return Locator::forThe()->radioButton("Moderators only")->
+ descendantOf(self::roomModerationMenu())->
+ describedAs("Enable for moderators only button input in room moderation menu in conversation info");
+ }
+
/**
* @Given I rename the conversation to :newConversationName
*/
@@ -156,6 +198,24 @@ public function iProtectTheConversationWithThePassword($password) {
$this->actor->find(self::passwordField(), 2)->setValue($password . "\r");
}
+ /**
+ * @When I enable the conversation for all participants
+ */
+ public function iEnableTheConversationForAllParticipants() {
+ $this->showRoomModerationMenu();
+
+ $this->actor->find(self::enableForAllParticipantsRadioButton(), 2)->click();
+ }
+
+ /**
+ * @When I enable the conversation for moderators only
+ */
+ public function iEnableTheConversationForModeratorsOnly() {
+ $this->showRoomModerationMenu();
+
+ $this->actor->find(self::enableForModeratorsOnlyRadioButton(), 2)->click();
+ }
+
/**
* @Then I see that the conversation is password protected
*/
@@ -180,6 +240,30 @@ public function iSeeThatTheConversationIsNotPasswordProtected() {
$this->actor->find(self::roomModerationButton(), 2)->click();
}
+ /**
+ * @Then I see that the conversation is enabled for all participants
+ */
+ public function iSeeThatTheConversationIsEnabledForAllParticipants() {
+ $this->showRoomModerationMenu();
+
+ PHPUnit_Framework_Assert::assertTrue($this->actor->find(self::enableForAllParticipantsRadioButtonInput(), 10)->isChecked(), "Enable for all participants radio button is checked");
+
+ // Hide menu again after checking the button.
+ $this->actor->find(self::roomModerationButton(), 2)->click();
+ }
+
+ /**
+ * @Then I see that the conversation is enabled for moderators only
+ */
+ public function iSeeThatTheConversationIsEnabledForModeratorsOnly() {
+ $this->showRoomModerationMenu();
+
+ PHPUnit_Framework_Assert::assertTrue($this->actor->find(self::enableForModeratorsOnlyRadioButtonInput(), 10)->isChecked(), "Enable for moderators only radio button is checked");
+
+ // Hide menu again after checking the button.
+ $this->actor->find(self::roomModerationButton(), 2)->click();
+ }
+
private function showRoomModerationMenu() {
// The room moderation menu is hidden after clicking on an action of the
// menu. Therefore, if the menu is visible, wait a little just in case
diff --git a/tests/acceptance/features/lobby.feature b/tests/acceptance/features/lobby.feature
new file mode 100644
index 00000000000..0c1a96e35dc
--- /dev/null
+++ b/tests/acceptance/features/lobby.feature
@@ -0,0 +1,71 @@
+Feature: lobby
+
+ Scenario: join public lobby as a user
+ Given I act as John
+ And I am logged in
+ And I have opened the Talk app
+ And I create a public conversation named "Public"
+ And I enable the conversation for moderators only
+ And I see that the conversation is enabled for moderators only
+ And I write down the public conversation link
+ And I add "admin" to the participants
+ When I act as Jane
+ And I am logged in as the admin
+ And I visit the public conversation link I wrote down
+ Then I see that the "Public" conversation is active
+ And I see that the "Public Waiting for the conversation to be opened" empty content message is shown in the main view
+ And I see that the sidebar is closed
+
+ Scenario: join public lobby as a self-joined user
+ Given I act as John
+ And I am logged in
+ And I have opened the Talk app
+ And I create a public conversation named "Public"
+ And I enable the conversation for moderators only
+ And I see that the conversation is enabled for moderators only
+ And I write down the public conversation link
+ When I act as Jane
+ And I am logged in as the admin
+ And I visit the public conversation link I wrote down
+ Then I see that the "Public" conversation is active
+ And I see that the "Public Waiting for the conversation to be opened" empty content message is shown in the main view
+ And I see that the sidebar is closed
+
+
+
+ Scenario: join public lobby protected by password as a user
+ Given I act as John
+ And I am logged in
+ And I have opened the Talk app
+ And I create a public conversation named "Public"
+ And I enable the conversation for moderators only
+ And I see that the conversation is enabled for moderators only
+ And I protect the conversation with the password "abcdef"
+ And I see that the conversation is password protected
+ And I write down the public conversation link
+ And I add "admin" to the participants
+ When I act as Jane
+ And I am logged in as the admin
+ And I visit the public conversation link I wrote down
+ Then I see that the "Public" conversation is active
+ And I see that the "Public Waiting for the conversation to be opened" empty content message is shown in the main view
+ And I see that the sidebar is closed
+
+ Scenario: join public lobby protected by password as a self-joined user
+ Given I act as John
+ And I am logged in
+ And I have opened the Talk app
+ And I create a public conversation named "Public"
+ And I enable the conversation for moderators only
+ And I see that the conversation is enabled for moderators only
+ And I protect the conversation with the password "abcdef"
+ And I see that the conversation is password protected
+ And I write down the public conversation link
+ When I act as Jane
+ And I am logged in as the admin
+ And I visit the public conversation link I wrote down
+ And I see that the current page is the Authenticate page for the public conversation link I wrote down
+ And I authenticate with password "abcdef" in public conversation
+ Then I see that the "Public" conversation is active
+ And I see that the "Public Waiting for the conversation to be opened" empty content message is shown in the main view
+ And I see that the sidebar is closed
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 0c81e2fea7b..1c8ce26c986 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -215,7 +215,7 @@ private function guestIsParticipantOfRoom($guest, $isParticipant, $identifier) {
if ($isParticipant) {
$this->assertStatusCode($this->response, 200);
- PHPUnit_Framework_Assert::assertEquals(self::$userToSessionId[$guest], sha1($response['sessionId']));
+ PHPUnit_Framework_Assert::assertEquals(self::$userToSessionId[$guest], $response['sessionId']);
return;
}
@@ -385,7 +385,7 @@ public function userJoinsRoom($user, $identifier, $statusCode, TableNode $formDa
// database, though, so the ID stored in the database and returned
// in chat messages is a hashed version instead.
self::$sessionIdToUser[sha1($response['sessionId'])] = $user;
- self::$userToSessionId[$user] = sha1($response['sessionId']);
+ self::$userToSessionId[$user] = $response['sessionId'];
}
}
@@ -480,6 +480,30 @@ public function userSetsTheRoomPassword($user, $password, $identifier, $statusCo
$this->assertStatusCode($this->response, $statusCode);
}
+ /**
+ * @When /^user "([^"]*)" sets lobby state for room "([^"]*)" to "([^"]*)" with (\d+)$/
+ *
+ * @param string $user
+ * @param string $identifier
+ * @param string $lobbyState
+ * @param string $statusCode
+ * @param TableNode
+ */
+ public function userSetsLobbyStateForRoomTo($user, $identifier, $lobbyState, $statusCode) {
+ if ($lobbyState === 'no lobby') {
+ $lobbyState = 0;
+ } else if ($lobbyState === 'non moderators') {
+ $lobbyState = 1;
+ }
+
+ $this->setCurrentUser($user);
+ $this->sendRequest(
+ 'PUT', '/apps/spreed/api/v1/room/' . self::$identifierToToken[$identifier] . '/webinary/lobby',
+ new TableNode([['state', $lobbyState]])
+ );
+ $this->assertStatusCode($this->response, $statusCode);
+ }
+
/**
* @Then /^user "([^"]*)" makes room "([^"]*)" (public|private) with (\d+)$/
*
@@ -541,11 +565,18 @@ public function userAddUserToRoom($user, $newUser, $identifier, $statusCode) {
* @param string $statusCode
*/
public function userPromoteDemoteInRoom($user, $isPromotion, $participant, $identifier, $statusCode) {
+ $requestParameters = [['participant', $participant]];
+
+ if (substr($participant, 0, strlen('guest')) === 'guest') {
+ $sessionId = self::$userToSessionId[$participant];
+ $requestParameters = [['sessionId', $sessionId]];
+ }
+
$this->setCurrentUser($user);
$this->sendRequest(
$isPromotion === 'promotes' ? 'POST' : 'DELETE',
'/apps/spreed/api/v1/room/' . self::$identifierToToken[$identifier] . '/moderators',
- new TableNode([['participant', $participant]])
+ new TableNode($requestParameters)
);
$this->assertStatusCode($this->response, $statusCode);
}
@@ -573,7 +604,7 @@ public function userJoinsCall($user, $identifier, $statusCode, TableNode $formDa
// database, though, so the ID stored in the database and returned
// in chat messages is a hashed version instead.
self::$sessionIdToUser[sha1($response['sessionId'])] = $user;
- self::$userToSessionId[$user] = sha1($response['sessionId']);
+ self::$userToSessionId[$user] = $response['sessionId'];
}
}
diff --git a/tests/integration/features/conversation/lobby.feature b/tests/integration/features/conversation/lobby.feature
new file mode 100644
index 00000000000..c48a013e749
--- /dev/null
+++ b/tests/integration/features/conversation/lobby.feature
@@ -0,0 +1,209 @@
+Feature: conversation/lobby
+
+ Background:
+ Given user "participant1" exists
+ Given user "participant2" exists
+ Given user "participant3" exists
+ Given user "participant4" exists
+
+ Scenario: set lobby state in group room
+ Given user "participant1" creates room "room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ And user "participant1" sets lobby state for room "room" to "no lobby" with 200
+ And user "participant2" sets lobby state for room "room" to "non moderators" with 200
+ And user "participant2" sets lobby state for room "room" to "no lobby" with 200
+ And user "participant3" sets lobby state for room "room" to "non moderators" with 403
+ And user "participant3" sets lobby state for room "room" to "no lobby" with 403
+
+ Scenario: set lobby state in public room
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ And user "participant1" sets lobby state for room "room" to "no lobby" with 200
+ And user "participant2" sets lobby state for room "room" to "non moderators" with 200
+ And user "participant2" sets lobby state for room "room" to "no lobby" with 200
+ And user "participant3" sets lobby state for room "room" to "non moderators" with 403
+ And user "participant3" sets lobby state for room "room" to "no lobby" with 403
+ And user "participant4" sets lobby state for room "room" to "non moderators" with 403
+ And user "participant4" sets lobby state for room "room" to "no lobby" with 403
+ And user "guest" sets lobby state for room "room" to "non moderators" with 401
+ And user "guest" sets lobby state for room "room" to "no lobby" with 401
+ And user "guest2" sets lobby state for room "room" to "non moderators" with 401
+ And user "guest2" sets lobby state for room "room" to "no lobby" with 401
+
+ Scenario: set lobby state in one-to-one room
+ Given user "participant1" creates room "room"
+ | roomType | 1 |
+ | invite | participant2 |
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 400
+ And user "participant1" sets lobby state for room "room" to "no lobby" with 400
+ And user "participant2" sets lobby state for room "room" to "non moderators" with 400
+ And user "participant2" sets lobby state for room "room" to "no lobby" with 400
+
+ Scenario: set lobby state in file room
+ Given user "participant1" shares "welcome.txt" with user "participant2" with OCS 100
+ And user "participant1" gets the room for path "welcome.txt" with 200
+ And user "participant2" gets the room for path "welcome (2).txt" with 200
+ When user "participant1" sets lobby state for room "file welcome.txt room" to "non moderators" with 403
+ And user "participant1" sets lobby state for room "file welcome.txt room" to "no lobby" with 403
+ And user "participant2" sets lobby state for room "file welcome (2).txt room" to "non moderators" with 403
+ And user "participant2" sets lobby state for room "file welcome (2).txt room" to "no lobby" with 403
+
+ Scenario: set lobby state of a room not joined to
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ When user "participant2" sets lobby state for room "room" to "non moderators" with 404
+ And user "participant2" sets lobby state for room "room" to "no lobby" with 404
+
+
+
+ Scenario: participants can join the room when the lobby is active
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ Then user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+
+ Scenario: participants can join a password protected room when the lobby is active
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" sets password "foobar" for room "room" with 200
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ Then user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ | password | foobar |
+ And user "guest" joins room "room" with 200
+ | password | foobar |
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+ | password | foobar |
+
+ Scenario: lobby prevents chats for non moderators
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ And user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ Then user "participant1" sends message "Message 1" to room "room" with 201
+ And user "participant2" sends message "Message 2" to room "room" with 201
+ And user "participant3" sends message "Message 3" to room "room" with 412
+ And user "participant4" sends message "Message 4" to room "room" with 412
+ And user "guest" sends message "Message 5" to room "room" with 201
+ And user "guest2" sends message "Message 6" to room "room" with 412
+ And user "participant1" sees the following messages in room "room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | room | guests | guest | | Message 5 | [] |
+ | room | users | participant2 | participant2-displayname | Message 2 | [] |
+ | room | users | participant1 | participant1-displayname | Message 1 | [] |
+ And user "participant2" sees the following messages in room "room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | room | guests | guest | | Message 5 | [] |
+ | room | users | participant2 | participant2-displayname | Message 2 | [] |
+ | room | users | participant1 | participant1-displayname | Message 1 | [] |
+ And user "participant3" sees the following messages in room "room" with 412
+ And user "participant4" sees the following messages in room "room" with 412
+ And user "guest" sees the following messages in room "room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | room | guests | guest | | Message 5 | [] |
+ | room | users | participant2 | participant2-displayname | Message 2 | [] |
+ | room | users | participant1 | participant1-displayname | Message 1 | [] |
+ And user "guest2" sees the following messages in room "room" with 412
+
+ Scenario: lobby prevents calls for non moderators
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ And user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ Then user "participant1" joins call "room" with 200
+ And user "participant2" joins call "room" with 200
+ And user "participant3" joins call "room" with 412
+ And user "participant4" joins call "room" with 412
+ And user "guest" joins call "room" with 200
+ And user "guest2" joins call "room" with 412
+ And user "participant1" sees 3 peers in call "room" with 200
+ And user "participant2" sees 3 peers in call "room" with 200
+ And user "participant3" sees 0 peers in call "room" with 412
+ And user "participant4" sees 0 peers in call "room" with 412
+ And user "guest" sees 3 peers in call "room" with 200
+ And user "guest2" sees 0 peers in call "room" with 412
+ And user "participant1" leaves call "room" with 200
+ And user "participant2" leaves call "room" with 200
+ And user "guest" leaves call "room" with 200
+
+ Scenario: lobby prevents some room actions for non moderators
+ Given user "participant1" creates room "room"
+ | roomType | 3 |
+ | roomName | room |
+ And user "participant1" adds "participant2" to room "room" with 200
+ And user "participant1" promotes "participant2" in room "room" with 200
+ And user "participant1" adds "participant3" to room "room" with 200
+ And user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "participant1" promotes "guest" in room "room" with 200
+ And user "guest2" joins room "room" with 200
+ When user "participant1" sets lobby state for room "room" to "non moderators" with 200
+ Then user "participant1" leaves room "room" with 200
+ And user "participant2" leaves room "room" with 200
+ And user "participant3" leaves room "room" with 200
+ And user "participant4" leaves room "room" with 200
+ And user "guest" leaves room "room" with 200
+ And user "guest2" leaves room "room" with 200
+ And user "participant1" joins room "room" with 200
+ And user "participant2" joins room "room" with 200
+ And user "participant3" joins room "room" with 200
+ And user "participant4" joins room "room" with 200
+ And user "guest" joins room "room" with 200
+ And user "guest2" joins room "room" with 200
+ And user "participant2" removes themselves from room "room" with 200
+ And user "participant3" removes themselves from room "room" with 200
diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php
index 123f2b597c9..f5e759df7f9 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -90,6 +90,7 @@ public function testGetCapabilitiesGuest(): void {
'locked-one-to-one-rooms',
'read-only-rooms',
'chat-read-marker',
+ 'webinary-lobby',
],
'config' => [
'chat' => [
@@ -144,6 +145,7 @@ public function testGetCapabilitiesUserAllowed(): void {
'locked-one-to-one-rooms',
'read-only-rooms',
'chat-read-marker',
+ 'webinary-lobby',
],
'config' => [
'chat' => [
diff --git a/tests/php/PasswordVerificationTest.php b/tests/php/PasswordVerificationTest.php
index 97142798591..8cd2a6d9abd 100644
--- a/tests/php/PasswordVerificationTest.php
+++ b/tests/php/PasswordVerificationTest.php
@@ -22,6 +22,7 @@
use OCA\Spreed\Manager;
use OCA\Spreed\Room;
+use OCA\Spreed\Webinary;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
use OCP\Security\IHasher;
@@ -56,6 +57,7 @@ public function testVerifyPassword() {
1,
Room::PUBLIC_CALL,
Room::READ_WRITE,
+ Webinary::LOBBY_NONE,
'foobar',
'Test',
'passy',
diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php
index d6265387955..8d9bc386b1d 100644
--- a/tests/php/Signaling/BackendNotifierTest.php
+++ b/tests/php/Signaling/BackendNotifierTest.php
@@ -30,6 +30,7 @@
use OCA\Spreed\Room;
use OCA\Spreed\Signaling\BackendNotifier;
use OCA\Spreed\TalkSession;
+use OCA\Spreed\Webinary;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\IGroupManager;
@@ -320,6 +321,27 @@ public function testRoomReadOnlyChanged() {
], $bodies);
}
+ public function testRoomLobbyStateChanged() {
+ $room = $this->manager->createPublicRoom();
+ $room->setLobby(Webinary::LOBBY_NON_MODERATORS, null);
+
+ $requests = $this->controller->getRequests();
+ $bodies = array_map(function($request) use ($room) {
+ return json_decode($this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request), true);
+ }, $requests);
+ $this->assertContains([
+ 'type' => 'update',
+ 'update' => [
+ 'userids' => [
+ ],
+ 'properties' => [
+ 'name' => $room->getDisplayName(''),
+ 'type' => $room->getType(),
+ ],
+ ],
+ ], $bodies);
+ }
+
public function testRoomDelete() {
$room = $this->manager->createPublicRoom();
$room->addUsers([