diff --git a/app/assets/javascripts/channels/recording_update.js b/app/assets/javascripts/channels/recording_update.js index 1b13b5bb..2b517ccd 100644 --- a/app/assets/javascripts/channels/recording_update.js +++ b/app/assets/javascripts/channels/recording_update.js @@ -26,23 +26,32 @@ var recordings = Recordings.getInstance(); var table = recordings.table.api(); - var row = table.row("#"+data.record_id); + var row = table.row("#"+data.id); if (data.action === 'update') { var rowData = row.data(); rowData.published = data.published; rowData.listed = data.listed; - table.row("#"+data.record_id).data(rowData); + table.row("#"+data.id).data(rowData); recordings.draw(); var status = data.published ? (data.listed ? 'published' : 'unlisted') : 'unpublished'; showAlert(I18n['recording_'+status], 4000); + } else if (data.action === 'delete') { row.remove(); recordings.draw(); - showAlert(I18n.recording_deleted, 4000); + + } else if (data.action === 'create') { + if (row.length == 0) { + data.duration = data.length; + table.rows.add([data]); + recordings.draw(); + showAlert(I18n.recording_created, 4000); + } + } } }); diff --git a/app/assets/javascripts/recordings.coffee b/app/assets/javascripts/recordings.coffee index d6ee8d3d..0c9cc382 100644 --- a/app/assets/javascripts/recordings.coffee +++ b/app/assets/javascripts/recordings.coffee @@ -166,13 +166,13 @@ class @Recordings listed = btn.data('visibility') == "published" btn.prop('disabled', true) + + data = { published: published.toString() } + data["meta_" + GreenLight.META_LISTED] = listed.toString(); $.ajax({ method: 'PATCH', url: url+'/recordings/'+id, - data: { - published: published.toString(), - "meta_greenlight-listed": listed.toString() - } + data: data }).done((data) -> ).fail((data) -> diff --git a/app/assets/javascripts/shared.js b/app/assets/javascripts/shared.js index 854679a5..166ef5ad 100644 --- a/app/assets/javascripts/shared.js +++ b/app/assets/javascripts/shared.js @@ -34,6 +34,7 @@ var loopJoin = function() { }); } +var alertTimeout = null; var showAlert = function(html, timeout_delay) { if (!html) { return; @@ -43,11 +44,12 @@ var showAlert = function(html, timeout_delay) { $('#alerts').html($('.alert-template').html()); if (timeout_delay) { - setTimeout(function() { + clearTimeout(alertTimeout); + alertTimeout = setTimeout(function() { $('#alerts > .alert').alert('close'); }, timeout_delay); } -} +}; var displayRoomURL = function() { $('.meeting-url').val(Meeting.getInstance().getURL()); diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index ae2272a7..1bf33933 100644 --- a/app/controllers/bbb_controller.rb +++ b/app/controllers/bbb_controller.rb @@ -20,6 +20,9 @@ class BbbController < ApplicationController before_action :authorize_recording_owner!, only: [:update_recordings, :delete_recordings] before_action :load_and_authorize_room_owner!, only: [:end] + skip_before_action :verify_authenticity_token, only: :callback + before_action :validate_checksum, only: :callback + # GET /:resource/:id/join def join if params[:name].blank? @@ -39,7 +42,9 @@ class BbbController < ApplicationController user_is_moderator: true } end - options[:meeting_logout_url] = "#{request.base_url}/#{params[:resource]}/#{params[:id]}" + base_url = "#{request.base_url}/#{params[:resource]}/#{params[:id]}" + options[:meeting_logout_url] = base_url + options[:hook_url] = "#{base_url}/callback" bbb_res = bbb_join_url( params[:id], @@ -55,6 +60,20 @@ class BbbController < ApplicationController end end + # POST /:resource/:id/callback + # Endpoint for webhook calls from BigBlueButton + def callback + begin + data = JSON.parse(read_body(request)) + treat_callback_event(data["event"]) + rescue Exception => e + logger.error "Error parsing webhook data. Data: #{data}, exception: #{e.inspect}" + + # respond with 200 anyway so BigBlueButton knows the hook call was ok + render head(:ok) + end + end + # DELETE /rooms/:id/end def end load_and_authorize_room_owner! @@ -130,4 +149,61 @@ class BbbController < ApplicationController @response = response render status: @status end + + def read_body(request) + request.body.read.force_encoding("UTF-8") + end + + def treat_callback_event(event) + + # a recording is ready + if event['header']['name'] == "publish_ended" + token = event['payload']['metadata'][META_TOKEN] + record_id = event['payload']['meeting_id'] + + # the webhook event doesn't have all the data we need, so we need + # to send a getRecordings anyway + # TODO: if the webhooks included all data we wouldn't need this + rec_info = bbb_get_recordings(token, record_id) + rec_info = rec_info[:recordings].first + RecordingCreatedJob.perform_later(token, parse_recording_for_view(rec_info)) + end + + render head(:ok) && return + end + + # Validates the checksum received in a callback call. + # If the checksum doesn't match, renders an ok and aborts execution. + def validate_checksum + secret = ENV['BIGBLUEBUTTON_SECRET'] + checksum = params["checksum"] + data = read_body(request) + callback_url = uri_remove_param(request.url, "checksum") + + checksum_str = "#{callback_url}#{data}#{secret}" + calculated_checksum = Digest::SHA1.hexdigest(checksum_str) + + if calculated_checksum != checksum + logger.error "Checksum did not match. Calculated: #{calculated_checksum}, received: #{checksum}" + + # respond with 200 anyway so BigBlueButton knows the hook call was ok + # but abort execution + render head(:ok) && return + end + end + + # Removes parameters from an URI + def uri_remove_param(uri, params = nil) + return uri unless params + params = Array(params) + uri_parsed = URI.parse(uri) + return uri unless uri_parsed.query + new_params = uri_parsed.query.gsub(/&/, '&').split('&').reject { |q| params.include?(q.split('=').first) } + uri = uri.split('?').first + if new_params.count > 0 + "#{uri}?#{new_params.join('&')}" + else + uri + end + end end diff --git a/app/jobs/recording_created_job.rb b/app/jobs/recording_created_job.rb new file mode 100644 index 00000000..c38370d8 --- /dev/null +++ b/app/jobs/recording_created_job.rb @@ -0,0 +1,26 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2016 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. +# +# BigBlueButton 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 BigBlueButton; if not, see . + +class RecordingCreatedJob < ApplicationJob + include BbbApi + + queue_as :default + + def perform(room, recording) + ActionCable.server.broadcast "#{room}_recording_updates_channel", + { action: 'create' }.merge(recording) + end +end diff --git a/app/jobs/recording_deletes_job.rb b/app/jobs/recording_deletes_job.rb index 624ada8f..74df9abd 100644 --- a/app/jobs/recording_deletes_job.rb +++ b/app/jobs/recording_deletes_job.rb @@ -28,7 +28,7 @@ class RecordingDeletesJob < ApplicationJob if !bbb_res[:recordings] || bbb_res[:messageKey] == 'noRecordings' ActionCable.server.broadcast "#{room}_recording_updates_channel", action: 'delete', - record_id: record_id + id: record_id break end sleep sleep_time diff --git a/app/jobs/recording_updates_job.rb b/app/jobs/recording_updates_job.rb index 6c21a6f0..9dafbb2f 100644 --- a/app/jobs/recording_updates_job.rb +++ b/app/jobs/recording_updates_job.rb @@ -24,7 +24,7 @@ class RecordingUpdatesJob < ApplicationJob recording = bbb_res[:recordings].first ActionCable.server.broadcast "#{room}_recording_updates_channel", action: 'update', - record_id: record_id, + id: record_id, published: recording[:published], listed: bbb_is_recording_listed(recording) end diff --git a/app/lib/bbb_api.rb b/app/lib/bbb_api.rb index a12657fb..e1690485 100644 --- a/app/lib/bbb_api.rb +++ b/app/lib/bbb_api.rb @@ -15,7 +15,9 @@ # with BigBlueButton; if not, see . module BbbApi - META_LISTED = "greenlight-listed" + META_LISTED = "gl-listed" + META_TOKEN = "gl-token" + META_HOOK_URL = "gl-webhooks-callback-url" def bbb_endpoint Rails.configuration.bigbluebutton_endpoint || '' @@ -53,7 +55,7 @@ module BbbApi # See if the meeting is running begin - bbb_meeting_info = bbb.get_meeting_info( meeting_id, nil ) + bbb_meeting_info = bbb.get_meeting_info(meeting_id, nil) rescue BigBlueButton::BigBlueButtonException => exc # This means that is not created @@ -72,9 +74,14 @@ module BbbApi logoutURL: logout_url, moderatorPW: moderator_password, attendeePW: viewer_password, - "meta_#{BbbApi::META_LISTED}": false + "meta_#{BbbApi::META_LISTED}": false, + "meta_#{BbbApi::META_TOKEN}": meeting_token } + meeting_options.merge!( + { "meta_#{BbbApi::META_HOOK_URL}": options[:hook_url] } + ) if options[:hook_url] + # Create the meeting bbb.create_meeting(options[:meeting_name], meeting_id, meeting_options) @@ -110,7 +117,7 @@ module BbbApi options[:recordID] = record_id end if meeting_id - options[:meetingID] = (Digest::SHA1.hexdigest(Rails.application.secrets[:secret_key_base]+meeting_id)).to_s + options[:meetingID] = bbb_meeting_id(meeting_id) end res = bbb_safe_execute :get_recordings, options @@ -210,6 +217,42 @@ module BbbApi recording[:metadata][BbbApi::META_LISTED.to_sym] == "true" end + # Parses a recording as returned by getRecordings and returns it + # as an object as expected by the views. + # TODO: this is almost the same done by jbuilder templates (bbb/recordings), + # how to reuse them? + def parse_recording_for_view(recording) + recording[:previews] ||= [] + previews = recording[:previews].map do |preview| + { + url: preview[:content], + width: preview[:width], + height: preview[:height], + alt: preview[:alt] + } + end + recording[:playbacks] ||= [] + playbacks = recording[:playbacks].map do |playback| + { + type: playback[:type], + type_i18n: t(playback[:type]), + url: playback[:url], + previews: previews + } + end + { + id: recording[:recordID], + name: recording[:name], + published: recording[:published], + end_time: recording[:endTime].to_s, + start_time: recording[:startTime].to_s, + length: recording[:length], + listed: recording[:listed], + playbacks: playbacks, + previews: previews + } + end + def success_join_res(join_url) { returncode: true, diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b038e670..e13b10ea 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -79,4 +79,8 @@ with BigBlueButton; if not, see . diff --git a/config/locales/en-us.yml b/config/locales/en-us.yml index b44ff5f5..86365cbe 100644 --- a/config/locales/en-us.yml +++ b/config/locales/en-us.yml @@ -48,6 +48,7 @@ en-US: no_recordings: No Recordings no_recordings_yet: No Recordings (Yet!) publish_recording: Publish recording + recording_created: A recording was created recording_deleted: Recording was deleted recording_published: Recording was published recording_unlisted: Recording was unlisted diff --git a/config/routes.rb b/config/routes.rb index 47af0695..5c89477d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,7 @@ Rails.application.routes.draw do get '/:resource/:id/join', to: 'bbb#join', as: :bbb_join, defaults: {format: 'json'} get '/:resource/:id/wait', to: 'landing#wait_for_moderator' get '/:resource/:id/session_status_refresh', to: 'landing#session_status_refresh' + post '/:resource/:id/callback', to: 'bbb#callback' #, defaults: {format: 'json'} delete '/rooms/:id/end', to: 'bbb#end', defaults: {format: 'json'} get '/rooms/:id/recordings', to: 'bbb#recordings', defaults: {format: 'json'} patch '/rooms/:id/recordings/:record_id', to: 'bbb#update_recordings', defaults: {format: 'json'}