diff --git a/appinfo/routes.php b/appinfo/routes.php index 8a9312a..2b70b37 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,6 +8,8 @@ return [ ], 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], + ['name' => 'server#isRunning', 'url' => '/server/{roomUid}/isRunning', 'verb' => 'GET'], + ['name' => 'server#insertDocument', 'url' => '/server/{roomUid}/insertDocument', 'verb' => 'POST'], ['name' => 'server#records', 'url' => '/server/{roomUid}/records', 'verb' => 'GET'], ['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'], ['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'], diff --git a/lib/BackgroundJob/IsRunningJob.php b/lib/BackgroundJob/IsRunningJob.php new file mode 100644 index 0000000..d069eb2 --- /dev/null +++ b/lib/BackgroundJob/IsRunningJob.php @@ -0,0 +1,54 @@ +jobList = $jobList; + $this->service = $service; + $this->api = $api; + + $this->setInterval(15 * 60); + } + + protected function run($argument) { + try { + $room = $this->service->find($argument['id']); + } catch (RoomNotFound $e) { + $this->jobList->remove($this, $argument); + return; + } + + if (!$room->running) { + $this->jobList->remove($this, $argument); + return; + } + + $isRunning = $this->api->isRunning($room); + + if (!$isRunning) { + $this->service->updateRunning($room->id, $isRunning); + + $this->jobList->remove($this, $argument); + } + } +} diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index 378b2e4..2176ca3 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -7,6 +7,7 @@ use BigBlueButton\Core\Record; use BigBlueButton\Parameters\CreateMeetingParameters; use BigBlueButton\Parameters\DeleteRecordingsParameters; use BigBlueButton\Parameters\GetRecordingsParameters; +use BigBlueButton\Parameters\InsertDocumentParameters; use BigBlueButton\Parameters\IsMeetingRunningParameters; use BigBlueButton\Parameters\JoinMeetingParameters; use OCA\BigBlueButton\AppInfo\Application; @@ -320,4 +321,14 @@ class API { return $response->success() && $response->isRunning(); } + + public function insertDocument(Room $room, string $url, string $filename): bool { + $insertDocumentParams = new InsertDocumentParameters($room->getUid()); + + $insertDocumentParams->addPresentation($url, $filename, null, null); + + $response = $this->getServer()->insertDocument($insertDocumentParams); + + return $response->success(); + } } diff --git a/lib/Controller/HookController.php b/lib/Controller/HookController.php index 98c6018..f3a1614 100644 --- a/lib/Controller/HookController.php +++ b/lib/Controller/HookController.php @@ -63,6 +63,8 @@ class HookController extends Controller { $recordingmarks = \boolval($recordingmarks); $room = $this->getRoom(); + $this->service->updateRunning($room->getId(), false); + $this->avatarRepository->clearRoom($room->uid); $this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks)); diff --git a/lib/Controller/JoinController.php b/lib/Controller/JoinController.php index 727673e..1a56a89 100644 --- a/lib/Controller/JoinController.php +++ b/lib/Controller/JoinController.php @@ -2,6 +2,7 @@ namespace OCA\BigBlueButton\Controller; +use OCA\BigBlueButton\BackgroundJob\IsRunningJob; use OCA\BigBlueButton\BigBlueButton\API; use OCA\BigBlueButton\BigBlueButton\Presentation; use OCA\BigBlueButton\Db\Room; @@ -12,6 +13,7 @@ use OCA\BigBlueButton\Service\RoomService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\BackgroundJob\IJobList; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; @@ -38,6 +40,9 @@ class JoinController extends Controller { /** @var Permission */ private $permission; + /** @var IJobList */ + private $jobList; + public function __construct( string $appName, IRequest $request, @@ -45,7 +50,8 @@ class JoinController extends Controller { IURLGenerator $urlGenerator, IUserSession $userSession, API $api, - Permission $permission + Permission $permission, + IJobList $jobList ) { parent::__construct($appName, $request); @@ -54,6 +60,7 @@ class JoinController extends Controller { $this->userSession = $userSession; $this->api = $api; $this->permission = $permission; + $this->jobList = $jobList; } public function setToken(string $token): void { @@ -129,6 +136,8 @@ class JoinController extends Controller { $creationDate = $this->api->createMeeting($room, $presentation); $joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $isModerator, $userId); + $this->markAsRunning($room); + \OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]); return new TemplateResponse($this->appName, 'forward', [ @@ -153,4 +162,18 @@ class JoinController extends Controller { ), ]); } + + private function markAsRunning(Room $room) { + if (!$room->running) { + $this->service->updateRunning($room->getId(), true); + } + + if (!$this->jobList->has(IsRunningJob::class, [ + 'id' => $room->id, + ])) { + $this->jobList->add(IsRunningJob::class, [ + 'id' => $room->id, + ]); + } + } } diff --git a/lib/Controller/ServerController.php b/lib/Controller/ServerController.php index 3ffc0d3..fd1eaf5 100644 --- a/lib/Controller/ServerController.php +++ b/lib/Controller/ServerController.php @@ -40,6 +40,44 @@ class ServerController extends Controller { $this->userId = $UserId; } + /** + * @NoAdminRequired + */ + public function isRunning(string $roomUid): DataResponse { + $room = $this->service->findByUid($roomUid); + + if ($room === null) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$this->permission->isUser($room, $this->userId)) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + $isRunning = $this->server->isRunning($room); + + return new DataResponse($isRunning); + } + + /** + * @NoAdminRequired + */ + public function insertDocument(string $roomUid, string $url, string $filename): DataResponse { + $room = $this->service->findByUid($roomUid); + + if ($room === null) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$this->permission->isModerator($room, $this->userId)) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + $success = $this->server->insertDocument($room, $url, $filename); + + return new DataResponse($success); + } + /** * @NoAdminRequired */ diff --git a/lib/Db/Room.php b/lib/Db/Room.php index e7881b8..7fb2816 100644 --- a/lib/Db/Room.php +++ b/lib/Db/Room.php @@ -25,6 +25,7 @@ use OCP\AppFramework\Db\Entity; * @method bool getMediaCheck() * @method bool getCleanLayout() * @method bool getJoinMuted() + * @method bool getRunning() * @method void setUid(string $uid) * @method void setName(string $name) * @method void setAttendeePassword(string $pw) @@ -42,6 +43,7 @@ use OCP\AppFramework\Db\Entity; * @method void setMediaCheck(bool $mediaCheck) * @method void setCleanLayout(bool $cleanLayout) * @method void setJoinMuted(bool $joinMuted) + * @method void setRunning(bool $running) */ class Room extends Entity implements JsonSerializable { public const ACCESS_PUBLIC = 'public'; @@ -71,6 +73,7 @@ class Room extends Entity implements JsonSerializable { public $mediaCheck; public $cleanLayout; public $joinMuted; + public $running; public function __construct() { $this->addType('maxParticipants', 'integer'); @@ -82,6 +85,7 @@ class Room extends Entity implements JsonSerializable { $this->addType('mediaCheck', 'boolean'); $this->addType('cleanLayout', 'boolean'); $this->addType('joinMuted', 'boolean'); + $this->addType('running', 'boolean'); } public function jsonSerialize(): array { @@ -103,6 +107,7 @@ class Room extends Entity implements JsonSerializable { 'mediaCheck' => boolval($this->mediaCheck), 'cleanLayout' => boolval($this->cleanLayout), 'joinMuted' => boolval($this->joinMuted), + 'running' => boolval($this->running), ]; } } diff --git a/lib/Migration/Version000000Date20220316151400.php b/lib/Migration/Version000000Date20220316151400.php new file mode 100644 index 0000000..551b4ae --- /dev/null +++ b/lib/Migration/Version000000Date20220316151400.php @@ -0,0 +1,42 @@ +hasTable('bbb_rooms')) { + $table = $schema->getTable('bbb_rooms'); + + if (!$table->hasColumn('running')) { + $table->addColumn('running', 'boolean', [ + 'notnull' => false, + 'default' => false + ]); + } + + return $schema; + } + + return null; + } +} diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 198ae95..d8e42b6 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -163,6 +163,21 @@ class RoomService { } } + /** + * @return \OCP\AppFramework\Db\Entity|null + */ + public function updateRunning(int $id, bool $running) { + try { + $room = $this->mapper->find($id); + + $room->setRunning($running); + + return $this->mapper->update($room); + } catch (Exception $e) { + $this->handleException($e); + } + } + /** * @return Room|null */ diff --git a/package.json b/package.json index e8b3a70..1d1b23c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@commitlint/config-conventional": "^16.2.1", "@commitlint/travis-cli": "^16.2.3", "@nextcloud/axios": "^1.3.2", + "@nextcloud/dialogs": "^3.1.2", "@nextcloud/router": "^2.0.0", "@octokit/rest": "^18.0.4", "archiver": "^5.0.0", diff --git a/tests/Unit/Controller/JoinControllerTest.php b/tests/Unit/Controller/JoinControllerTest.php index 5bae8b9..0c66091 100644 --- a/tests/Unit/Controller/JoinControllerTest.php +++ b/tests/Unit/Controller/JoinControllerTest.php @@ -11,6 +11,7 @@ use OCA\BigBlueButton\Service\RoomService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\BackgroundJob\IJobList; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUser; @@ -26,6 +27,7 @@ class JoinControllerTest extends TestCase { private $api; private $permission; private $room; + private $jobList; public function setUp(): void { parent::setUp(); @@ -36,6 +38,7 @@ class JoinControllerTest extends TestCase { $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->api = $this->createMock(API::class); $this->permission = $this->createMock(Permission::class); + $this->jobList = $this->createMock(IJobList::class); $this->controller = new JoinController( 'bbb', @@ -44,10 +47,12 @@ class JoinControllerTest extends TestCase { $this->urlGenerator, $this->userSession, $this->api, - $this->permission + $this->permission, + $this->jobList ); $this->room = new Room(); + $this->room->id = 1; $this->room->uid = 'uid_foo'; $this->room->userId = 'user_foo'; $this->room->access = Room::ACCESS_PUBLIC; diff --git a/ts/Common/Api.ts b/ts/Common/Api.ts index 61c128a..6a2c278 100644 --- a/ts/Common/Api.ts +++ b/ts/Common/Api.ts @@ -44,6 +44,7 @@ export interface Room { mediaCheck: boolean, cleanLayout: boolean, joinMuted: boolean, + running: boolean, } export interface RoomShare { @@ -132,6 +133,18 @@ class Api { return response.data; } + public async isRunning(uid: string): Promise { + const response = await axios.get(this.getUrl(`server/${uid}/isRunning`)); + + return response.data; + } + + public async insertDocument(uid: string, url: string, filename: string): Promise { + const response = await axios.post(this.getUrl(`server/${uid}/insertDocument`), { url, filename }); + + return response.data; + } + 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; @@ -257,9 +270,9 @@ class Api { groups: [], circles: [], exact: { - users: response.data.ocs.data.exact.users, - groups: response.data.ocs.data.exact.groups, - circles: response.data.ocs.data.exact.circles || [], + users: response.data.ocs.data.exact.users, + groups: response.data.ocs.data.exact.groups, + circles: response.data.ocs.data.exact.circles || [], }, }; } diff --git a/ts/Manager/App.scss b/ts/Manager/App.scss index 14085e0..7b1f295 100644 --- a/ts/Manager/App.scss +++ b/ts/Manager/App.scss @@ -102,6 +102,16 @@ pre { margin-bottom: 1em; } + .button.success { + background-color: var(--color-success); + border-color: var(--color-success-hover); + color: var(--color-primary-text); + + &:hover { + background-color: var(--color-success-hover); + } + } + .icon { display: inline-block; opacity: 0; @@ -184,6 +194,10 @@ pre { width: 42px; padding: 0; } + + &.name { + width: 100%; + } } tfoot td { diff --git a/ts/Manager/App.tsx b/ts/Manager/App.tsx index b7f32cc..2a6dbc4 100644 --- a/ts/Manager/App.tsx +++ b/ts/Manager/App.tsx @@ -43,7 +43,7 @@ const App: React.FC = () => { const [orderBy, setOrderBy] = useState('name'); const [sortOrder, setSortOrder] = useState(SortOrder.ASC); - const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => ); + const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => ); useEffect(() => { Promise.all([ @@ -105,13 +105,18 @@ const App: React.FC = () => { function updateRoom(room: Room) { return api.updateRoom(room).then(updatedRoom => { - setRooms(rooms.map(room => { - if (room.id === updatedRoom.id) { - return updatedRoom; - } - return room; - })); + if (!rooms.find(room => room.id === updatedRoom.id)) { + setRooms(rooms.concat([updatedRoom])); + } else { + setRooms(rooms.map(room => { + if (room.id === updatedRoom.id) { + return updatedRoom; + } + + return room; + })); + } }); } @@ -121,6 +126,25 @@ const App: React.FC = () => { }); } + function cloneRoom(room: Room) { + + if (room.moderatorToken !== null) { + room.moderatorToken = 'true'; + } + + return api.createRoom(room.name, room.access, room.maxParticipants).then(newRoom => { + api.getRoomShares(room.id).then(shares => shares.forEach(share => { + api.createRoomShare(newRoom.id, share.shareType, share.shareWith, share.permission); + })); + + updateRoom({ + ...room, + uid: newRoom.uid, + id: newRoom.id, + }); + }); + } + const maxRooms = restriction?.maxRooms || 0; const quota = maxRooms < 0 ? t('bbb', 'unlimited') : rooms.filter(room => room.userId === OC.currentUser).length + ' / ' + maxRooms; @@ -151,6 +175,7 @@ const App: React.FC = () => { + diff --git a/ts/Manager/RoomRow.tsx b/ts/Manager/RoomRow.tsx index abf2cdc..bc6808a 100644 --- a/ts/Manager/RoomRow.tsx +++ b/ts/Manager/RoomRow.tsx @@ -11,6 +11,7 @@ type Props = { restriction?: Restriction; updateRoom: (room: Room) => Promise; deleteRoom: (id: number) => void; + cloneRoom: (room: Room) => void; } type RecordingsNumberProps = { @@ -175,6 +176,10 @@ const RoomRow: React.FC = (props) => { return ; } + function cloneRow() { + props.cloneRoom({...props.room}); + } + const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, { user: room.userId, size: 24, @@ -187,9 +192,9 @@ const RoomRow: React.FC = (props) => { return ( <> - - - + + + {room.running ? t('bbb', 'Join') : t('bbb', 'Start')} @@ -225,6 +230,11 @@ const RoomRow: React.FC = (props) => { + + +