diff --git a/Gemfile b/Gemfile index 9b584f9b..28799e49 100644 --- a/Gemfile +++ b/Gemfile @@ -61,5 +61,6 @@ gem 'bootstrap-social-rails', '~> 4.12' gem 'font-awesome-rails' gem 'jquery-ui-rails' gem 'jquery-datatables-rails', '~> 3.4.0' +gem 'rails-timeago', '~> 2.0' gem 'http_accept_language' diff --git a/Gemfile.lock b/Gemfile.lock index ba66b8b8..0857cd9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,6 +152,9 @@ GEM nokogiri (~> 1.6.0) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails-timeago (2.15.0) + actionpack (>= 3.1) + activesupport (>= 3.1) railties (5.0.0.1) actionpack (= 5.0.0.1) activesupport (= 5.0.0.1) @@ -224,6 +227,7 @@ DEPENDENCIES omniauth-twitter (= 1.2.1) puma (~> 3.0) rails (~> 5.0.0, >= 5.0.0.1) + rails-timeago (~> 2.0) sass-rails (~> 5.0) spring spring-watcher-listen (~> 2.0.0) @@ -237,4 +241,4 @@ RUBY VERSION ruby 2.3.1p112 BUNDLED WITH - 1.13.5 + 1.13.6 diff --git a/app/assets/images/bg_personal_room.jpg b/app/assets/images/bg_personal_room.jpg new file mode 100644 index 00000000..0d0dfa8f Binary files /dev/null and b/app/assets/images/bg_personal_room.jpg differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3f386151..f087ae3b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -18,6 +18,7 @@ //= require jquery-ui //= require dataTables/jquery.dataTables //= require dataTables/bootstrap/3/jquery.dataTables.bootstrap +//= require rails-timeago-all //= require bootstrap-sprockets //= require turbolinks //= require_self diff --git a/app/assets/javascripts/channels/meeting_updates.js b/app/assets/javascripts/channels/meeting_updates.js index c0ec9c4e..87f6af15 100644 --- a/app/assets/javascripts/channels/meeting_updates.js +++ b/app/assets/javascripts/channels/meeting_updates.js @@ -21,7 +21,7 @@ $(".center-panel-wrapper").html(html); displayRoomURL(); }); - } + }; var initRooms = function() { App.messages = App.cable.subscriptions.create({ @@ -43,6 +43,13 @@ } else if (data.action === 'meeting_ended') { sessionStatusRefresh($('.meeting-url').val()); showAlert(I18n.meeting_ended, 4000); + } else if (data.action === 'user_waiting') { + // show a browser notification only to the owner + if (GreenLight.user.roomOwner) { + showNotification(I18n.user_waiting_title, { + body: I18n.user_waiting_body.replace(/%{user}/, data.user) + }); + } } } }); diff --git a/app/assets/javascripts/channels/recording_update.js b/app/assets/javascripts/channels/recording_update.js index 500dee66..2b517ccd 100644 --- a/app/assets/javascripts/channels/recording_update.js +++ b/app/assets/javascripts/channels/recording_update.js @@ -23,22 +23,35 @@ }, { received: function(data) { + var recordings = Recordings.getInstance(); - var table = recordings.table.api() - var row = table.row("#"+data.record_id); + var table = recordings.table.api(); + var row = table.row("#"+data.id); + if (data.action === 'update') { var rowData = row.data(); - rowData.published = data.published - table.row("#"+data.record_id).data(rowData); + + rowData.published = data.published; + rowData.listed = data.listed; + table.row("#"+data.id).data(rowData); recordings.draw(); - var published = (data.published) ? 'published' : 'unpublished'; - showAlert(I18n['recording_'+published], 4000); + 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/landing.js b/app/assets/javascripts/landing.js index ce201d3b..da819eb0 100644 --- a/app/assets/javascripts/landing.js +++ b/app/assets/javascripts/landing.js @@ -100,6 +100,15 @@ .tooltip('fixTitle'); }); + // button used to send invitations to the meeting (i.e. "mailto:" link) + $('.center-panel-wrapper').on('click', '.meeting-invite', function (event) { + var meetingURL = Meeting.getInstance().getURL(); + var subject = $(this).data("invite-subject"); + var body = $(this).data("invite-body").replace("&&URL&&", meetingURL); + var mailto = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body); + window.open(mailto); + }); + $('.center-panel-wrapper').on('mouseleave', '.meeting-url-copy', function (event, msg) { $(this).blur(); }); @@ -120,7 +129,7 @@ selector: '.has-tooltip', container: 'body' }; - $(document).tooltip(options) + $(document).tooltip(options); var options = { selector: '.bottom-tooltip', container: 'body', @@ -151,7 +160,7 @@ var joinedMeetings = localStorage.getItem('joinedMeetings'); if (joinedMeetings && joinedMeetings.length > 0) { joinedMeetings = joinedMeetings.split(','); - $('.center-panel-wrapper .panel-footer').removeClass('hidden') + $('.center-panel-wrapper .panel-footer').removeClass('hidden'); for (var i = joinedMeetings.length - 1; i >= 0; i--) { $('ul.previously-joined').append('
  • '+joinedMeetings[i]+'
  • '); diff --git a/app/assets/javascripts/recordings.coffee b/app/assets/javascripts/recordings.coffee index 00abec2f..a4c69396 100644 --- a/app/assets/javascripts/recordings.coffee +++ b/app/assets/javascripts/recordings.coffee @@ -37,7 +37,7 @@ class @Recordings { data: "previews", orderable: false }, { data: "duration", orderable: false }, { data: "playbacks", orderable: false }, - { data: "published", visible: false }, + { data: "listed", visible: false }, { data: "id", orderable: false } ], columnDefs: [ @@ -45,10 +45,13 @@ class @Recordings targets: 0, render: (data, type, row) -> if type == 'display' - return new Date(data) + date = new Date(data) + title = date .toLocaleString($('html').attr('lang'), {month: 'long', day: 'numeric', year: 'numeric', hour12: 'true', hour: '2-digit', minute: '2-digit'}) + timeago = '' + return title+'('+timeago+')' return data }, { @@ -78,18 +81,18 @@ class @Recordings render: (data, type, row) -> if type == 'display' roomName = Meeting.getInstance().getId() - published = row.published - publishText = if published then 'unpublish' else 'publish' recordingActions = $('.hidden-elements').find('.recording-actions') - recordingActions.find('.recording-update > i.default') - .removeClass(PUBLISHED_CLASSES.join(' ')) - .addClass(getPublishClass(published)) - recordingActions.find('.recording-update > i.hover') - .removeClass(PUBLISHED_CLASSES.join(' ')) - .addClass(getPublishClass(!published)) - recordingActions.find('.recording-update') - .attr('data-published', published) - .attr('title', I18n[publishText+'_recording']) + classes = ['recording-unpublished', 'recording-unlisted', 'recording-published'] + if row.published + if row.listed + cls = classes[2] + else + cls = classes[1] + else + cls = classes[0] + trigger = recordingActions.find('.recording-update-trigger') + trigger.removeClass(classes.join(' ')) + trigger.addClass(cls) return recordingActions.html() return data } @@ -106,17 +109,33 @@ class @Recordings @getTable().api().clear().draw().destroy() # enable popovers + # can't use trigger:'focus' because it doesn't work will with buttons inside + # the popover options = { selector: '.has-popover', html: true, - trigger: 'focus', + trigger: 'click', title: -> - return I18n.are_you_sure; + return $(this).data("popover-title"); content: -> - return $(".delete-popover-body").html() + bodySelector = $(this).data("popover-body") + return $(bodySelector).html() } $('#recordings').popover(options) + # close popovers manually when clicking outside of them or in buttons + # with [data-dismiss="popover"] + # careful to hide only the open popover and not all of them, otherwise they won't reopen + $('body').on 'click', (e) -> + $('.has-popover').each -> + if !$(this).is(e.target) and $(this).has(e.target).length == 0 and $('.popover.in').has(e.target).length == 0 + if $(this).next(".popover.in").length > 0 + $(this).popover('hide') + $(document).on 'click', '[data-dismiss="popover"]', (e) -> + $('.has-popover').each -> + if $(this).next(".popover.in").length > 0 + $(this).popover('hide') + # Gets the current instance or creates a new one @getInstance: -> if _recordingsInstance && Recordings.initialized() @@ -153,24 +172,32 @@ class @Recordings # setup click handlers for the action buttons setupActionHandlers: -> table_api = this.table.api() + @getTable().on 'click', '.recording-update', (event) -> btn = $(this) row = table_api.row($(this).closest('tr')).data() url = $('.meeting-url').val() id = row.id - published = btn.data('published') + + published = btn.data('visibility') == "unlisted" || + btn.data('visibility') == "published" + 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()} + data: data }).done((data) -> ).fail((data) -> btn.prop('disabled', false) ) - this.table.on 'click', '.recording-delete', (event) -> + @getTable().on 'click', '.recording-delete', (event) -> btn = $(this) row = table_api.row($(this).closest('tr')).data() url = $('.meeting-url').val() @@ -185,6 +212,9 @@ class @Recordings btn.prop('disabled', false) ) + @getTable().on 'draw.dt', (event) -> + $('time[data-time-ago]').timeago(); + getTable: -> @table diff --git a/app/assets/javascripts/shared.js b/app/assets/javascripts/shared.js.erb similarity index 71% rename from app/assets/javascripts/shared.js rename to app/assets/javascripts/shared.js.erb index 52553d87..dba6461e 100644 --- a/app/assets/javascripts/shared.js +++ b/app/assets/javascripts/shared.js.erb @@ -20,12 +20,6 @@ $.ajaxSetup({ } }); -var PUBLISHED_CLASSES = ['fa-eye-slash', 'fa-eye'] - -var getPublishClass = function(published) { - return PUBLISHED_CLASSES[+published]; -} - var loopJoin = function() { var jqxhr = Meeting.getInstance().getJoinMeetingResponse(); jqxhr.done(function(data) { @@ -38,8 +32,9 @@ var loopJoin = function() { jqxhr.fail(function(xhr, status, error) { console.info("meeting join failed"); }); -} +}; +var alertTimeout = null; var showAlert = function(html, timeout_delay) { if (!html) { return; @@ -49,12 +44,34 @@ 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()); -} +}; + +var showNotification = function(title, options) { + if (Notification.permission === "granted") { + var icon = '<%= asset_path("bbb-logo.png") %>'; + options = $.extend(options, { + icon: icon + }); + var notification = new Notification(title, options); + notification.onclick = function() { + window.focus(); + }; + } +}; + +// For now there are notifications only for users signed in and when they +// are in their room's page +$(document).on("turbolinks:load", function() { + if (GreenLight.user.roomOwner) { + Notification.requestPermission(); + } +}); diff --git a/app/assets/stylesheets/landing.scss b/app/assets/stylesheets/landing.scss index b53da801..06900393 100644 --- a/app/assets/stylesheets/landing.scss +++ b/app/assets/stylesheets/landing.scss @@ -37,16 +37,11 @@ .dataTables_empty { text-align: center; } - .recording-update:hover > { - .default { - display: none; - } - .hover { - display: inline-block; - } - } - .fa.hover { - display: none; + .timeago { + margin-left: 5px; + font-size: 13px; + cursor: pointer; + color: #999; } } } @@ -69,3 +64,34 @@ z-index: 1; } } + +.meeting-url-group { + position: relative; + + .generate-link { + position: absolute; + right: 12px; + top: 10px; + z-index: 9; + color: #999; + cursor: pointer; + + &:hover { + color: #000; + } + } +} + +.recording-update-trigger { + &.recording-unpublished { + color: red; + } + + &.recording-unlisted { + color: #e3a91e; + } + + &.recording-published { + color: green; + } +} diff --git a/app/assets/stylesheets/shared.scss b/app/assets/stylesheets/shared.scss index b11c4df6..e6d8d0ca 100644 --- a/app/assets/stylesheets/shared.scss +++ b/app/assets/stylesheets/shared.scss @@ -35,6 +35,10 @@ html, body { background-repeat: no-repeat; } +body[data-controller=landing][data-action=rooms].app-background { + background-image: asset-url('bg_personal_room'); +} + #alerts { position: absolute; top: 10px; @@ -106,3 +110,7 @@ html, body { text-align: center; } } + +.popover { + max-width: none; +} diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index 84fff310..485402fe 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], @@ -47,14 +52,33 @@ class BbbController < ApplicationController options ) + # the user can join the meeting if bbb_res[:returncode] && current_user && current_user == user JoinMeetingJob.perform_later(user.encrypted_id) + + # user will be waiting for a moderator + else + NotifyUserWaitingJob.perform_later(user.encrypted_id, params[:name]) end render_bbb_response bbb_res, bbb_res[:response] 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! @@ -76,9 +100,11 @@ class BbbController < ApplicationController # PATCH /rooms/:id/recordings/:record_id def update_recordings - bbb_res = bbb_update_recordings(params[:record_id], params[:published] == 'true') + published = params[:published] == 'true' + metadata = params.select{ |k, v| k.match(/^meta_/) } + bbb_res = bbb_update_recordings(params[:record_id], published, metadata) if bbb_res[:returncode] - RecordingUpdatesJob.perform_later(@user.encrypted_id, params[:record_id], bbb_res[:published]) + RecordingUpdatesJob.perform_later(@user.encrypted_id, params[:record_id]) end render_bbb_response bbb_res end @@ -128,4 +154,80 @@ 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) + eventName = (event.present? && event['header'].present?) ? event['header']['name'] : nil + + # a recording is ready + if eventName == "publish_ended" + if event['payload'] && event['payload']['metadata'] && event['payload']['meeting_id'] + 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 in the event 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)) + + # send an email to the owner of this recording, if defined + if Rails.configuration.mail_notifications + owner = User.find_by(encrypted_id: token) + RecordingReadyEmailJob.perform_later(owner) if owner.present? + end + + # TODO: remove the webhook now that the meeting and recording are done + # remove only if the meeting is not running, otherwise the hook is needed + # if Rails.configuration.use_webhooks + # webhook_remove("#{base_url}/callback") + # end + else + logger.error "Bad format for event #{event}, won't process" + end + else + logger.info "Callback event will not be treated. Event name: #{eventName}" + 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/helpers/users_helper.rb b/app/helpers/users_helper.rb index 2eba122d..19758ad6 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -15,4 +15,8 @@ # with BigBlueButton; if not, see . module UsersHelper + def is_room_owner + token = current_user ? current_user.encrypted_id : nil + token.present? && params[:id].present? && token == params[:id] + end end diff --git a/app/jobs/notify_user_waiting_job.rb b/app/jobs/notify_user_waiting_job.rb new file mode 100644 index 00000000..0211e42a --- /dev/null +++ b/app/jobs/notify_user_waiting_job.rb @@ -0,0 +1,24 @@ +# 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 NotifyUserWaitingJob < ApplicationJob + queue_as :default + + def perform(room, user) + ActionCable.server.broadcast "#{room}_meeting_updates_channel", + { action: 'user_waiting', user: user } + 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_ready_email_job.rb b/app/jobs/recording_ready_email_job.rb new file mode 100644 index 00000000..465f0f38 --- /dev/null +++ b/app/jobs/recording_ready_email_job.rb @@ -0,0 +1,25 @@ +# 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 RecordingReadyEmailJob < ApplicationJob + queue_as :default + + def perform(user) + if user.email.present? + NotificationMailer.recording_ready_email(user).deliver + end + end +end diff --git a/app/jobs/recording_updates_job.rb b/app/jobs/recording_updates_job.rb index 5b9903ea..9dafbb2f 100644 --- a/app/jobs/recording_updates_job.rb +++ b/app/jobs/recording_updates_job.rb @@ -19,22 +19,13 @@ class RecordingUpdatesJob < ApplicationJob 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", - action: 'update', - record_id: record_id, - published: bbb_res[:recordings].first[:published] - break - end - sleep sleep_time - sleep_time = sleep_time * 2 - tries += 1 - end + def perform(room, record_id) + bbb_res = bbb_get_recordings(nil, record_id) + recording = bbb_res[:recordings].first + ActionCable.server.broadcast "#{room}_recording_updates_channel", + action: 'update', + id: record_id, + published: recording[:published], + listed: bbb_is_recording_listed(recording) end end diff --git a/app/lib/bbb_api.rb b/app/lib/bbb_api.rb index 090a0f3d..35f3c450 100644 --- a/app/lib/bbb_api.rb +++ b/app/lib/bbb_api.rb @@ -15,6 +15,10 @@ # with BigBlueButton; if not, see . module BbbApi + META_LISTED = "gl-listed" + META_TOKEN = "gl-token" + META_HOOK_URL = "gl-webhooks-callback-url" + def bbb_endpoint Rails.configuration.bigbluebutton_endpoint || '' end @@ -51,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 @@ -65,7 +69,22 @@ module BbbApi 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, + "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] + + if Rails.configuration.use_webhooks + webhook_register(options[:hook_url], meeting_id) + end + # Create the meeting bbb.create_meeting(options[:meeting_name], meeting_id, meeting_options) @@ -101,7 +120,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 @@ -148,6 +167,8 @@ module BbbApi else [] end + + recording[:listed] = bbb_is_recording_listed(recording) end res end @@ -168,8 +189,11 @@ module BbbApi response_data = bbb_exception_res exc end - def bbb_update_recordings(id, published) + def bbb_update_recordings(id, published, metadata) bbb_safe_execute :publish_recordings, id, published + + params = { recordID: id }.merge(metadata) + bbb_safe_execute :send_api_request, "updateRecordings", params end def bbb_delete_recordings(id) @@ -191,6 +215,67 @@ module BbbApi response_data end + def bbb_is_recording_listed(recording) + !recording[:metadata].blank? && + 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 webhook_register(url, meeting_id=nil) + params = { callbackURL: url } + params.merge!({ meetingID: meeting_id }) if meeting_id.present? + bbb_safe_execute :send_api_request, "hooks/create", params + end + + def webhook_remove(url) + res = bbb_safe_execute :send_api_request, "hooks/list" + if res && res[:hooks] && res[:hooks][:hook] + res[:hooks][:hook] = [res[:hooks][:hook]] unless res[:hooks][:hook].is_a?(Array) + hook = res[:hooks][:hook].select{ |h| + h[:callbackURL] == url + }.first + if hook.present? + params = { hookID: hook[:hookID] } + bbb_safe_execute :send_api_request, "hooks/destroy", params + end + end + end + def success_join_res(join_url) { returncode: true, diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb new file mode 100644 index 00000000..9c73a0bd --- /dev/null +++ b/app/mailers/notification_mailer.rb @@ -0,0 +1,25 @@ +# 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 NotificationMailer < ActionMailer::Base + default from: Rails.configuration.smtp_from + + def recording_ready_email(user) + @user = user + @room_url = resource_url(resource: "rooms", id: user.encrypted_id) + mail(to: user.email, subject: t('.subject')) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c1a77b10..01a16ce4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,6 +21,7 @@ class User < ApplicationRecord def self.from_omniauth(auth_hash) user = find_or_initialize_by(uid: auth_hash['uid'], provider: auth_hash['provider']) user.username = self.send("#{auth_hash['provider']}_username", auth_hash) rescue nil + user.email = self.send("#{auth_hash['provider']}_email", auth_hash) rescue nil user.name = auth_hash['info']['name'] user.save! user @@ -30,10 +31,18 @@ class User < ApplicationRecord auth_hash['info']['nickname'] end + def self.twitter_email(auth_hash) + auth_hash['info']['email'] + end + def self.google_username(auth_hash) auth_hash['info']['email'].split('@').first end + def self.google_email(auth_hash) + auth_hash['info']['email'] + end + def room_url "/rooms/#{encrypted_id}" end diff --git a/app/views/bbb/recordings.jbuilder b/app/views/bbb/recordings.jbuilder index e1174e8b..b7f7d7cc 100644 --- a/app/views/bbb/recordings.jbuilder +++ b/app/views/bbb/recordings.jbuilder @@ -24,6 +24,7 @@ json.recordings do json.end_time recording[:endTime] json.published recording[:published] json.length recording[:length] + json.listed recording[:listed] json.previews do json.array!(recording[:previews]) do |preview| json.partial! 'preview', preview: preview diff --git a/app/views/landing/rooms.html.erb b/app/views/landing/rooms.html.erb index aa350b17..6edf3f77 100644 --- a/app/views/landing/rooms.html.erb +++ b/app/views/landing/rooms.html.erb @@ -43,13 +43,24 @@ with BigBlueButton; if not, see .