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