diff --git a/app/controllers/api/v1/recordings_controller.rb b/app/controllers/api/v1/recordings_controller.rb index f6f1bcd267363eb82c7f946899cd207326a1b8d9..ece4ba6b07a217ff9a6c460f95979ab0d0bdc9d1 100644 --- a/app/controllers/api/v1/recordings_controller.rb +++ b/app/controllers/api/v1/recordings_controller.rb @@ -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 diff --git a/app/javascript/components/recordings/RecordingRow.jsx b/app/javascript/components/recordings/RecordingRow.jsx index bfca6aedefb0d2d0b5ab8d437c4a997c84fe1951..849a091a2ab2ac8f9e0e2ac6c251eb6a004225d0 100644 --- a/app/javascript/components/recordings/RecordingRow.jsx +++ b/app/javascript/components/recordings/RecordingRow.jsx @@ -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> diff --git a/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b15ca620832445618122c3d0f5e4e80bb384c695 --- /dev/null +++ b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx @@ -0,0 +1,37 @@ +// 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')); + }, + }, + ); +} diff --git a/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx new file mode 100644 index 0000000000000000000000000000000000000000..df48ff1c5eb1430e39754ee642f3e03f996a3a96 --- /dev/null +++ b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx @@ -0,0 +1,37 @@ +// 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')); + }, + }, + ); +} diff --git a/config/routes.rb b/config/routes.rb index a9d7f4955288496b0486a02c0f9d654c8dd18cae..4696bc02a284e219b2e7da2f63951ccea2597127 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/spec/controllers/recordings_controller_spec.rb b/spec/controllers/recordings_controller_spec.rb index f4dd50494c31e5f0d5c18717893f9bff00a9a65d..687d53300924472a519829b8fdf58ec217b98a85 100644 --- a/spec/controllers/recordings_controller_spec.rb +++ b/spec/controllers/recordings_controller_spec.rb @@ -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