feat: add option for moderator url

pull/120/head
sualko 2021-01-22 19:12:55 +01:00
parent af0c769a8c
commit 3b86cf5b4a
11 changed files with 91 additions and 22 deletions

View File

@ -12,7 +12,7 @@ return [
['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'], ['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'],
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'], ['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'], ['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'], ['name' => 'join#index', 'url' => '/b/{token}/{moderatorToken}', 'verb' => 'GET', 'defaults' => ['moderatorToken' => '']],
['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'], ['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'],
['name' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'], ['name' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'],
['name' => 'hook#recordingReady', 'url' => '/hook/recording/{token}/{mac}', 'verb' => 'GET'], ['name' => 'hook#recordingReady', 'url' => '/hook/recording/{token}/{mac}', 'verb' => 'GET'],

View File

@ -12,7 +12,6 @@ use BigBlueButton\Parameters\JoinMeetingParameters;
use OCA\BigBlueButton\Crypto; use OCA\BigBlueButton\Crypto;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Event\MeetingStartedEvent; use OCA\BigBlueButton\Event\MeetingStartedEvent;
use OCA\BigBlueButton\Permission;
use OCA\BigBlueButton\UrlHelper; use OCA\BigBlueButton\UrlHelper;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig; use OCP\IConfig;
@ -26,9 +25,6 @@ class API {
/** @var IURLGenerator */ /** @var IURLGenerator */
private $urlGenerator; private $urlGenerator;
/** @var Permission */
private $permission;
/** @var BigBlueButton|null */ /** @var BigBlueButton|null */
private $server; private $server;
@ -47,7 +43,6 @@ class API {
public function __construct( public function __construct(
IConfig $config, IConfig $config,
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
Permission $permission,
Crypto $crypto, Crypto $crypto,
IEventDispatcher $eventDispatcher, IEventDispatcher $eventDispatcher,
IL10N $l10n, IL10N $l10n,
@ -55,7 +50,6 @@ class API {
) { ) {
$this->config = $config; $this->config = $config;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->permission = $permission;
$this->crypto = $crypto; $this->crypto = $crypto;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n; $this->l10n = $l10n;
@ -78,8 +72,8 @@ class API {
* *
* @return string join url * @return string join url
*/ */
public function createJoinUrl(Room $room, float $creationTime, string $displayname, ?string $uid = null) { public function createJoinUrl(Room $room, float $creationTime, string $displayname, bool $isModerator, ?string $uid = null) {
$password = $this->permission->isModerator($room, $uid) ? $room->moderatorPassword : $room->attendeePassword; $password = $isModerator ? $room->moderatorPassword : $room->attendeePassword;
$joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password); $joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password);

View File

@ -78,6 +78,12 @@ class JoinController extends Controller {
throw new NotFoundException(); throw new NotFoundException();
} }
$moderatorToken = $this->request->getParam('moderatorToken');
if (!empty($moderatorToken) && $moderatorToken !== $room->moderatorToken) {
throw new NoPermissionException();
}
$displayname = trim($displayname); $displayname = trim($displayname);
$userId = null; $userId = null;
$presentation = null; $presentation = null;
@ -108,7 +114,9 @@ class JoinController extends Controller {
return $response; return $response;
} }
if ($room->requireModerator && ($userId === null || !$this->permission->isModerator($room, $userId)) && !$this->api->isRunning($room)) { $isModerator = (!empty($moderatorToken) && $moderatorToken === $room->moderatorToken) || $this->permission->isModerator($room, $userId);
if ($room->requireModerator && !$isModerator && !$this->api->isRunning($room)) {
return new TemplateResponse($this->appName, 'waiting', [ return new TemplateResponse($this->appName, 'waiting', [
'room' => $room->name, 'room' => $room->name,
'name' => $displayname, 'name' => $displayname,
@ -116,7 +124,7 @@ class JoinController extends Controller {
} }
$creationDate = $this->api->createMeeting($room, $presentation); $creationDate = $this->api->createMeeting($room, $presentation);
$joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $userId); $joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $isModerator, $userId);
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]); \OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);

View File

@ -114,7 +114,8 @@ class RoomController extends Controller {
bool $record, bool $record,
string $access, string $access,
bool $everyoneIsModerator, bool $everyoneIsModerator,
bool $requireModerator bool $requireModerator,
?string $moderatorToken
): DataResponse { ): DataResponse {
$room = $this->service->find($id); $room = $this->service->find($id);
@ -137,8 +138,8 @@ class RoomController extends Controller {
return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST); return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST);
} }
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator) { return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken) {
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator); return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken);
}); });
} }

