feat: add support for URL shortener

fix #57
pull/83/head
sualko 2020-09-23 09:13:26 +02:00
parent a72c877f6b
commit 5debcb6d2a
11 changed files with 185 additions and 18 deletions

View File

@ -14,6 +14,7 @@ use OCA\BigBlueButton\Event\MeetingStartedEvent;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Permission; use OCA\BigBlueButton\Permission;
use OCA\BigBlueButton\Crypto; use OCA\BigBlueButton\Crypto;
use OCA\BigBlueButton\UrlHelper;
use OCP\IConfig; use OCP\IConfig;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IL10N; use OCP\IL10N;
@ -40,13 +41,17 @@ class API {
/** @var IL10N */ /** @var IL10N */
private $l10n; private $l10n;
/** @var UrlHelper */
private $urlHelper;
public function __construct( public function __construct(
IConfig $config, IConfig $config,
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
Permission $permission, Permission $permission,
Crypto $crypto, Crypto $crypto,
IEventDispatcher $eventDispatcher, IEventDispatcher $eventDispatcher,
IL10N $l10n IL10N $l10n,
UrlHelper $urlHelper
) { ) {
$this->config = $config; $this->config = $config;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
@ -54,6 +59,7 @@ class API {
$this->crypto = $crypto; $this->crypto = $crypto;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->urlHelper = $urlHelper;
} }
private function getServer() { private function getServer() {
@ -132,7 +138,7 @@ class API {
$recordingReadyUrl = $this->urlGenerator->linkToRouteAbsolute('bbb.hook.recordingReady', ['token' => $room->uid, 'mac' => $mac]); $recordingReadyUrl = $this->urlGenerator->linkToRouteAbsolute('bbb.hook.recordingReady', ['token' => $room->uid, 'mac' => $mac]);
$createMeetingParams->setRecordingReadyCallbackUrl($recordingReadyUrl); $createMeetingParams->setRecordingReadyCallbackUrl($recordingReadyUrl);
$invitationUrl = $this->urlGenerator->linkToRouteAbsolute('bbb.join.index', ['token' => $room->uid]); $invitationUrl = $this->urlHelper->linkToInvitationAbsolute($room);
$createMeetingParams->setModeratorOnlyMessage($this->l10n->t('To invite someone to the meeting, send them this link: %s', [$invitationUrl])); $createMeetingParams->setModeratorOnlyMessage($this->l10n->t('To invite someone to the meeting, send them this link: %s', [$invitationUrl]));
if (!empty($room->welcome)) { if (!empty($room->welcome)) {

View File

@ -5,10 +5,16 @@ namespace OCA\BigBlueButton\Controller;
use OCP\IRequest; use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\IConfig;
class PageController extends Controller { class PageController extends Controller {
public function __construct(string $appName, IRequest $request) { /** @var IConfig */
private $config;
public function __construct(string $appName, IRequest $request, IConfig $config) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->config = $config;
} }
/** /**
@ -16,6 +22,8 @@ class PageController extends Controller {
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function index() { public function index() {
return new TemplateResponse($this->appName, 'manager'); return new TemplateResponse($this->appName, 'manager', [
'shortener' => $this->config->getAppValue('bbb', 'app.shortener', ''),
]);
} }
} }

View File

@ -28,6 +28,7 @@ class Admin implements ISettings {
'api.url' => $this->config->getAppValue('bbb', 'api.url'), 'api.url' => $this->config->getAppValue('bbb', 'api.url'),
'api.secret' => $this->config->getAppValue('bbb', 'api.secret'), 'api.secret' => $this->config->getAppValue('bbb', 'api.secret'),
'app.navigation' => $this->config->getAppValue('bbb', 'app.navigation') === 'true' ? 'checked' : '', 'app.navigation' => $this->config->getAppValue('bbb', 'app.navigation') === 'true' ? 'checked' : '',
'app.shortener' => $this->config->getAppValue('bbb', 'app.shortener'),
]; ];
return new TemplateResponse('bbb', 'admin', $parameters); return new TemplateResponse('bbb', 'admin', $parameters);

View File

@ -34,7 +34,10 @@ class Personal implements ISettings {
$warning = $this->l->t('API URL or secret not configured. Please contact your administrator.'); $warning = $this->l->t('API URL or secret not configured. Please contact your administrator.');
} }
return new TemplateResponse('bbb', 'manager', ['warning' => $warning]); return new TemplateResponse('bbb', 'manager', [
'warning' => $warning,
'shortener' => $this->config->getAppValue('bbb', 'app.shortener', ''),
]);
} }
/** /**

44
lib/UrlHelper.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace OCA\BigBlueButton;
use OCA\BigBlueButton\Db\Room;
use OCP\IConfig;
use OCP\IURLGenerator;
class UrlHelper {
/** @var IConfig */
private $config;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(
IConfig $config,
IURLGenerator $urlGenerator
) {
$this->config = $config;
$this->urlGenerator = $urlGenerator;
}
public function linkToInvitationAbsolute(Room $room): string {
$url = $this->config->getAppValue('bbb', 'app.shortener', '');
if (empty($url) || strpos($url, 'https://') !== 0 || strpos($url, '{token}') === false) {
return $this->urlGenerator->linkToRouteAbsolute('bbb.join.index', ['token' => $room->getUid()]);
}
$placeholders = [];
$replacements = [
'token' => $room->getUid(),
'user' => $room->getUserId(),
];
foreach ($replacements as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
}
return str_replace($placeholders, $replacements, $url);
}
}

View File

@ -11,12 +11,12 @@ script('bbb', 'restrictions');
<p><?php p($l->t('Get your API URL and secret by executing "sudo bbb-conf --secret" on your BigBlueButton server.')); ?></p> <p><?php p($l->t('Get your API URL and secret by executing "sudo bbb-conf --secret" on your BigBlueButton server.')); ?></p>
<form> <form id="bbb-api">
<input type="url" name="api.url" value="<?php p($_['api.url']); ?>" placeholder="<?php p($l->t('API URL')); ?>" pattern="https://.*" required /> <input type="url" name="api.url" value="<?php p($_['api.url']); ?>" placeholder="<?php p($l->t('API URL')); ?>" pattern="https://.*" required />
<input type="password" name="api.secret" value="<?php p($_['api.secret']); ?>" placeholder="<?php p($l->t('API secret')); ?>" autocomplete="new-password" required /> <input type="password" name="api.secret" value="<?php p($_['api.secret']); ?>" placeholder="<?php p($l->t('API secret')); ?>" autocomplete="new-password" required />
<input type="submit" value="<?php p($l->t('Save')); ?>" /> <input type="submit" value="<?php p($l->t('Save')); ?>" />
<div id="bbb-result"></div> <div class="bbb-result"></div>
</form> </form>
<p> <p>
@ -24,6 +24,18 @@ script('bbb', 'restrictions');
<label for="bbb-app-navigation"><?php p($l->t('Show room manager in app navigation instead of settings page.')); ?></label> <label for="bbb-app-navigation"><?php p($l->t('Show room manager in app navigation instead of settings page.')); ?></label>
</p> </p>
<h3>URL Shortener</h3>
<p><?php p($l->t('If you like to use shorter urls, you can enter a forwarding proxy below.')); ?></p>
<form id="bbb-shortener">
<input type="url" name="app.shortener" value="<?php p($_['app.shortener']); ?>" placeholder="<?php p($l->t('URL shortener')); ?>" pattern="https://.*" />
<input type="submit" value="<?php p($l->t('Save')); ?>" />
<div id="bbb-shortener-example"></div>
<div class="bbb-result"></div>
</form>
<h3>Restrictions</h3> <h3>Restrictions</h3>
<div id="bbb-restrictions"> <div id="bbb-restrictions">
</div> </div>

View File

@ -2,7 +2,7 @@
script('bbb', 'manager'); script('bbb', 'manager');
?> ?>
<div id="bbb-root"></div> <div id="bbb-root" data-shortener="<?php p($_[shortener]); ?>"></div>
<?php if (!empty($_['warning'])): ?> <?php if (!empty($_['warning'])): ?>
<div id="bbb-warning"> <div id="bbb-warning">

View File

@ -122,6 +122,12 @@ class Api {
} }
public getRoomUrl(room: Room) { public getRoomUrl(room: Room) {
const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || '';
if (shortener) {
return shortener.replace(/\{user\}/g, room.userId).replace(/\{token\}/g, room.uid);
}
return window.location.origin + api.getUrl(`b/${room.uid}`); return window.location.origin + api.getUrl(`b/${room.uid}`);
} }

View File

@ -27,6 +27,14 @@
width: 100%; width: 100%;
} }
pre {
font-weight: normal;
background-color: #f1f1f1;
padding: 1em 2em;
display: inline-block;
font-family: monospace;
}
#bbb-warning { #bbb-warning {
padding: 1em; padding: 1em;
background-color: rgb(255, 255, 123); background-color: rgb(255, 255, 123);

View File

@ -187,10 +187,10 @@ const RoomRow: React.FC<Props> = (props) => {
<> <>
<tr className={showRecordings ? 'selected-row' : ''}> <tr className={showRecordings ? 'selected-row' : ''}>
<td className="start icon-col"> <td className="start icon-col">
<a href={api.getUrl(`b/${room.uid}`)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a> <a href={api.getRoomUrl(room)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a>
</td> </td>
<td className="share icon-col"> <td className="share icon-col">
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}> <CopyToClipboard text={api.getRoomUrl(room)}>
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span> <span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
</CopyToClipboard> </CopyToClipboard>
</td> </td>

View File

@ -34,7 +34,7 @@ $(() => {
}); });
} }
async function saveSettings(url: string, secret: string) { async function saveApiSettings(url: string, secret: string) {
url += url.endsWith('/') ? '' : '/'; url += url.endsWith('/') ? '' : '/';
await checkServer(url, secret); await checkServer(url, secret);
@ -44,19 +44,19 @@ $(() => {
OCP.AppConfig.setValue('bbb', 'api.secret', secret); OCP.AppConfig.setValue('bbb', 'api.secret', secret);
} }
$('#bbb-settings form').submit(function (ev) { $('#bbb-api').on('submit', function (ev) {
ev.preventDefault(); ev.preventDefault();
$('#bbb-result').empty(); const resultElement = $(this).find('.bbb-result').empty();
saveSettings(this['api.url'].value, this['api.secret'].value).then(() => { saveApiSettings(this['api.url'].value, this['api.secret'].value).then(() => {
const successElement = generateSuccessElement(t('bbb', 'Settings saved')); const successElement = generateSuccessElement(t('bbb', 'Settings saved'));
setTimeout(() => { setTimeout(() => {
$('#bbb-result').empty(); resultElement.empty();
}, 3000); }, 3000);
$('#bbb-result').append(successElement); resultElement.append(successElement);
}).catch(err => { }).catch(err => {
let message = t('bbb', 'Unexpected error occurred'); let message = t('bbb', 'Unexpected error occurred');
@ -68,11 +68,90 @@ $(() => {
const warningElement = generateWarningElement(message); const warningElement = generateWarningElement(message);
$('#bbb-result').append(warningElement); resultElement.append(warningElement);
}); });
}); });
$<HTMLInputElement>('#bbb-settings [name="app.navigation"]').change((ev) => { function generateExampleShortener(shortener: string) {
return shortener.replace(/</g, '&lt;').replace(/\{user\}/g, `<strong>${OC.currentUser}</strong>`).replace(/\{token\}/g, '<strong>your_room_id</strong>');
}
async function saveAppSettings(shortener: string) {
await checkPasswordConfirmation();
if (shortener.indexOf('https://') !== 0) {
throw 'https';
}
if (shortener.indexOf('{token}') < 0) {
throw 'token';
}
OCP.AppConfig.setValue('bbb', 'app.shortener', shortener);
}
$('#bbb-shortener').on('submit', function (ev) {
ev.preventDefault();
const resultElement = $(this).find('.bbb-result').empty();
saveAppSettings(this['app.shortener'].value).then(() => {
const successElement = generateSuccessElement(t('bbb', 'Settings saved'));
setTimeout(() => {
resultElement.empty();
}, 3000);
resultElement.append(successElement);
}).catch(err => {
let message = t('bbb', 'Unexpected error occurred');
if (err === 'https') {
message = t('bbb', 'URL has to start with https');
} else if (err === 'token') {
message = t('bbb', 'URL has to contain the {token} placeholder');
}
const warningElement = generateWarningElement(message);
console.warn('Could not save app settings', err);
resultElement.append(warningElement);
});
});
$<HTMLInputElement>('#bbb-shortener [name="app.shortener"]').on('keyup', (ev) => {
ev.preventDefault();
const {value} = ev.target;
if (!value || value.indexOf('https://') !== 0 || value.indexOf('{token}') < 0) {
$('#bbb-shortener-example').text(t('bbb', 'URL has to start with https:// and contain {token}. Additionally the {user} placeholder can be used.'));
return;
}
const target = window.location.origin + OC.generateUrl('apps/bbb/b/$1');
const url = (new URL(value));
const rewritePath = '^' + url.pathname.replace(/^\//, '').replace(/%7Buser%7D/g, '.+').replace(/%7Btoken%7D/g, '(.+)');
$('#bbb-shortener-example').html(`<p>${generateExampleShortener(value)}</p>
<details>
<summary>${t('bbb', 'Example configuration for Apache and Nginx')}</summary>
<pre>#Apache with mod_rewrite
ServerName ${url.hostname}
RewriteEngine on
RewriteRule "${rewritePath}" "${target}" [R=307,L]
#Nginx config
server_name ${url.hostname};
rewrite ${rewritePath} ${target} last;
return 307;</pre></details>
`);
});
$('#bbb-shortener [name="app.shortener"]').trigger('keyup');
$<HTMLInputElement>('#bbb-settings [name="app.navigation"]').on('change', (ev) => {
ev.preventDefault(); ev.preventDefault();
console.log('checkbox changed to', ev.target.checked); console.log('checkbox changed to', ev.target.checked);