Merge pull request #19 from zach-chai/recordings

Recordings
This commit is contained in:
Zachary Chai 2016-11-01 15:27:30 -04:00 committed by GitHub
commit a9230616bc
21 changed files with 341 additions and 25 deletions

View File

@ -12,6 +12,8 @@
// //
//= require jquery2 //= require jquery2
//= require jquery-ui //= require jquery-ui
//= require dataTables/jquery.dataTables
//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
//= require bootstrap-sprockets //= require bootstrap-sprockets
//= require turbolinks //= require turbolinks
//= require_self //= require_self

View File

@ -0,0 +1,25 @@
(function() {
var initRooms = function() {
App.messages = App.cable.subscriptions.create({
channel: 'RecordingUpdatesChannel',
username: window.location.pathname.split('/').pop()
},
{
received: function(data) {
var table = $("#recordings").DataTable();
var rowData = table.row("#"+data.record_id).data();
rowData.published = data.published
table.row("#"+data.record_id).data(rowData).draw();
}
});
};
$(document).on("turbolinks:load", function() {
if ($("body[data-controller=landing]").get(0)) {
if ($("body[data-action=rooms]").get(0)) {
initRooms();
}
}
});
}).call(this);

View File

@ -1,4 +1,6 @@
(function() { (function() {
var recordingsTable = null;
var waitForModerator = function(url) { var waitForModerator = function(url) {
$.get(url + "/wait", function(html) { $.get(url + "/wait", function(html) {
$(".center-panel-wrapper").html(html); $(".center-panel-wrapper").html(html);
@ -72,8 +74,112 @@
window.location.hostname + window.location.hostname +
meetingURL.data('path'); meetingURL.data('path');
meetingURL.val(link); meetingURL.val(link);
// initialize recordings datatable
recordingsTable = $('#recordings').dataTable({
data: [],
rowId: 'id',
paging: false,
searching: false,
info: false,
ordering: false,
language: {
emptyTable: "Past recordings are shown here."
},
columns: [
{ title: "Date Recorded", data: "start_time" },
{ title: "Duration", data: "duration" },
{ title: "Views", data: "playbacks" },
{ title: "Actions", data: "id" }
],
columnDefs: [
{
targets: 2,
render: function(data, type, row) {
if (type === 'display') {
var str = "";
if (row.published) {
for(let i in data) {
str += '<a href="'+data[i].url+'">'+data[i].type+'</a> ';
}
}
return str;
}
return data;
}
},
{
targets: -1,
render: function(data, type, row) {
if (type === 'display') {
var roomName = window.location.pathname.split('/').pop();
var published = row.published;
var eye = getPublishClass(published);
return '<button type="button" class="btn btn-default recording-update" data-id="'+data+'" data-room="'+roomName+'" data-published="'+published+'">' +
'<i class="fa '+eye+'" aria-hidden="true"></i></button> ' +
'<button type="button" class="btn btn-default recording-delete" data-id="'+data+'" data-room="'+roomName+'">' +
'<i class="fa fa-trash-o" aria-hidden="true"></i></button>';
}
return data;
}
}
]
});
$('#recordings').on('click', '.recording-update', function(event) {
var btn = $(this);
var room = btn.data('room');
var id = btn.data('id');
var published = btn.data('published');
btn.prop("disabled", true);
$.ajax({
method: 'PATCH',
url: '/rooms/'+room+'/recordings/'+id,
data: {published: (!published).toString()}
}).done(function(data) {
}).fail(function(data) {
btn.prop("disabled", false);
});
});
$('#recordings').on('click', '.recording-delete', function(event) {
var room = $(this).data('room');
var id = $(this).data('id');
$.ajax({
method: 'DELETE',
url: '/rooms/'+room+'/recordings/'+id
}).done(function() {
recordingsTable.api().row("#"+id).remove().draw();
});
});
refreshRecordings();
}; };
var refreshRecordings = function() {
if (!recordingsTable) {
return;
}
table = recordingsTable.api();
$.get("/rooms/"+window.location.pathname.split('/').pop()+"/recordings", function(data) {
if (!data.is_owner) {
table.column(-1).visible( false );
}
var i;
for (i = 0; i < data.recordings.length; i++) {
var totalMinutes = Math.round((new Date(data.recordings[i].end_time) - new Date(data.recordings[i].start_time)) / 1000 / 60);
data.recordings[i].duration = totalMinutes;
data.recordings[i].start_time = new Date(data.recordings[i].start_time)
.toLocaleString([], {month: 'long', day: 'numeric', year: 'numeric', hour12: 'true', hour: '2-digit', minute: '2-digit'});
}
table.clear();
table.rows.add(data.recordings);
table.columns.adjust().draw();
});
}
$(document).on("turbolinks:load", function() { $(document).on("turbolinks:load", function() {
init(); init();
if ($("body[data-controller=landing]").get(0)) { if ($("body[data-controller=landing]").get(0)) {

View File

@ -1,3 +1,15 @@
$.ajaxSetup({
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
}
});
var PUBLISHED_CLASSES = ['fa-eye-slash', 'fa-eye']
var getPublishClass = function(published) {
return PUBLISHED_CLASSES[+published];
}
var meetingInstance = null; var meetingInstance = null;
class Meeting { class Meeting {
constructor(url, name) { constructor(url, name) {

View File

@ -11,7 +11,7 @@
* It is generally better to create a new file per style scope. * It is generally better to create a new file per style scope.
* *
*= require jquery-ui *= require jquery-ui
*= require dataTables/jquery.dataTables *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
*= require_tree . *= require_tree .
*= require_self *= require_self
*/ */

View File

@ -1,3 +1,9 @@
// Place all the styles related to the landing controller here. // Place all the styles related to the landing controller here.
// They will automatically be included in application.css. // They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/ // You can use Sass (SCSS) here: http://sass-lang.com/
.rooms {
.table-wrapper {
padding: 40px 50px 10px 50px;
}
}

View File

@ -0,0 +1,5 @@
class RecordingUpdatesChannel < ApplicationCable::Channel
def subscribed
stream_from "#{params[:username]}_recording_updates_channel"
end
end

View File

@ -1,17 +1,19 @@
class BbbController < ApplicationController class BbbController < ApplicationController
include BbbApi
before_action :authorize_owner_recording, only: [:update_recordings, :delete_recordings]
# GET /:resource/:id/join # GET /:resource/:id/join
def join def join
if ( params[:id].blank? ) if params[:name].blank?
render_response("missing_parameter", "meeting token was not included", :bad_request) render_bbb_response("missing_parameter", "user name was not included", :unprocessable_entity)
elsif ( params[:name].blank? )
render_response("missing_parameter", "user name was not included", :bad_request)
else else
user = User.find_by username: params[:id] user = User.find_by username: params[:id]
options = if user options = if user
{ {
wait_for_moderator: true, wait_for_moderator: true,
meeting_recorded: true,
user_is_moderator: current_user == user user_is_moderator: current_user == user
} }
else else
@ -19,28 +21,72 @@ class BbbController < ApplicationController
end end
options[:meeting_logout_url] = "#{request.base_url}/#{params[:resource]}/#{params[:id]}" options[:meeting_logout_url] = "#{request.base_url}/#{params[:resource]}/#{params[:id]}"
bbb_res = helpers.bbb_join_url( bbb_res = bbb_join_url(
params[:id], params[:id],
params[:name], params[:name],
options options
) )
if bbb_res[:returncode] && current_user && current_user == user if bbb_res[:returncode] && current_user && current_user == user
ActionCable.server.broadcast "moderator_#{user.username}_join_channel", ActionCable.server.broadcast "moderator_#{user.username}_join_channel",
moderator: "joined" moderator: "joined"
end end
render_response bbb_res[:messageKey], bbb_res[:message], bbb_res[:status], bbb_res[:response] render_bbb_response bbb_res, bbb_res[:response]
end end
end end
# GET /rooms/:id/recordings
def recordings
@user = User.find_by username: params[:id]
if !@user
render head(:not_found) && return
end
bbb_res = bbb_get_recordings @user.username
render_bbb_response bbb_res, bbb_res[:recordings]
end
# PATCH /rooms/:id/recordings/:record_id
def update_recordings
bbb_res = bbb_update_recordings(params[:record_id], params[:published] == 'true')
if bbb_res[:returncode]
RecordingUpdatesJob.perform_later(@user.username, params[:record_id], bbb_res[:published])
end
render_bbb_response bbb_res
end
# DELETE /rooms/:id/recordings/:record_id
def delete_recordings
bbb_res = bbb_delete_recordings(params[:record_id])
render_bbb_response bbb_res
end
private private
def render_response(messageKey, message, status, response={})
@messageKey = messageKey def authorize_owner_recording
@message = message user = User.find_by username: params[:id]
@status = status if !user
render head(:not_found) && return
elsif !current_user || current_user != user
render head(:unauthorized) && return
end
recordings = bbb_get_recordings(params[:id])[:recordings]
recordings.each do |recording|
if recording[:recordID] == params[:record_id]
@user = user
return true
end
end
render head(:not_found) && return
end
def render_bbb_response(bbb_res, response={})
@messageKey = bbb_res[:messageKey]
@message = bbb_res[:message]
@status = bbb_res[:status]
@response = response @response = response
render status: @status render status: @status && return
end end
end end

View File

@ -0,0 +1,23 @@
class RecordingUpdatesJob < ApplicationJob
include BbbApi
queue_as :default
def perform(room, record_id, published)
tries = 0
sleep_time = 2
while tries < 4
bbb_res = bbb_get_recordings(nil, record_id)
if bbb_res[:recordings].first[:published].to_s == published
ActionCable.server.broadcast "#{room}_recording_updates_channel",
record_id: record_id,
published: bbb_res[:recordings].first[:published]
break
end
sleep sleep_time
sleep_time = sleep_time * 2
tries += 1
end
end
end

View File

@ -1,4 +1,4 @@
module BbbHelper module BbbApi
def bbb_endpoint def bbb_endpoint
Rails.application.secrets[:bbb_endpoint] Rails.application.secrets[:bbb_endpoint]
end end
@ -7,6 +7,10 @@ module BbbHelper
Rails.application.secrets[:bbb_secret] Rails.application.secrets[:bbb_secret]
end end
def bbb
@bbb ||= BigBlueButton::BigBlueButtonApi.new(bbb_endpoint + "api", bbb_secret, "0.8", true)
end
def random_password(length) def random_password(length)
o = [('a'..'z'), ('A'..'Z')].map { |i| i.to_a }.flatten o = [('a'..'z'), ('A'..'Z')].map { |i| i.to_a }.flatten
password = (0...length).map { o[rand(o.length)] }.join password = (0...length).map { o[rand(o.length)] }.join
@ -19,7 +23,6 @@ module BbbHelper
options[:wait_for_moderator] ||= false options[:wait_for_moderator] ||= false
options[:meeting_logout_url] ||= nil options[:meeting_logout_url] ||= nil
bbb ||= BigBlueButton::BigBlueButtonApi.new(bbb_endpoint + "api", bbb_secret, "0.8", true)
if !bbb if !bbb
return call_invalid_res return call_invalid_res
else else
@ -41,8 +44,7 @@ module BbbHelper
logout_url = options[:meeting_logout_url] || "#{request.base_url}" logout_url = options[:meeting_logout_url] || "#{request.base_url}"
moderator_password = random_password(12) moderator_password = random_password(12)
viewer_password = random_password(12) viewer_password = random_password(12)
meeting_options = {:record => options[:meeting_recorded].to_s, :logoutURL => logout_url, :moderatorPW => moderator_password, :attendeePW => viewer_password } meeting_options = {record: options[:meeting_recorded].to_s, logoutURL: logout_url, moderatorPW: moderator_password, attendeePW: viewer_password}
# Create the meeting # Create the meeting
bbb.create_meeting(meeting_token, meeting_id, meeting_options) bbb.create_meeting(meeting_token, meeting_id, meeting_options)
@ -65,6 +67,40 @@ module BbbHelper
end end
end end
def bbb_get_recordings(meeting_id, record_id=nil)
options={}
if record_id
options[:recordID] = record_id
end
if meeting_id
options[:meetingID] = (Digest::SHA1.hexdigest(Rails.application.secrets[:secret_key_base]+meeting_id)).to_s
end
bbb_safe_execute :get_recordings, options
end
def bbb_update_recordings(id, published)
bbb_safe_execute :publish_recordings, id, published
end
def bbb_delete_recordings(id)
bbb_safe_execute :delete_recordings, id
end
# method must be a symbol of the method's name
def bbb_safe_execute(method, *args)
if !bbb
return call_invalid_res
else
begin
response_data = bbb.send(method, *args)
response_data[:status] = :ok
rescue BigBlueButton::BigBlueButtonException => exc
response_data = bbb_exception_res exc
end
end
response_data
end
def success_res(join_url) def success_res(join_url)
{ {
returncode: true, returncode: true,
@ -94,4 +130,13 @@ module BbbHelper
status: :internal_server_error status: :internal_server_error
} }
end end
def bbb_exception_res(exc)
{
returncode: false,
messageKey: 'BBB'+exc.key.capitalize.underscore,
message: exc.message,
status: :internal_server_error
}
end
end end

View File

@ -0,0 +1,3 @@
json.messageKey messageKey
json.message message
json.status status

View File

@ -0,0 +1 @@
json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status

View File

@ -1,7 +1,5 @@
json.messageKey @messageKey json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status
json.message @message unless @response.blank?
json.status @status
if @response
json.response do json.response do
json.join_url(@response[:join_url]) if @response[:join_url] json.join_url(@response[:join_url]) if @response[:join_url]
end end

View File

@ -0,0 +1,25 @@
json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status
unless @response.blank?
json.is_owner current_user == @user
json.recordings do
unless @response.is_a? Array
@response = [@response]
end
json.array!(@response) do |recording|
json.id recording[:recordID]
json.name recording[:name]
json.start_time recording[:startTime]
json.end_time recording[:endTime]
json.published recording[:published]
json.playbacks do
unless recording[:playback][:format].is_a? Array
recording[:playback][:format] = [recording[:playback][:format]]
end
json.array!(recording[:playback][:format]) do |playback|
json.type playback[:type]
json.url playback[:url]
end
end
end
end
end

View File

@ -0,0 +1 @@
json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status

View File

@ -15,7 +15,7 @@
</div> </div>
<% end %> <% end %>
<div class="page-wrapper meeting"> <div class="page-wrapper meetings">
<div class='container-fluid'> <div class='container-fluid'>
<%= render 'shared/title', title: 'Start A New Session' %> <%= render 'shared/title', title: 'Start A New Session' %>

View File

@ -16,7 +16,7 @@
</div> </div>
<% end %> <% end %>
<div class="page-wrapper room"> <div class="page-wrapper rooms">
<div class="container-fluid"> <div class="container-fluid">
<%= render 'shared/title', title: page_title %> <%= render 'shared/title', title: page_title %>
@ -34,5 +34,9 @@
<% end %> <% end %>
</div> </div>
<div class="table-wrapper">
<h3>Past Recordings</h3>
<table id="recordings" class="table" width="100%"></table>
</div>
</div> </div>
</div> </div>

View File

@ -31,11 +31,13 @@ Rails.application.configure do
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
config.active_job.queue_adapter = :async
# action cable socket URI # action cable socket URI
config.action_cable.url = "ws://localhost/cable" config.action_cable.url = "ws://localhost/cable"
# allowed action cable origins # allowed action cable origins
Rails.application.config.action_cable.allowed_request_origins = ['http://localhost'] config.action_cable.allowed_request_origins = ['http://localhost']
# Print deprecation notices to the Rails logger. # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log config.active_support.deprecation = :log

View File

@ -55,6 +55,8 @@ Rails.application.configure do
# Use a real queuing backend for Active Job (and separate queues per environment) # Use a real queuing backend for Active Job (and separate queues per environment)
# config.active_job.queue_adapter = :resque # config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "greenlight_#{Rails.env}" # config.active_job.queue_name_prefix = "greenlight_#{Rails.env}"
config.active_job.queue_adapter = :async
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors. # Ignore bad email addresses and do not raise email delivery errors.

View File

@ -11,8 +11,11 @@ Rails.application.routes.draw do
# meetings offer a landing page for NON authenticated users to create and join session in BigBlueButton # meetings offer a landing page for NON authenticated users to create and join session in BigBlueButton
# rooms offer a customized landing page for authenticated users to create and join session in BigBlueButton # rooms offer a customized landing page for authenticated users to create and join session in BigBlueButton
get '/:resource/:id', to: 'landing#index', as: :resource get '/:resource/:id', to: 'landing#index', as: :resource
get '/:resource/:id/join', to: 'bbb#join', as: :bbb_join, defaults: { :format => 'json' } 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/wait', to: 'landing#wait_for_moderator'
get '/rooms/:id/recordings', to: 'bbb#recordings', defaults: {format: 'json'}
patch '/rooms/:id/recordings/:record_id', to: 'bbb#update_recordings', defaults: {format: 'json'}
delete '/rooms/:id/recordings/:record_id', to: 'bbb#delete_recordings', defaults: {format: 'json'}
root to: 'landing#index', :resource => "meetings" root to: 'landing#index', :resource => "meetings"
end end

View File

@ -0,0 +1,7 @@
require 'test_helper'
class RecordingUpdatesJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end