mirror of https://github.com/sualko/cloud_bbb
Compare commits
6 Commits
119d988b95
...
e25f969e68
Author | SHA1 | Date |
---|---|---|
|
e25f969e68 | |
|
0469641a30 | |
|
862cda79c3 | |
|
dd87506f22 | |
|
b7ad4367e2 | |
|
27aa28da81 |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- add optional avatar cache
|
||||
- add command to clear avatar cache
|
||||
|
||||
### Fixed
|
||||
- use injection for random generator
|
||||
|
||||
### Misc
|
||||
- remove deprecated api calls
|
||||
- enable app for unit tests
|
||||
|
||||
## 2.1.0 (2021-12-08)
|
||||
### Added
|
||||
|
|
38
README.md
38
README.md
|
@ -76,6 +76,44 @@ Key | Description
|
|||
`api.url` | URL to your BBB server. Should start with `https://`
|
||||
`api.secret` | Secret of your BBB server
|
||||
`app.shortener` | Value of your shortener service. Should start with `https://` and contain `{token}`.
|
||||
`avatar.path` | Absolute path to an optional avatar cache directory.
|
||||
`avatar.url` | URL which serves `avatar.path` to be used as avatar cache.
|
||||
|
||||
### Avatar cache (v2.2+)
|
||||
The generation of avatars puts a high load on your Nextcloud instance, since the
|
||||
number of requests increases squarely to the number of participants in a room.
|
||||
To mitigate this situation, this app provides an optional avatar file cache. To
|
||||
activate the cache `avatar.path` and `avatar.url` have to be configured.
|
||||
`avatar.path` must provide an absolute path (e.g. `/srv/bbb-avatar-cache/`) to a
|
||||
directory which is writable by the PHP user. `avatar.url` must contain the url
|
||||
which serves all files from `avatar.path`. To bypass browser connection limits
|
||||
we recommend to setup a dedicated host.
|
||||
|
||||
Example Apache configuration for a dedicated host with `avatar.path = /srv/bbb-avatar-cache/`
|
||||
and `avatar.url = https://avatar-cache.your-nextcloud.com/`:
|
||||
|
||||
```
|
||||
<VirtualHost *:443>
|
||||
ServerName avatar-cache.your-nextcloud.com
|
||||
|
||||
Header always set Strict-Transport-Security "max-age=15768000;"
|
||||
|
||||
DocumentRoot /srv/bbb-avatar-cache
|
||||
<Directory /srv/bbb-avatar-cache>
|
||||
Options -FollowSymLinks -Indexes
|
||||
</Directory>
|
||||
|
||||
SSLEngine On
|
||||
# SSL config...
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
For additional security, we recommend to disable directory listing, symlinks and
|
||||
any language interpreter such as php for the cache directory.
|
||||
|
||||
Cached avatars are usually deleted as soon as the meeting ends. In cases the BBB
|
||||
server shuts down unexpected, we provide the `bbb:clear-avatar-cache` occ
|
||||
command (example use: `./occ bbb:clear-avatar-cache`).
|
||||
|
||||
|
||||
## :bowtie: User guide
|
||||
|
|
|
@ -22,7 +22,7 @@ Developer wanted! If you have time it would be awesome if you could help to enha
|
|||
|
||||
*This app integrates BigBlueButton and is not endorsed or certified by BigBlueButton Inc. BigBlueButton and the BigBlueButton Logo are trademarks of BigBlueButton Inc.*
|
||||
]]></description>
|
||||
<version>2.1.0</version>
|
||||
<version>2.2.0-beta.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author mail="klaus@jsxc.org">Klaus Herberth</author>
|
||||
<namespace>BigBlueButton</namespace>
|
||||
|
@ -45,6 +45,9 @@ Developer wanted! If you have time it would be awesome if you could help to enha
|
|||
<lib>SimpleXML</lib>
|
||||
<nextcloud min-version="20" max-version="23"/>
|
||||
</dependencies>
|
||||
<commands>
|
||||
<command>OCA\BigBlueButton\Command\ClearAvatarCache</command>
|
||||
</commands>
|
||||
<settings>
|
||||
<admin>OCA\BigBlueButton\Settings\Admin</admin>
|
||||
<personal-section>OCA\BigBlueButton\Settings\Section</personal-section>
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton;
|
||||
|
||||
use OCA\BigBlueButton\AppInfo\Application;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCP\IAvatarManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Security\ISecureRandom;
|
||||
|
||||
class AvatarRepository {
|
||||
public const CONF_KEY_PATH = 'avatar.path';
|
||||
public const CONF_KEY_URL = 'avatar.url';
|
||||
|
||||
/** @var IAvatarManager */
|
||||
private $avatarManager;
|
||||
|
||||
/** @var ISecureRandom */
|
||||
private $random;
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(
|
||||
IAvatarManager $avatarManager,
|
||||
IURLGenerator $urlGenerator,
|
||||
ISecureRandom $random,
|
||||
IConfig $config) {
|
||||
$this->avatarManager = $avatarManager;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->random = $random;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getAvatarUrl(Room $room, string $userId): string {
|
||||
if (!$this->isAvatarCacheConfigured()) {
|
||||
return $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $userId, 'size' => 32]);
|
||||
}
|
||||
|
||||
$roomDirName = $room->uid . '-' . $this->generateUrlSafeRandom(16);
|
||||
$roomDirPath = $this->getRootPath() . $roomDirName . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (!file_exists($roomDirPath)) {
|
||||
if (!mkdir($roomDirPath, 0755, true)) {
|
||||
throw new \RuntimeException('Can not create room directory: ' . $roomDirPath);
|
||||
}
|
||||
|
||||
file_put_contents($roomDirPath . 'index.html', '');
|
||||
|
||||
if (!file_exists($this->getRootPath() . 'index.html')) {
|
||||
file_put_contents($this->getRootPath() . 'index.html', 'Avatar cache is working.');
|
||||
}
|
||||
}
|
||||
|
||||
$avatar = $this->avatarManager->getAvatar($userId);
|
||||
$file = $avatar->getFile(32);
|
||||
|
||||
$avatarFileName = $this->generateUrlSafeRandom(16) . '-' . $file->getName();
|
||||
$avatarPath = $roomDirPath . $avatarFileName;
|
||||
|
||||
if (!file_put_contents($avatarPath, $file->getContent())) {
|
||||
throw new \RuntimeException('Could not write avatar file: ' . $avatarPath);
|
||||
}
|
||||
|
||||
chmod($avatarPath, 0644);
|
||||
|
||||
return $this->getBaseUrl() . $roomDirName . '/' . $avatarFileName;
|
||||
}
|
||||
|
||||
public function clearRoom(string $roomUid): int {
|
||||
if (!$this->isAvatarCacheConfigured() || empty($roomUid)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$fileCounter = 0;
|
||||
|
||||
foreach (glob($this->getRootPath() . $roomUid . '-*' . DIRECTORY_SEPARATOR) as $dir) {
|
||||
foreach (scandir($dir) as $file) {
|
||||
if (in_array($file, ['.', '..'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($dir . $file);
|
||||
|
||||
if ($file !== 'index.html') {
|
||||
$fileCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
return $fileCounter;
|
||||
}
|
||||
|
||||
public function clearAllRooms(): array {
|
||||
if (!$this->isAvatarCacheConfigured()) {
|
||||
return [
|
||||
'rooms' => 0,
|
||||
'files' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$path = $this->getRootPath();
|
||||
$roomCounter = 0;
|
||||
$fileCounter = 0;
|
||||
|
||||
foreach (scandir($path) as $dir) {
|
||||
if (in_array($dir, ['.', '..']) || $dir === 'index.html') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$roomUid = \explode("-", $dir)[0];
|
||||
|
||||
$fileCounter += $this->clearRoom($roomUid);
|
||||
|
||||
$roomCounter++;
|
||||
}
|
||||
|
||||
return [
|
||||
'rooms' => $roomCounter,
|
||||
'files' => $fileCounter,
|
||||
];
|
||||
}
|
||||
|
||||
private function generateUrlSafeRandom(int $length): string {
|
||||
// from Nextcloud 23 ISecureRandom::CHAR_ALPHANUMERIC can be used as shortcut
|
||||
return $this->random->generate($length, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
|
||||
}
|
||||
|
||||
private function isAvatarCacheConfigured(): bool {
|
||||
return !empty($this->getRootPath()) && !empty($this->getBaseUrl());
|
||||
}
|
||||
|
||||
private function getRootPath(): string {
|
||||
$path = $this->config->getAppValue(Application::ID, self::CONF_KEY_PATH);
|
||||
|
||||
if (empty($path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr($path, -\strlen($path)) === DIRECTORY_SEPARATOR ? $path : ($path . DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
private function getBaseUrl(): string {
|
||||
$url = $this->config->getAppValue(Application::ID, self::CONF_KEY_URL);
|
||||
|
||||
if (empty($url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//', $url) === 0) {
|
||||
$url = $this->urlGenerator->getAbsoluteURL($url);
|
||||
}
|
||||
|
||||
if (preg_match('/\/$/', $url) === 0) {
|
||||
$url .= '/';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ use BigBlueButton\Parameters\GetRecordingsParameters;
|
|||
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||
use OCA\BigBlueButton\AppInfo\Application;
|
||||
use OCA\BigBlueButton\AvatarRepository;
|
||||
use OCA\BigBlueButton\Crypto;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCA\BigBlueButton\Event\MeetingStartedEvent;
|
||||
|
@ -50,6 +51,9 @@ class API {
|
|||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
|
||||
/** @var AvatarRepository */
|
||||
private $avatarRepository;
|
||||
|
||||
/** @var IRequest */
|
||||
private $request;
|
||||
|
||||
|
@ -62,6 +66,7 @@ class API {
|
|||
UrlHelper $urlHelper,
|
||||
Defaults $defaults,
|
||||
IAppManager $appManager,
|
||||
AvatarRepository $avatarRepository,
|
||||
IRequest $request
|
||||
) {
|
||||
$this->config = $config;
|
||||
|
@ -72,6 +77,7 @@ class API {
|
|||
$this->urlHelper = $urlHelper;
|
||||
$this->defaults = $defaults;
|
||||
$this->appManager = $appManager;
|
||||
$this->avatarRepository = $avatarRepository;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
|
@ -97,7 +103,7 @@ class API {
|
|||
$joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password);
|
||||
|
||||
// ensure that float is not converted to a string in scientific notation
|
||||
$joinMeetingParams->setCreationTime(sprintf("%.0f", $creationTime));
|
||||
$joinMeetingParams->setCreateTime(sprintf("%.0f", $creationTime));
|
||||
$joinMeetingParams->setJoinViaHtml5(true);
|
||||
$joinMeetingParams->setRedirect(true);
|
||||
$joinMeetingParams->setGuest($uid === null);
|
||||
|
@ -121,8 +127,10 @@ class API {
|
|||
}
|
||||
|
||||
if ($uid) {
|
||||
$joinMeetingParams->setUserId($uid);
|
||||
$joinMeetingParams->setAvatarURL($this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $uid, 'size' => 32]));
|
||||
$avatarUrl = $this->avatarRepository->getAvatarUrl($room, $uid);
|
||||
|
||||
$joinMeetingParams->setUserID($uid);
|
||||
$joinMeetingParams->setAvatarURL($avatarUrl);
|
||||
}
|
||||
|
||||
return $this->getServer()->getJoinMeetingURL($joinMeetingParams);
|
||||
|
@ -156,11 +164,11 @@ class API {
|
|||
|
||||
private function buildMeetingParams(Room $room, Presentation $presentation = null): CreateMeetingParameters {
|
||||
$createMeetingParams = new CreateMeetingParameters($room->uid, $room->name);
|
||||
$createMeetingParams->setAttendeePassword($room->attendeePassword);
|
||||
$createMeetingParams->setModeratorPassword($room->moderatorPassword);
|
||||
$createMeetingParams->setAttendeePW($room->attendeePassword);
|
||||
$createMeetingParams->setModeratorPW($room->moderatorPassword);
|
||||
$createMeetingParams->setRecord($room->record);
|
||||
$createMeetingParams->setAllowStartStopRecording($room->record);
|
||||
$createMeetingParams->setLogoutUrl($this->urlGenerator->getBaseUrl());
|
||||
$createMeetingParams->setLogoutURL($this->urlGenerator->getBaseUrl());
|
||||
$createMeetingParams->setMuteOnStart($room->getJoinMuted());
|
||||
|
||||
$createMeetingParams->addMeta('bbb-origin-version', $this->appManager->getAppVersion(Application::ID));
|
||||
|
@ -179,7 +187,7 @@ class API {
|
|||
$createMeetingParams->setModeratorOnlyMessage($this->l10n->t('To invite someone to the meeting, send them this link: %s', [$invitationUrl]));
|
||||
|
||||
if (!empty($room->welcome)) {
|
||||
$createMeetingParams->setWelcomeMessage($room->welcome);
|
||||
$createMeetingParams->setWelcome($room->welcome);
|
||||
}
|
||||
|
||||
if ($room->maxParticipants > 0) {
|
||||
|
@ -200,7 +208,7 @@ class API {
|
|||
|
||||
public function getRecording(string $recordId) {
|
||||
$recordingParams = new GetRecordingsParameters();
|
||||
$recordingParams->setRecordId($recordId);
|
||||
$recordingParams->setRecordID($recordId);
|
||||
$recordingParams->setState('any');
|
||||
|
||||
$response = $this->getServer()->getRecordings($recordingParams);
|
||||
|
@ -220,7 +228,7 @@ class API {
|
|||
|
||||
public function getRecordings(Room $room): array {
|
||||
$recordingParams = new GetRecordingsParameters();
|
||||
$recordingParams->setMeetingId($room->uid);
|
||||
$recordingParams->setMeetingID($room->uid);
|
||||
$recordingParams->setState('processing,processed,published,unpublished');
|
||||
|
||||
$response = $this->getServer()->getRecordings($recordingParams);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Command;
|
||||
|
||||
use OCA\BigBlueButton\AvatarRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ClearAvatarCache extends Command {
|
||||
|
||||
/**
|
||||
* @var AvatarRepository
|
||||
*/
|
||||
private $avatarRepository;
|
||||
|
||||
public function __construct(
|
||||
AvatarRepository $avatarRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->avatarRepository = $avatarRepository;
|
||||
}
|
||||
|
||||
protected function configure() {
|
||||
$this->setName('bbb:clear-avatar-cache');
|
||||
$this->setDescription('Clear all avatars in cache');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$stats = $this->avatarRepository->clearAllRooms();
|
||||
|
||||
$output->writeln("Removed " . $stats["files"] . " avatars in " . $stats["rooms"] . " rooms");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace OCA\BigBlueButton\Controller;
|
||||
|
||||
use OCA\BigBlueButton\AvatarRepository;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCA\BigBlueButton\Event\MeetingEndedEvent;
|
||||
use OCA\BigBlueButton\Event\RecordingReadyEvent;
|
||||
|
@ -20,6 +21,9 @@ class HookController extends Controller {
|
|||
/** @var RoomService */
|
||||
private $service;
|
||||
|
||||
/** @var AvatarRepository */
|
||||
private $avatarRepository;
|
||||
|
||||
/** @var IEventDispatcher */
|
||||
private $eventDispatcher;
|
||||
|
||||
|
@ -27,11 +31,13 @@ class HookController extends Controller {
|
|||
string $appName,
|
||||
IRequest $request,
|
||||
RoomService $service,
|
||||
AvatarRepository $avatarRepository,
|
||||
IEventDispatcher $eventDispatcher
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->service = $service;
|
||||
$this->avatarRepository = $avatarRepository;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
|
@ -55,8 +61,11 @@ class HookController extends Controller {
|
|||
*/
|
||||
public function meetingEnded($recordingmarks = false): void {
|
||||
$recordingmarks = \boolval($recordingmarks);
|
||||
$room = $this->getRoom();
|
||||
|
||||
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($this->getRoom(), $recordingmarks));
|
||||
$this->avatarRepository->clearRoom($room->uid);
|
||||
|
||||
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ use OCP\AppFramework\Db\DoesNotExistException;
|
|||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\ISecureRandom;
|
||||
|
||||
class RoomService {
|
||||
|
||||
|
@ -25,13 +26,18 @@ class RoomService {
|
|||
/** @var IEventDispatcher */
|
||||
private $eventDispatcher;
|
||||
|
||||
/** @var ISecureRandom */
|
||||
private $random;
|
||||
|
||||
public function __construct(
|
||||
RoomMapper $mapper,
|
||||
IConfig $config,
|
||||
IEventDispatcher $eventDispatcher) {
|
||||
IEventDispatcher $eventDispatcher,
|
||||
ISecureRandom $random) {
|
||||
$this->mapper = $mapper;
|
||||
$this->config = $config;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->random = $random;
|
||||
}
|
||||
|
||||
public function findAll(string $userId, array $groupIds, array $circleIds): array {
|
||||
|
@ -84,12 +90,12 @@ class RoomService {
|
|||
|
||||
$mediaCheck = $this->config->getAppValue('bbb', 'join.mediaCheck', 'true') === 'true';
|
||||
|
||||
$room->setUid(\OC::$server->getSecureRandom()->generate(16, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setUid($this->humanReadableRandom(16));
|
||||
$room->setName($name);
|
||||
$room->setWelcome($welcome);
|
||||
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||
$room->setAttendeePassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setModeratorPassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setAttendeePassword($this->humanReadableRandom(32));
|
||||
$room->setModeratorPassword($this->humanReadableRandom(32));
|
||||
$room->setRecord($record);
|
||||
$room->setAccess($access);
|
||||
$room->setUserId($userId);
|
||||
|
@ -178,6 +184,6 @@ class RoomService {
|
|||
* @param int $length
|
||||
*/
|
||||
private function humanReadableRandom(int $length) {
|
||||
return \OC::$server->getSecureRandom()->generate($length, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE);
|
||||
return $this->random->generate($length, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@sualko/cloud_bbb",
|
||||
"description": "Nextcloud Integration for BigBlueButton",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0-beta.1",
|
||||
"author": "Klaus Herberth <klaus@jsxc.org>",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sualko/cloud_bbb/issues"
|
||||
|
|
Loading…
Reference in New Issue