diff --git a/appinfo/routes.php b/appinfo/routes.php index 2b70b37..269123b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,6 +14,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' => 'server#publish_record', 'url' => '/server/record/{recordId}/publish', 'verb' => 'POST'], ['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'], diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index 2176ca3..4912f4c 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -10,6 +10,7 @@ use BigBlueButton\Parameters\GetRecordingsParameters; use BigBlueButton\Parameters\InsertDocumentParameters; use BigBlueButton\Parameters\IsMeetingRunningParameters; use BigBlueButton\Parameters\JoinMeetingParameters; +use BigBlueButton\Parameters\PublishRecordingsParameters; use OCA\BigBlueButton\AppInfo\Application; use OCA\BigBlueButton\AvatarRepository; use OCA\BigBlueButton\Crypto; @@ -262,6 +263,14 @@ class API { return $response->isDeleted(); } + public function publishRecording(string $recordingId, bool $published): bool { + $publishParams = new PublishRecordingsParameters($recordingId, $published); + + $response = $this->getServer()->publishRecordings($publishParams); + + return $response->isPublished(); + } + /** * @return (array|bool|int|string)[] * diff --git a/lib/Controller/RoomShareController.php b/lib/Controller/RoomShareController.php index 4b04371..b819f21 100644 --- a/lib/Controller/RoomShareController.php +++ b/lib/Controller/RoomShareController.php @@ -11,6 +11,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\IGroupManager; use OCP\IRequest; use OCP\IUserManager; @@ -24,6 +25,9 @@ class RoomShareController extends Controller { /** @var IUserManager */ private $userManager; + /** @var IGroupManager */ + private $groupManager; + /** @var RoomService */ private $roomService; @@ -37,6 +41,7 @@ class RoomShareController extends Controller { IRequest $request, RoomShareService $service, IUserManager $userManager, + IGroupManager $groupManager, RoomService $roomService, CircleHelper $circleHelper, $userId @@ -44,6 +49,7 @@ class RoomShareController extends Controller { parent::__construct($appName, $request); $this->service = $service; $this->userManager = $userManager; + $this->groupManager = $groupManager; $this->roomService = $roomService; $this->circleHelper = $circleHelper; $this->userId = $userId; @@ -90,6 +96,14 @@ class RoomShareController extends Controller { } $roomShare->setShareWithDisplayName($circle->getName()); + } elseif ($roomShare->getShareType() === RoomShare::SHARE_TYPE_GROUP) { + $shareWithGroup = $this->groupManager->get($roomShare->getShareWith()); + + if ($shareWithGroup === null) { + continue; + } + + $roomShare->setShareWithDisplayName($shareWithGroup->getDisplayName()); } $shares[] = $roomShare; diff --git a/lib/Controller/ServerController.php b/lib/Controller/ServerController.php index fd1eaf5..476cd0f 100644 --- a/lib/Controller/ServerController.php +++ b/lib/Controller/ServerController.php @@ -88,12 +88,18 @@ class ServerController extends Controller { return new DataResponse([], Http::STATUS_NOT_FOUND); } - if (!$this->permission->isAdmin($room, $this->userId)) { + if (!$this->permission->isUser($room, $this->userId)) { return new DataResponse([], Http::STATUS_FORBIDDEN); } $recordings = $this->server->getRecordings($room); + if (!$this->permission->isAdmin($room, $this->userId)) { + $recordings = array_values(array_filter($recordings, function ($recording) { + return $recording['published']; + })); + } + return new DataResponse($recordings); } @@ -118,6 +124,27 @@ class ServerController extends Controller { return new DataResponse($success); } + /** + * @NoAdminRequired + */ + public function publishRecord(string $recordId, bool $published): DataResponse { + $record = $this->server->getRecording($recordId); + + $room = $this->service->findByUid($record['meetingId']); + + if ($room === null) { + return new DataResponse(false, Http::STATUS_NOT_FOUND); + } + + if (!$this->permission->isAdmin($room, $this->userId)) { + return new DataResponse(false, Http::STATUS_FORBIDDEN); + } + + $success = $this->server->publishRecording($recordId, $published); + + return new DataResponse($success); + } + public function check(?string $url, ?string $secret): DataResponse { if ($url === null || empty($url) || $secret === null || empty($secret)) { return new DataResponse(false); diff --git a/lib/Db/Room.php b/lib/Db/Room.php index 7fb2816..1ab048e 100644 --- a/lib/Db/Room.php +++ b/lib/Db/Room.php @@ -74,6 +74,7 @@ class Room extends Entity implements JsonSerializable { public $cleanLayout; public $joinMuted; public $running; + public $permission; public function __construct() { $this->addType('maxParticipants', 'integer'); @@ -86,6 +87,7 @@ class Room extends Entity implements JsonSerializable { $this->addType('cleanLayout', 'boolean'); $this->addType('joinMuted', 'boolean'); $this->addType('running', 'boolean'); + $this->addType('permission', 'integer'); } public function jsonSerialize(): array { @@ -102,6 +104,7 @@ class Room extends Entity implements JsonSerializable { 'everyoneIsModerator' => boolval($this->everyoneIsModerator), 'requireModerator' => boolval($this->requireModerator), 'shared' => boolval($this->shared), + 'permission' => $this->permission, 'moderatorToken' => $this->moderatorToken, 'listenOnly' => boolval($this->listenOnly), 'mediaCheck' => boolval($this->mediaCheck), diff --git a/lib/Db/RoomMapper.php b/lib/Db/RoomMapper.php index a1a9c3c..8567ef3 100644 --- a/lib/Db/RoomMapper.php +++ b/lib/Db/RoomMapper.php @@ -12,6 +12,16 @@ class RoomMapper extends QBMapper { parent::__construct($db, 'bbb_rooms', Room::class); } + private function joinShares(IQueryBuilder $qb): IQueryBuilder { + $qb->select('r.*') + ->from($this->tableName, 'r') + ->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id')) + ->addSelect($qb->createFunction('count(case when `s`.`permission` IN ('. + RoomShare::PERMISSION_ADMIN.','.RoomShare::PERMISSION_MODERATOR.','.RoomShare::PERMISSION_USER + .') then 1 else null end) as shared')); + return $qb; + } + /** * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws DoesNotExistException @@ -19,10 +29,7 @@ class RoomMapper extends QBMapper { public function find(int $id): Room { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('r.*') - ->from($this->tableName, 'r') - ->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id')) - ->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared')) + $this->joinShares($qb) ->where($qb->expr()->eq('r.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->groupBy('r.id'); ; @@ -38,10 +45,7 @@ class RoomMapper extends QBMapper { public function findByUid(string $uid): Room { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('r.*') - ->from($this->tableName, 'r') - ->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id')) - ->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared')) + $this->joinShares($qb) ->where($qb->expr()->eq('r.uid', $qb->createNamedParameter($uid))) ->groupBy('r.id'); ; @@ -70,25 +74,20 @@ class RoomMapper extends QBMapper { public function findAll(string $userId, array $groupIds, array $circleIds): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('r.*') - ->from($this->tableName, 'r') - ->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id')) - ->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared')) + $this->joinShares($qb) + ->addSelect($qb->createFunction('min(case when '.$qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)).' then '.RoomShare::PERMISSION_ADMIN.' else `s`.`permission` end) as permission')) ->where( $qb->expr()->orX( $qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)), $qb->expr()->andX( - $qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)), $qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_USER, IQueryBuilder::PARAM_INT)), $qb->expr()->eq('s.share_with', $qb->createNamedParameter($userId)) ), $qb->expr()->andX( - $qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)), $qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_GROUP, IQueryBuilder::PARAM_INT)), $qb->expr()->in('s.share_with', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)) ), $qb->expr()->andX( - $qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)), $qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)), $qb->expr()->in('s.share_with', $qb->createNamedParameter($circleIds, IQueryBuilder::PARAM_STR_ARRAY)) ) @@ -99,7 +98,7 @@ class RoomMapper extends QBMapper { /** @var array */ return $this->findEntities($qb); } - + /** * @return array */ diff --git a/tests/Unit/Controller/RoomShareControllerTest.php b/tests/Unit/Controller/RoomShareControllerTest.php index afd75e2..220bb23 100644 --- a/tests/Unit/Controller/RoomShareControllerTest.php +++ b/tests/Unit/Controller/RoomShareControllerTest.php @@ -9,6 +9,7 @@ use OCA\BigBlueButton\Db\RoomShare; use OCA\BigBlueButton\Service\RoomService; use OCA\BigBlueButton\Service\RoomShareService; use OCP\AppFramework\Http; +use OCP\IGroupManager; use OCP\IRequest; use OCP\IUserManager; use PHPUnit\Framework\TestCase; @@ -19,6 +20,7 @@ class RoomShareControllerTest extends TestCase { private $roomService; private $circleHelper; private $userManager; + private $groupManager; private $controller; private $userId = 'user_foo'; @@ -29,6 +31,7 @@ class RoomShareControllerTest extends TestCase { $this->request = $this->createMock(IRequest::class); $this->service = $this->createMock(RoomShareService::class); $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); $this->roomService = $this->createMock(RoomService::class); $this->circleHelper = $this->createMock(CircleHelper::class); @@ -37,6 +40,7 @@ class RoomShareControllerTest extends TestCase { $this->request, $this->service, $this->userManager, + $this->groupManager, $this->roomService, $this->circleHelper, $this->userId diff --git a/ts/Common/Api.ts b/ts/Common/Api.ts index 6a2c278..e31c5b5 100644 --- a/ts/Common/Api.ts +++ b/ts/Common/Api.ts @@ -39,6 +39,7 @@ export interface Room { everyoneIsModerator: boolean; requireModerator: boolean; shared: boolean; + permission: Permission; moderatorToken: string; listenOnly: boolean, mediaCheck: boolean, @@ -200,6 +201,14 @@ class Api { return response.data; } + public async publishRecording(id: string, publish: boolean,) { + const response = await axios.post(this.getUrl(`server/record/${id}/publish`), { + published: publish, + }); + + return response.data; + } + public async storeRecording(recording: Recording, path: string) { const startDate = new Date(recording.startTime); const filename = `${encodeURIComponent(recording.name + ' ' + startDate.toISOString())}.url`; diff --git a/ts/Common/Translation.ts b/ts/Common/Translation.ts index d80eb5d..8d78cf7 100644 --- a/ts/Common/Translation.ts +++ b/ts/Common/Translation.ts @@ -1,4 +1,4 @@ -import { Access } from './Api'; +import { Access, Permission } from './Api'; export const AccessOptions = { [Access.Public]: t('bbb', 'Public'), @@ -8,3 +8,9 @@ export const AccessOptions = { [Access.Internal]: t('bbb', 'Internal'), [Access.InternalRestricted]: t('bbb', 'Internal restricted'), }; + +export const PermissionsOptions = { + [Permission.Admin]: t('bbb', 'admin'), + [Permission.Moderator]: t('bbb', 'moderator'), + [Permission.User]: t('bbb', 'user'), +}; diff --git a/ts/Manager/App.scss b/ts/Manager/App.scss index 1a0458c..09ec746 100644 --- a/ts/Manager/App.scss +++ b/ts/Manager/App.scss @@ -161,8 +161,23 @@ pre { .bbb-shrink { width: 44px; white-space: nowrap; - } + input[type="checkbox"]{ + + &+label:before { + border-radius: 3px; + border-width: 2px; + } + + &:disabled+label:before { + opacity: .5; + } + + &:not(input:checked):disabled+label:before { + background-color: transparent !important; + } + } + } th { padding: 14px 6px; @@ -282,6 +297,10 @@ pre { } } +.bbb-simple-menu { + min-width: auto; +} + .bbb-input-container { display: flex; } diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx index e0a6377..9c4da00 100644 --- a/ts/Manager/EditRoomDialog.tsx +++ b/ts/Manager/EditRoomDialog.tsx @@ -123,17 +123,18 @@ const EditRoomDialog: React.FC = ({ room, restriction, updateProperty, op updateProperty('access', value); })} - {room.access === Access.InternalRestricted &&
- - {descriptions.internalRestrictedShareWith} -
} -
-