View File

@ -20,6 +20,7 @@ use OCP\AppFramework\Db\Entity;
* @method bool getEveryoneIsModerator() * @method bool getEveryoneIsModerator()
* @method bool getRequireModerator() * @method bool getRequireModerator()
* @method bool getEveryoneIsModerator() * @method bool getEveryoneIsModerator()
* @method string getModeratorToken()
* @method void setUid(string $uid) * @method void setUid(string $uid)
* @method void setName(string $name) * @method void setName(string $name)
* @method void setAttendeePassword(string $pw) * @method void setAttendeePassword(string $pw)
@ -32,6 +33,7 @@ use OCP\AppFramework\Db\Entity;
* @method void setPassword(string $pw) * @method void setPassword(string $pw)
* @method void setEveryoneIsModerator(bool $everyone) * @method void setEveryoneIsModerator(bool $everyone)
* @method void setRequireModerator(bool $require) * @method void setRequireModerator(bool $require)
* @method void setModeratorToken(string $moderatorToken)
*/ */
class Room extends Entity implements JsonSerializable { class Room extends Entity implements JsonSerializable {
public const ACCESS_PUBLIC = 'public'; public const ACCESS_PUBLIC = 'public';
@ -55,6 +57,7 @@ class Room extends Entity implements JsonSerializable {
public $everyoneIsModerator; public $everyoneIsModerator;
public $requireModerator = false; public $requireModerator = false;
public $shared = false; public $shared = false;
public $moderatorToken;
public function __construct() { public function __construct() {
$this->addType('maxParticipants', 'integer'); $this->addType('maxParticipants', 'integer');
@ -78,6 +81,7 @@ class Room extends Entity implements JsonSerializable {
'everyoneIsModerator' => boolval($this->everyoneIsModerator), 'everyoneIsModerator' => boolval($this->everyoneIsModerator),
'requireModerator' => boolval($this->requireModerator), 'requireModerator' => boolval($this->requireModerator),
'shared' => boolval($this->shared), 'shared' => boolval($this->shared),
'moderatorToken' => $this->moderatorToken,
]; ];
} }
} }

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace OCA\BigBlueButton\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version000000Date20210122164501 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) {
$schema = $schemaClosure();
if ($schema->hasTable('bbb_rooms')) {
$table = $schema->getTable('bbb_rooms');
if (!$table->hasColumn('moderator_token')) {
$table->addColumn('moderator_token', 'string', [
'notnull' => false,
'unique' => true,
'length' => 64
]);
}
return $schema;
}
return null;
}
}

View File

