Add endpoint to detect new recordings and update the interface

The endpoint receives events from BigBlueButton via webhooks or scripts
in the record and playback workflow.
For now it only treats the event for when a recording is ready.
When it happens, it uses action cable to update the interface dynamically
with the new recording.
This commit is contained in:
Leonardo Crauss Daronco 2016-12-07 16:20:10 -02:00
parent b518458622
commit e5efb05a01
11 changed files with 178 additions and 16 deletions

View File

@ -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);
}
}
}
});

View File

@ -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) ->

View File

@ -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());

View File

@ -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(/&amp;/, '&').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

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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

View File

@ -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

View File

@ -15,7 +15,9 @@
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
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,

View File

@ -79,4 +79,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<script type="text/javascript">
window.I18n = <%= client_translations.to_json.html_safe %>
window.GreenLight = {};
window.GreenLight.META_LISTED = "<%= BbbApi::META_LISTED %>";
window.GreenLight.META_TOKEN = "<%= BbbApi::META_TOKEN %>";
window.GreenLight.META_HOOK_URL = "<%= BbbApi::META_HOOK_URL %>";
</script>

View File

@ -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

View File

@ -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'}