Skip to content
Snippets Groups Projects
Unverified Commit 6df4dba8 authored by Ahmad Farhat's avatar Ahmad Farhat Committed by GitHub
Browse files

Added support for protected recordings (#5220)

* First draft of fixing protected recordings

* Cleaned up code

* Eslint

* Add specs

* CR

* CR2

* Fix tests
parent 9cce2b92
Branches
Tags
No related merge requests found
......@@ -19,11 +19,11 @@
module Api
module V1
class RecordingsController < ApiController
before_action :find_recording, only: %i[update update_visibility]
before_action :find_recording, only: %i[update update_visibility recording_url]
before_action only: %i[destroy] do
ensure_authorized('ManageRecordings', record_id: params[:id])
end
before_action only: %i[update update_visibility] do
before_action only: %i[update update_visibility recording_url] do
ensure_authorized(%w[ManageRecordings SharedRoom], record_id: params[:id])
end
before_action only: %i[index recordings_count] do
......@@ -89,6 +89,22 @@ module Api
render_data data: count, status: :ok
end
# POST /api/v1/recordings/recording_url.json
def recording_url
record_format = params[:recording_format]
url = if @recording.visibility == 'Protected'
recording = BigBlueButtonApi.new(provider: current_provider).get_recording(record_id: @recording.record_id)
formats = recording[:playback][:format]
record_format.present? ? formats.find { |format| format[:type] == record_format }[:url] : formats.pluck(:url)
else
record_format.present? ? @recording.formats.find_by(recording_type: record_format).url : @recording.formats.pluck(:url)
end
render_data data: url, status: :ok
end
private
def recording_params
......
......@@ -23,7 +23,6 @@ import PropTypes from 'prop-types';
import {
Button, Stack, Dropdown,
} from 'react-bootstrap';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../contexts/auth/AuthProvider';
import Spinner from '../shared_components/utilities/Spinner';
......@@ -31,6 +30,8 @@ import UpdateRecordingForm from './forms/UpdateRecordingForm';
import DeleteRecordingForm from './forms/DeleteRecordingForm';
import Modal from '../shared_components/modals/Modal';
import { localizeDateTimeString } from '../../helpers/DateTimeHelper';
import useRedirectRecordingUrl from '../../hooks/mutations/recordings/useRedirectRecordingUrl';
import useCopyRecordingUrl from '../../hooks/mutations/recordings/useCopyRecordingUrl';
// TODO: Amir - Refactor this.
export default function RecordingRow({
......@@ -38,16 +39,14 @@ export default function RecordingRow({
}) {
const { t } = useTranslation();
function copyUrls() {
const formatUrls = recording.formats.map((format) => format.url);
navigator.clipboard.writeText(formatUrls);
toast.success(t('toast.success.recording.copied_urls'));
}
const visibilityAPI = useVisibilityAPI();
const [isEditing, setIsEditing] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const currentUser = useAuth();
const redirectRecordingUrl = useRedirectRecordingUrl();
const copyRecordingUrl = useCopyRecordingUrl();
const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language);
const formats = recording.formats.sort(
(a, b) => (a.recording_type.toLowerCase() > b.recording_type.toLowerCase() ? 1 : -1),
......@@ -114,7 +113,7 @@ export default function RecordingRow({
<td className="border-0">
{formats.map((format) => (
<Button
onClick={() => window.open(format.url, '_blank')}
onClick={() => redirectRecordingUrl.mutate({ record_id: recording.record_id, format: format.recording_type })}
className={`btn-sm rounded-pill me-1 mt-1 border-0 btn-format-${format.recording_type.toLowerCase()}`}
key={`${format.recording_type}-${format.url}`}
>
......@@ -128,7 +127,7 @@ export default function RecordingRow({
<Dropdown className="float-end cursor-pointer">
<Dropdown.Toggle className="hi-s" as={EllipsisVerticalIcon} />
<Dropdown.Menu>
<Dropdown.Item onClick={() => copyUrls()}>
<Dropdown.Item onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}>
<ClipboardDocumentIcon className="hi-s me-2" />
{ t('recording.copy_recording_urls') }
</Dropdown.Item>
......@@ -149,7 +148,7 @@ export default function RecordingRow({
<Button
variant="icon"
className="mt-1 me-3"
onClick={() => copyUrls()}
onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}
>
<ClipboardDocumentIcon className="hi-s text-muted" />
</Button>
......
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
//
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
//
// This program is free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free Software
// Foundation; either version 3.0 of the License, or (at your option) any later
// version.
//
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
import { useMutation } from 'react-query';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import axios from '../../../helpers/Axios';
export default function useCopyRecordingUrl() {
const { t } = useTranslation();
return useMutation(
(data) => axios.post('/recordings/recording_url.json', { id: data.record_id })
.then((resp) => resp.data),
{
onSuccess: (url) => {
navigator.clipboard.writeText(url?.join('\n')).then(() => toast.success(t('toast.success.recording.copied_urls')));
},
onError: () => {
toast.error(t('toast.error.problem_completing_action'));
},
},
);
}
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
//
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
//
// This program is free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free Software
// Foundation; either version 3.0 of the License, or (at your option) any later
// version.
//
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
import { useMutation } from 'react-query';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import axios from '../../../helpers/Axios';
export default function useRedirectRecordingUrl() {
const { t } = useTranslation();
return useMutation(
(data) => axios.post('/recordings/recording_url.json', { id: data.record_id, recording_format: data.format })
.then((resp) => resp.data.data),
{
onSuccess: (url) => {
window.open(url, '_blank');
},
onError: () => {
toast.error(t('toast.error.problem_completing_action'));
},
},
);
}
......@@ -62,6 +62,7 @@ Rails.application.routes.draw do
collection do
post '/update_visibility', to: 'recordings#update_visibility'
get '/recordings_count', to: 'recordings#recordings_count'
post '/recording_url', to: 'recordings#recording_url'
end
end
resources :shared_accesses, only: %i[create show destroy], param: :friendly_id do
......
......@@ -268,6 +268,54 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do
expect(JSON.parse(response.body)['data']).to be(5)
end
end
describe '#recording_url' do
let(:room) { create(:room, user:) }
before do
allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return(
playback: { format: [{ type: 'screenshare', url: 'https://test.com/screenshare' }, { type: 'video', url: 'https://test.com/video' }] }
)
end
context 'format not passed' do
it 'makes a call to BBB and returns the url returned if the recording is protected' do
recording = create(:recording, visibility: 'Protected', room:)
post :recording_url, params: { id: recording.record_id }
expect(JSON.parse(response.body)).to match_array ['https://test.com/screenshare', 'https://test.com/video']
end
it 'returns the formats url' do
recording = create(:recording, visibility: 'Published', room:)
create(:format, recording:)
post :recording_url, params: { id: recording.record_id }
expect(JSON.parse(response.body)).to match_array recording.formats.pluck(:url)
end
end
context 'format is passed' do
it 'makes a call to BBB and returns the url returned if the recording is protected' do
recording = create(:recording, visibility: 'Protected', room:)
post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' }
expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare'
end
it 'returns the formats url' do
recording = create(:recording, visibility: 'Published', room:)
format = create(:format, recording:, recording_type: 'podcast')
post :recording_url, params: { id: recording.record_id, recording_format: format.recording_type }
expect(JSON.parse(response.body)['data']).to eq format.url
end
end
end
end
def http_ok_response
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment