pull/424/merge
Linus Groschke 2026-04-27 17:54:42 +02:00 committed by GitHub
commit 93a2fd70cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1195 additions and 1709 deletions

View File

@ -9,7 +9,7 @@
} }
], ],
"require": { "require": {
"littleredbutton/bigbluebutton-api-php": "^4.0" "littleredbutton/bigbluebutton-api-php": "^6.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5 || ^9.3", "phpunit/phpunit": "^8.5 || ^9.3",

View File

@ -4,6 +4,7 @@ namespace OCA\BigBlueButton\BigBlueButton;
use BigBlueButton\BigBlueButton; use BigBlueButton\BigBlueButton;
use BigBlueButton\Core\Record; use BigBlueButton\Core\Record;
use BigBlueButton\Enum\Role;
use BigBlueButton\Parameters\CreateMeetingParameters; use BigBlueButton\Parameters\CreateMeetingParameters;
use BigBlueButton\Parameters\DeleteRecordingsParameters; use BigBlueButton\Parameters\DeleteRecordingsParameters;
use BigBlueButton\Parameters\GetRecordingsParameters; use BigBlueButton\Parameters\GetRecordingsParameters;
@ -62,13 +63,10 @@ class API {
* @return string join url * @return string join url
*/ */
public function createJoinUrl(Room $room, float $creationTime, string $displayname, bool $isModerator, ?string $uid = null) { 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, $isModerator ? Role::MODERATOR : Role::VIEWER);
$joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password);
// ensure that float is not converted to a string in scientific notation // ensure that float is not converted to a string in scientific notation
$joinMeetingParams->setCreateTime(sprintf("%.0f", $creationTime)); $joinMeetingParams->setCreateTime(intval(sprintf("%.0f", $creationTime)));
$joinMeetingParams->setJoinViaHtml5(true);
$joinMeetingParams->setRedirect(true); $joinMeetingParams->setRedirect(true);
// set the guest parameter for everyone but moderators to send all users to the waiting room if setting is selected // set the guest parameter for everyone but moderators to send all users to the waiting room if setting is selected
@ -130,8 +128,6 @@ class API {
private function buildMeetingParams(Room $room, ?Presentation $presentation = null): CreateMeetingParameters { private function buildMeetingParams(Room $room, ?Presentation $presentation = null): CreateMeetingParameters {
$createMeetingParams = new CreateMeetingParameters($room->uid, $room->name); $createMeetingParams = new CreateMeetingParameters($room->uid, $room->name);
$createMeetingParams->setAttendeePW($room->attendeePassword);
$createMeetingParams->setModeratorPW($room->moderatorPassword);
$createMeetingParams->setRecord($room->record); $createMeetingParams->setRecord($room->record);
$createMeetingParams->setAllowStartStopRecording($room->record); $createMeetingParams->setAllowStartStopRecording($room->record);
$createMeetingParams->setLogoutURL($this->urlGenerator->getBaseUrl()); $createMeetingParams->setLogoutURL($this->urlGenerator->getBaseUrl());
@ -239,6 +235,16 @@ class API {
* @psalm-return array{id: string, meetingId: string, name: string, published: bool, state: string, startTime: string, participants: int, type: string, length: string, url: string, metas: array} * @psalm-return array{id: string, meetingId: string, name: string, published: bool, state: string, startTime: string, participants: int, type: string, length: string, url: string, metas: array}
*/ */
private function recordToArray(Record $record): array { private function recordToArray(Record $record): array {
$formats = [];
foreach ($record->getPlaybackFormats() as $format) {
$formats[] = [
'type' => $format->getType(),
'length' => $format->getLength(),
'url' => $format->getUrl()
];
}
return [ return [
'id' => $record->getRecordId(), 'id' => $record->getRecordId(),
'meetingId' => $record->getMeetingId(), 'meetingId' => $record->getMeetingId(),
@ -247,10 +253,8 @@ class API {
'state' => $record->getState(), 'state' => $record->getState(),
'startTime' => $record->getStartTime(), 'startTime' => $record->getStartTime(),
'participants' => $record->getParticipantCount(), 'participants' => $record->getParticipantCount(),
'type' => $record->getPlaybackType(), 'formats' => $formats,
'length' => $record->getPlaybackLength(), 'metas' => $record->getMetas()
'url' => $record->getPlaybackUrl(),
'metas' => $record->getMetas(),
]; ];
} }

2837
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -65,12 +65,16 @@ export type Recording = {
state: 'processing' | 'processed' | 'published' | 'unpublished' | 'deleted'; state: 'processing' | 'processed' | 'published' | 'unpublished' | 'deleted';
startTime: number; startTime: number;
participants: number; participants: number;
type: string; formats: RecordingFormat[];
length: number;
url: string;
meta: any; meta: any;
} }
export type RecordingFormat = {
type: string
length: number
url: string
}
export interface ShareWithOption { export interface ShareWithOption {
label: string; label: string;
value: { value: {
@ -210,12 +214,12 @@ class Api {
return response.data; return response.data;
} }
public async storeRecording(recording: Recording, path: string) { public async storeRecording(recording: Recording, format: RecordingFormat, path: string) {
const startDate = new Date(recording.startTime); const startDate = new Date(recording.startTime);
const filename = `${encodeURIComponent(recording.name + ' ' + startDate.toISOString().replace(/:/g, '-').substr(0,19))}.url`; const filename = `${encodeURIComponent(recording.name + ' ' + startDate.toISOString().replace(/:/g, '-').substr(0,19))}.url`;
const url = OC.linkToRemote(`dav/files/${OC.currentUser}${path}/${filename}`); const url = OC.linkToRemote(`dav/files/${OC.currentUser}${path}/${filename}`);
await axios.put(url, `[InternetShortcut]\nURL=${recording.url}`); await axios.put(url, `[InternetShortcut]\nURL=${format.url}`);
return filename; return filename;
} }

View File

@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Recording } from '../Common/Api'; import {Recording, RecordingFormat} from '../Common/Api';
type Props = { type Props = {
recording: Recording; recording: Recording;
format: RecordingFormat;
isAdmin : boolean; isAdmin : boolean;
deleteRecording: (recording: Recording) => void; deleteRecording: (recording: Recording) => void;
storeRecording: (recording: Recording) => void; storeRecording: (recording: Recording, format: RecordingFormat) => void;
publishRecording: (recording: Recording, publish: boolean) => void; publishRecording: (recording: Recording, publish: boolean) => void;
} }
const RecordingRow = ({recording, isAdmin, deleteRecording, storeRecording, publishRecording}: Props): JSX.Element => { const RecordingRow = ({recording, format, isAdmin, deleteRecording, storeRecording, publishRecording}: Props): JSX.Element => {
function checkPublished(recording: Recording, onChange: (value: boolean) => void) { function checkPublished(recording: Recording, onChange: (value: boolean) => void) {
return ( return (
@ -28,19 +29,19 @@ const RecordingRow = ({recording, isAdmin, deleteRecording, storeRecording, publ
return ( return (
<tr key={recording.id}> <tr key={recording.id}>
<td className="start icon-col"> <td className="start icon-col">
<a href={recording.url} className="action-item" target="_blank" rel="noopener noreferrer" title={t('bbb', 'Open recording')}> <a href={format.url} className="action-item" target="_blank" rel="noopener noreferrer" title={t('bbb', 'Open recording')}>
<span className="icon icon-external icon-visible"></span> <span className="icon icon-external icon-visible"></span>
</a> </a>
</td> </td>
<td className="share icon-col"> <td className="share icon-col">
<CopyToClipboard text={recording.url} options={{format:'text/plain'}}> <CopyToClipboard text={format.url} options={{format:'text/plain'}}>
<button className="action-item copy-to-clipboard" title={t('bbb', 'Copy to clipboard')}> <button className="action-item copy-to-clipboard" title={t('bbb', 'Copy to clipboard')}>
<span className="icon icon-clippy icon-visible" ></span> <span className="icon icon-clippy icon-visible" ></span>
</button> </button>
</CopyToClipboard> </CopyToClipboard>
</td> </td>
<td className="icon-col"> <td className="icon-col">
<button className="action-item" onClick={() => storeRecording(recording)} title={t('bbb', 'Save as file')}> <button className="action-item" onClick={() => storeRecording(recording, format)} title={t('bbb', 'Save as file')}>
<span className="icon icon-add-shortcut icon-visible"></span> <span className="icon icon-add-shortcut icon-visible"></span>
</button> </button>
</td> </td>
@ -48,13 +49,13 @@ const RecordingRow = ({recording, isAdmin, deleteRecording, storeRecording, publ
{(new Date(recording.startTime)).toLocaleString()} {(new Date(recording.startTime)).toLocaleString()}
</td> </td>
<td> <td>
{recording.length === 0 ? '< 1 min' : (recording.length + ' min')} {format.length === 0 ? '< 1 min' : (format.length + ' min')}
</td> </td>
<td> <td>
{n('bbb', '%n participant', '%n participants', recording.participants)} {n('bbb', '%n participant', '%n participants', recording.participants)}
</td> </td>
<td> <td>
{recording.type} {format.type}
</td> </td>
<td> <td>
{isAdmin && checkPublished(recording, (checked) => { {isAdmin && checkPublished(recording, (checked) => {

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { api, Recording, Room, Restriction, Access, Permission } from '../Common/Api'; import {api, Recording, Room, Restriction, Access, Permission, RecordingFormat} from '../Common/Api';
import EditRoom from './EditRoom'; import EditRoom from './EditRoom';
import RecordingRow from './RecordingRow'; import RecordingRow from './RecordingRow';
import EditableValue from './EditableValue'; import EditableValue from './EditableValue';
@ -98,9 +98,9 @@ const RoomRow = (props: Props): JSX.Element => {
}, undefined, 'httpd/unix-directory'); }, undefined, 'httpd/unix-directory');
} }
function storeRecording(recording: Recording) { function storeRecording(recording: Recording, format: RecordingFormat) {
OC.dialogs.filepicker(t('bbb', 'Select target folder'), (path: string) => { OC.dialogs.filepicker(t('bbb', 'Select target folder'), (path: string) => {
api.storeRecording(recording, path).then((filename) => { api.storeRecording(recording, format, path).then((filename) => {
OC.dialogs.info( OC.dialogs.info(
t('bbb', 'URL to presentation was stored in "{path}" as "{filename}".', { path: path + '/', filename }), t('bbb', 'URL to presentation was stored in "{path}" as "{filename}".', { path: path + '/', filename }),
t('bbb', 'Link stored'), t('bbb', 'Link stored'),
@ -288,7 +288,7 @@ const RoomRow = (props: Props): JSX.Element => {
<td colSpan={11}> <td colSpan={11}>
<table> <table>
<tbody> <tbody>
{recordings?.sort((r1, r2) => r1.startTime - r2.startTime).map(recording => <RecordingRow key={recording.id} isAdmin={adminRoom} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} publishRecording={publishRecording} />)} {recordings?.sort((r1, r2) => r1.startTime - r2.startTime).map(recording => recording.formats.map(format => <RecordingRow key={recording.id} isAdmin={adminRoom} recording={recording} format={format} deleteRecording={deleteRecording} storeRecording={storeRecording} publishRecording={publishRecording} />))}
</tbody> </tbody>
</table> </table>
</td> </td>