@ -90,7 +90,7 @@ class RoomService {
return $createdRoom; return $createdRoom;
} }
public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator) { public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken) {
try { try {
$room = $this->mapper->find($id); $room = $this->mapper->find($id);
@ -98,6 +98,10 @@ class RoomService {
$room->setPassword($access === Room::ACCESS_PASSWORD ? $this->humanReadableRandom(8) : null); $room->setPassword($access === Room::ACCESS_PASSWORD ? $this->humanReadableRandom(8) : null);
} }
if ($room->moderatorToken !== $moderatorToken) {
$room->setModeratorToken(empty($moderatorToken) ? null : $this->humanReadableRandom(16));
}
$room->setName($name); $room->setName($name);
$room->setWelcome($welcome); $room->setWelcome($welcome);
$room->setMaxParticipants(\max($maxParticipants, 0)); $room->setMaxParticipants(\max($maxParticipants, 0));

View File

@ -95,7 +95,7 @@ class JoinControllerTest extends TestCase {
$this->api $this->api
->expects($this->once()) ->expects($this->once())
->method('createJoinUrl') ->method('createJoinUrl')
->with($this->room, 12345, 'User Bar', 'user_bar') ->with($this->room, 12345, 'User Bar', false, 'user_bar')
->willReturn($url); ->willReturn($url);
$result = $this->controller->index(null); $result = $this->controller->index(null);
@ -177,7 +177,7 @@ class JoinControllerTest extends TestCase {
$this->api $this->api
->expects($this->once()) ->expects($this->once())
->method('createJoinUrl') ->method('createJoinUrl')
->with($this->room, 12345, 'Foo Bar', null) ->with($this->room, 12345, 'Foo Bar', false, null)
->willReturn($url); ->willReturn($url);
$this->invalidDisplayname('a'); $this->invalidDisplayname('a');

View File

@ -38,6 +38,7 @@ export interface Room {
everyoneIsModerator: boolean; everyoneIsModerator: boolean;
requireModerator: boolean; requireModerator: boolean;
shared: boolean; shared: boolean;
moderatorToken: string;
} }
export interface RoomShare { export interface RoomShare {
@ -121,14 +122,17 @@ class Api {
return response.data; return response.data;
} }
public getRoomUrl(room: Room) { public getRoomUrl(room: Room, forModerator = false) {
const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || ''; const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || '';
const token = (forModerator && room.moderatorToken) ? `${room.uid}/${room.moderatorToken}` : room.uid;
if (shortener) { if (shortener) {
return shortener.replace(/\{user\}/g, room.userId).replace(/\{token\}/g, room.uid); return shortener
.replace(/\{user\}/g, room.userId)
.replace(/\{token\}/g, token);
} }
return window.location.origin + api.getUrl(`b/${room.uid}`); return window.location.origin + api.getUrl(`b/${token}`);
} }
public async getRooms(): Promise<Room[]> { public async getRooms(): Promise<Room[]> {

View File

@ -5,7 +5,7 @@ import EditRoomDialog from './EditRoomDialog';
type Props = { type Props = {
room: Room; room: Room;
restriction?: Restriction; restriction?: Restriction;
updateProperty: (key: string, value: string | boolean | number) => Promise<void>; updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
} }
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => { const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => {

View File

@ -13,12 +13,13 @@ const descriptions: { [key: string]: string } = {
access: t('bbb', 'Public: Everyone knowing the link is able to join. Password: Guests have to provide a password. Waiting room: A moderator has to accept every guest before they can join. Internal: Only Nextcloud users can join.'), access: t('bbb', 'Public: Everyone knowing the link is able to join. Password: Guests have to provide a password. Waiting room: A moderator has to accept every guest before they can join. Internal: Only Nextcloud users can join.'),
moderator: t('bbb', 'A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings.'), moderator: t('bbb', 'A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings.'),
requireModerator: t('bbb', 'If enabled, normal users have to wait until a moderator is in the room.'), requireModerator: t('bbb', 'If enabled, normal users have to wait until a moderator is in the room.'),
moderatorToken: t('bbb', 'If enabled, a moderator URL is generated which allows access with moderator permission.'),
}; };
type Props = { type Props = {
room: Room; room: Room;
restriction?: Restriction; restriction?: Restriction;
updateProperty: (key: string, value: string | boolean | number) => Promise<void>; updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
} }
@ -123,6 +124,17 @@ const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, op
<label htmlFor={`bbb-everyoneIsModerator-${room.id}`}>{t('bbb', 'Every participant is moderator')}</label> <label htmlFor={`bbb-everyoneIsModerator-${room.id}`}>{t('bbb', 'Every participant is moderator')}</label>
</div> </div>
<em>{descriptions.moderator}</em> <em>{descriptions.moderator}</em>
<div className="bbb-mt-1">
<input id={`bbb-moderatorToken-${room.id}`}
type="checkbox"
className="checkbox"
checked={!!room.moderatorToken}
onChange={(event) => updateProperty('moderatorToken', event.target.checked ? 'true' : null)} />
<label htmlFor={`bbb-moderatorToken-${room.id}`}>{t('bbb', 'Moderator access via URL')}</label>
</div>
{!!room.moderatorToken && <input type="text" readOnly={true} value={api.getRoomUrl(room, true)} />}
<em>{descriptions.moderatorToken}</em>
</div> </div>
<h3>{t('bbb', 'Miscellaneous')}</h3> <h3>{t('bbb', 'Miscellaneous')}</h3>