diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 34532f70..d780648c 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,6 +12,8 @@ // //= require jquery2 //= require jquery-ui +//= require dataTables/jquery.dataTables +//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap //= require bootstrap-sprockets //= require turbolinks //= require_self diff --git a/app/assets/javascripts/channels/recording_update.js b/app/assets/javascripts/channels/recording_update.js new file mode 100644 index 00000000..aae65063 --- /dev/null +++ b/app/assets/javascripts/channels/recording_update.js @@ -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); diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js index 1e9e48e2..86bab1b1 100644 --- a/app/assets/javascripts/landing.js +++ b/app/assets/javascripts/landing.js @@ -1,4 +1,6 @@ (function() { + var recordingsTable = null; + var waitForModerator = function(url) { $.get(url + "/wait", function(html) { $(".center-panel-wrapper").html(html); @@ -72,8 +74,112 @@ window.location.hostname + meetingURL.data('path'); 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 += ''+data[i].type+' '; + } + } + 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 ' ' + + ''; + } + 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() { init(); if ($("body[data-controller=landing]").get(0)) { diff --git a/app/assets/javascripts/shared.js b/app/assets/javascripts/shared.js index edf84fc9..a2488f93 100644 --- a/app/assets/javascripts/shared.js +++ b/app/assets/javascripts/shared.js @@ -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; class Meeting { constructor(url, name) { diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6f2b6eff..ca8885fa 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -11,7 +11,7 @@ * It is generally better to create a new file per style scope. * *= require jquery-ui - *= require dataTables/jquery.dataTables + *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap *= require_tree . *= require_self */ diff --git a/app/assets/stylesheets/landing.scss b/app/assets/stylesheets/landing.scss index f33d6f8d..e2d46925 100644 --- a/app/assets/stylesheets/landing.scss +++ b/app/assets/stylesheets/landing.scss @@ -1,3 +1,9 @@ // Place all the styles related to the landing controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ + +.rooms { + .table-wrapper { + padding: 40px 50px 10px 50px; + } +} diff --git a/app/channels/recording_updates_channel.rb b/app/channels/recording_updates_channel.rb new file mode 100644 index 00000000..ff814506 --- /dev/null +++ b/app/channels/recording_updates_channel.rb @@ -0,0 +1,5 @@ +class RecordingUpdatesChannel < ApplicationCable::Channel + def subscribed + stream_from "#{params[:username]}_recording_updates_channel" + end +end diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index cc35c019..bc65542e 100644 --- a/app/controllers/bbb_controller.rb +++ b/app/controllers/bbb_controller.rb @@ -1,17 +1,19 @@ class BbbController < ApplicationController + include BbbApi + + before_action :authorize_owner_recording, only: [:update_recordings, :delete_recordings] # GET /:resource/:id/join def join - if ( params[:id].blank? ) - render_response("missing_parameter", "meeting token was not included", :bad_request) - elsif ( params[:name].blank? ) - render_response("missing_parameter", "user name was not included", :bad_request) + if params[:name].blank? + render_bbb_response("missing_parameter", "user name was not included", :unprocessable_entity) else user = User.find_by username: params[:id] options = if user { wait_for_moderator: true, + meeting_recorded: true, user_is_moderator: current_user == user } else @@ -19,28 +21,72 @@ class BbbController < ApplicationController end 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[:name], options ) - if bbb_res[:returncode] && current_user && current_user == user ActionCable.server.broadcast "moderator_#{user.username}_join_channel", moderator: "joined" 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 + # 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 - def render_response(messageKey, message, status, response={}) - @messageKey = messageKey - @message = message - @status = status + + def authorize_owner_recording + user = User.find_by username: params[:id] + 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 - render status: @status + render status: @status && return end end diff --git a/app/jobs/recording_updates_job.rb b/app/jobs/recording_updates_job.rb new file mode 100644 index 00000000..7d884756 --- /dev/null +++ b/app/jobs/recording_updates_job.rb @@ -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 diff --git a/app/helpers/bbb_helper.rb b/app/lib/bbb_api.rb similarity index 65% rename from app/helpers/bbb_helper.rb rename to app/lib/bbb_api.rb index dce6942d..61aaa257 100644 --- a/app/helpers/bbb_helper.rb +++ b/app/lib/bbb_api.rb @@ -1,4 +1,4 @@ -module BbbHelper +module BbbApi def bbb_endpoint Rails.application.secrets[:bbb_endpoint] end @@ -7,6 +7,10 @@ module BbbHelper Rails.application.secrets[:bbb_secret] end + def bbb + @bbb ||= BigBlueButton::BigBlueButtonApi.new(bbb_endpoint + "api", bbb_secret, "0.8", true) + end + def random_password(length) o = [('a'..'z'), ('A'..'Z')].map { |i| i.to_a }.flatten password = (0...length).map { o[rand(o.length)] }.join @@ -19,7 +23,6 @@ module BbbHelper options[:wait_for_moderator] ||= false options[:meeting_logout_url] ||= nil - bbb ||= BigBlueButton::BigBlueButtonApi.new(bbb_endpoint + "api", bbb_secret, "0.8", true) if !bbb return call_invalid_res else @@ -41,8 +44,7 @@ module BbbHelper logout_url = options[:meeting_logout_url] || "#{request.base_url}" moderator_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 bbb.create_meeting(meeting_token, meeting_id, meeting_options) @@ -65,6 +67,40 @@ module BbbHelper 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) { returncode: true, @@ -94,4 +130,13 @@ module BbbHelper status: :internal_server_error } end + + def bbb_exception_res(exc) + { + returncode: false, + messageKey: 'BBB'+exc.key.capitalize.underscore, + message: exc.message, + status: :internal_server_error + } + end end diff --git a/app/views/bbb/_bbb.jbuilder b/app/views/bbb/_bbb.jbuilder new file mode 100644 index 00000000..8d70ae36 --- /dev/null +++ b/app/views/bbb/_bbb.jbuilder @@ -0,0 +1,3 @@ +json.messageKey messageKey +json.message message +json.status status diff --git a/app/views/bbb/delete_recordings.jbuilder b/app/views/bbb/delete_recordings.jbuilder new file mode 100644 index 00000000..0f12a451 --- /dev/null +++ b/app/views/bbb/delete_recordings.jbuilder @@ -0,0 +1 @@ +json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status diff --git a/app/views/bbb/join.jbuilder b/app/views/bbb/join.jbuilder index cc60f524..59bbed24 100644 --- a/app/views/bbb/join.jbuilder +++ b/app/views/bbb/join.jbuilder @@ -1,7 +1,5 @@ -json.messageKey @messageKey -json.message @message -json.status @status -if @response +json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status +unless @response.blank? json.response do json.join_url(@response[:join_url]) if @response[:join_url] end diff --git a/app/views/bbb/recordings.jbuilder b/app/views/bbb/recordings.jbuilder new file mode 100644 index 00000000..23781b77 --- /dev/null +++ b/app/views/bbb/recordings.jbuilder @@ -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 diff --git a/app/views/bbb/update_recordings.jbuilder b/app/views/bbb/update_recordings.jbuilder new file mode 100644 index 00000000..0f12a451 --- /dev/null +++ b/app/views/bbb/update_recordings.jbuilder @@ -0,0 +1 @@ +json.partial! 'bbb', messageKey: @messageKey, message: @message, status: @status diff --git a/app/views/landing/meetings.html.erb b/app/views/landing/meetings.html.erb index 9bef80de..271c96d9 100644 --- a/app/views/landing/meetings.html.erb +++ b/app/views/landing/meetings.html.erb @@ -15,7 +15,7 @@ <% end %> -
+
<%= render 'shared/title', title: 'Start A New Session' %> diff --git a/app/views/landing/rooms.html.erb b/app/views/landing/rooms.html.erb index 7900b70f..810489a4 100644 --- a/app/views/landing/rooms.html.erb +++ b/app/views/landing/rooms.html.erb @@ -16,7 +16,7 @@
<% end %> -
+
<%= render 'shared/title', title: page_title %> @@ -34,5 +34,9 @@ <% end %>
+
+

Past Recordings

+
+
diff --git a/config/environments/development.rb b/config/environments/development.rb index b91c71aa..9616a76b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -31,11 +31,13 @@ Rails.application.configure do config.action_mailer.perform_caching = false + config.active_job.queue_adapter = :async + # action cable socket URI config.action_cable.url = "ws://localhost/cable" # 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. config.active_support.deprecation = :log diff --git a/config/environments/production.rb b/config/environments/production.rb index 6956dec8..6f2ca454 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -55,6 +55,8 @@ Rails.application.configure do # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "greenlight_#{Rails.env}" + config.active_job.queue_adapter = :async + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. diff --git a/config/routes.rb b/config/routes.rb index 188c6eb7..30280265 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 # 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/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 '/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" end diff --git a/test/jobs/recording_updates_job_test.rb b/test/jobs/recording_updates_job_test.rb new file mode 100644 index 00000000..62dd0969 --- /dev/null +++ b/test/jobs/recording_updates_job_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class RecordingUpdatesJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end