mirror of https://github.com/sualko/cloud_bbb
feat: add option for moderator url
parent
af0c769a8c
commit
3b86cf5b4a
|
@ -12,7 +12,7 @@ return [
|
|||
['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'],
|
||||
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
||||
['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' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'],
|
||||
['name' => 'hook#recordingReady', 'url' => '/hook/recording/{token}/{mac}', 'verb' => 'GET'],
|
||||
|
|
|
@ -12,7 +12,6 @@ use BigBlueButton\Parameters\JoinMeetingParameters;
|
|||
use OCA\BigBlueButton\Crypto;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCA\BigBlueButton\Event\MeetingStartedEvent;
|
||||
use OCA\BigBlueButton\Permission;
|
||||
use OCA\BigBlueButton\UrlHelper;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
|
@ -26,9 +25,6 @@ class API {
|
|||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var Permission */
|
||||
private $permission;
|
||||
|
||||
/** @var BigBlueButton|null */
|
||||
private $server;
|
||||
|
||||
|
@ -47,7 +43,6 @@ class API {
|
|||
public function __construct(
|
||||
IConfig $config,
|
||||
IURLGenerator $urlGenerator,
|
||||
Permission $permission,
|
||||
Crypto $crypto,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
IL10N $l10n,
|
||||
|
@ -55,7 +50,6 @@ class API {
|
|||
) {
|
||||
$this->config = $config;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->permission = $permission;
|
||||
$this->crypto = $crypto;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->l10n = $l10n;
|
||||
|
@ -78,8 +72,8 @@ class API {
|
|||
*
|
||||
* @return string join url
|
||||
*/
|
||||
public function createJoinUrl(Room $room, float $creationTime, string $displayname, ?string $uid = null) {
|
||||
$password = $this->permission->isModerator($room, $uid) ? $room->moderatorPassword : $room->attendeePassword;
|
||||
public function createJoinUrl(Room $room, float $creationTime, string $displayname, bool $isModerator, ?string $uid = null) {
|
||||
$password = $isModerator ? $room->moderatorPassword : $room->attendeePassword;
|
||||
|
||||
$joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password);
|
||||
|
||||
|
|
|
@ -78,6 +78,12 @@ class JoinController extends Controller {
|
|||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$moderatorToken = $this->request->getParam('moderatorToken');
|
||||
|
||||
if (!empty($moderatorToken) && $moderatorToken !== $room->moderatorToken) {
|
||||
throw new NoPermissionException();
|
||||
}
|
||||
|
||||
$displayname = trim($displayname);
|
||||
$userId = null;
|
||||
$presentation = null;
|
||||
|
@ -108,7 +114,9 @@ class JoinController extends Controller {
|
|||
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', [
|
||||
'room' => $room->name,
|
||||
'name' => $displayname,
|
||||
|
@ -116,7 +124,7 @@ class JoinController extends Controller {
|
|||
}
|
||||
|
||||
$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]);
|
||||
|
||||
|
|
|
@ -114,7 +114,8 @@ class RoomController extends Controller {
|
|||
bool $record,
|
||||
string $access,
|
||||
bool $everyoneIsModerator,
|
||||
bool $requireModerator
|
||||
bool $requireModerator,
|
||||
?string $moderatorToken
|
||||
): DataResponse {
|
||||
$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 $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator) {
|
||||
return $this->service->update($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, $moderatorToken);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @method bool getEveryoneIsModerator()
|
||||
* @method bool getRequireModerator()
|
||||
* @method bool getEveryoneIsModerator()
|
||||
* @method string getModeratorToken()
|
||||
* @method void setUid(string $uid)
|
||||
* @method void setName(string $name)
|
||||
* @method void setAttendeePassword(string $pw)
|
||||
|
@ -32,6 +33,7 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @method void setPassword(string $pw)
|
||||
* @method void setEveryoneIsModerator(bool $everyone)
|
||||
* @method void setRequireModerator(bool $require)
|
||||
* @method void setModeratorToken(string $moderatorToken)
|
||||
*/
|
||||
class Room extends Entity implements JsonSerializable {
|
||||
public const ACCESS_PUBLIC = 'public';
|
||||
|
@ -55,6 +57,7 @@ class Room extends Entity implements JsonSerializable {
|
|||
public $everyoneIsModerator;
|
||||
public $requireModerator = false;
|
||||
public $shared = false;
|
||||
public $moderatorToken;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('maxParticipants', 'integer');
|
||||
|
@ -78,6 +81,7 @@ class Room extends Entity implements JsonSerializable {
|
|||
'everyoneIsModerator' => boolval($this->everyoneIsModerator),
|
||||
'requireModerator' => boolval($this->requireModerator),
|
||||
'shared' => boolval($this->shared),
|
||||
'moderatorToken' => $this->moderatorToken,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -90,7 +90,7 @@ class RoomService {
|
|||
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 {
|
||||
$room = $this->mapper->find($id);
|
||||
|
||||
|
@ -98,6 +98,10 @@ class RoomService {
|
|||
$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->setWelcome($welcome);
|
||||
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||
|
|
|
@ -95,7 +95,7 @@ class JoinControllerTest extends TestCase {
|
|||
$this->api
|
||||
->expects($this->once())
|
||||
->method('createJoinUrl')
|
||||
->with($this->room, 12345, 'User Bar', 'user_bar')
|
||||
->with($this->room, 12345, 'User Bar', false, 'user_bar')
|
||||
->willReturn($url);
|
||||
|
||||
$result = $this->controller->index(null);
|
||||
|
@ -177,7 +177,7 @@ class JoinControllerTest extends TestCase {
|
|||
$this->api
|
||||
->expects($this->once())
|
||||
->method('createJoinUrl')
|
||||
->with($this->room, 12345, 'Foo Bar', null)
|
||||
->with($this->room, 12345, 'Foo Bar', false, null)
|
||||
->willReturn($url);
|
||||
|
||||
$this->invalidDisplayname('a');
|
||||
|
|
|
@ -38,6 +38,7 @@ export interface Room {
|
|||
everyoneIsModerator: boolean;
|
||||
requireModerator: boolean;
|
||||
shared: boolean;
|
||||
moderatorToken: string;
|
||||
}
|
||||
|
||||
export interface RoomShare {
|
||||
|
@ -121,14 +122,17 @@ class Api {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
public getRoomUrl(room: Room) {
|
||||
public getRoomUrl(room: Room, forModerator = false) {
|
||||
const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || '';
|
||||
const token = (forModerator && room.moderatorToken) ? `${room.uid}/${room.moderatorToken}` : room.uid;
|
||||
|
||||
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[]> {
|
||||
|
|
|
@ -5,7 +5,7 @@ import EditRoomDialog from './EditRoomDialog';
|
|||
type Props = {
|
||||
room: Room;
|
||||
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 }) => {
|
||||
|
|
|
@ -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.'),
|
||||
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.'),
|
||||
moderatorToken: t('bbb', 'If enabled, a moderator URL is generated which allows access with moderator permission.'),
|
||||
};
|
||||
|
||||
type Props = {
|
||||
room: Room;
|
||||
restriction?: Restriction;
|
||||
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
||||
updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
|
||||
open: boolean;
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<h3>{t('bbb', 'Miscellaneous')}</h3>
|
||||
|
|
Loading…
Reference in New Issue