Merge pull request #74 from daronco/summit-9

Several improvements made during the Summit IX
This commit is contained in:
Fred Dixon 2016-12-11 11:35:36 -05:00 committed by GitHub
commit 520aacf302
34 changed files with 763 additions and 89 deletions

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

View File

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

View File

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

View File

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

View File

@ -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('<li><a href="/meetings/'+joinedMeetings[i]+'">'+joinedMeetings[i]+'</a></li>');

View File

@ -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 = '<time datetime="'+date.toISOString()+'" data-time-ago="'+date.toISOString()+'">'+date.toISOString()+'</time>'
return title+'<span class="timeago">('+timeago+')</span>'
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

View File

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

View File

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

View File

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

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],
@ -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(/&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

@ -15,4 +15,8 @@
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
module UsersHelper
def is_room_owner
token = current_user ? current_user.encrypted_id : nil
token.present? && params[:id].present? && token == params[:id]
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
class NotifyUserWaitingJob < ApplicationJob
queue_as :default
def perform(room, user)
ActionCable.server.broadcast "#{room}_meeting_updates_channel",
{ action: 'user_waiting', user: user }
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

@ -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 <http://www.gnu.org/licenses/>.
class RecordingReadyEmailJob < ApplicationJob
queue_as :default
def perform(user)
if user.email.present?
NotificationMailer.recording_ready_email(user).deliver
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,13 +43,24 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<div hidden class="hidden-elements">
<div class="delete-popover-body">
<button type="button" class="btn btn-danger recording-delete">
<button type="button" class="btn btn-danger recording-delete" data-dismiss="popover">
<%= t('yes') %>
</button>
<button type="button" class="btn btn-default">
<button type="button" class="btn btn-default" data-dismiss="popover">
<%= t('no') %>
</button>
</div>
<div class="recording-visibility-popover">
<button type="button" class="btn btn-default btn-danger recording-update" data-visibility="unpublished">
<%= t('unpublished') %>
</button>
<button type="button" class="btn btn-default btn-warning recording-update" data-visibility="unlisted">
<%= t('unlisted') %>
</button>
<button type="button" class="btn btn-default btn-success recording-update" data-visibility="published">
<%= t('published') %>
</button>
</div>
<div class="alert-template">
<div class="alert alert-success alert-dismissible fade in" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
@ -59,12 +70,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div class="recording-actions">
<button type="button" class="btn btn-default recording-update bottom-tooltip" data-published="">
<i class="fa default" aria-hidden="true"></i>
<i class="fa hover" aria-hidden="true"></i>
<button type="button" class="btn btn-default has-popover recording-update-trigger"
data-placement="left" data-popover-body=".recording-visibility-popover"
data-popover-title="<%= t('change_recording_visibility') %>">
<i class="fa fa-eye" aria-hidden="true"></i>
</button>
<a tabindex="0" role="button" class="btn btn-default has-popover delete-tooltip"
data-placement="top">
data-placement="top" data-popover-body=".delete-popover-body"
data-popover-title="<%= t('are_you_sure') %>">
<i class="fa fa-trash-o" aria-hidden="true"></i>
</a>
</div>

View File

@ -77,6 +77,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</body>
</html>
<!-- Global javascript variables and helpers -->
<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 %>";
window.GreenLight.user = {};
window.GreenLight.user.roomOwner = <%= is_room_owner %>;
</script>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<p><%= t('.hi', name: @user.name) %></p>
<p><%= t('.phrase1') %></p>
<p><%= t('.phrase2', url: @room_url) %></p>
</body>
</html>

View File

@ -15,15 +15,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<div <%= "hidden" if hidden %> class="meeting-url-wrapper">
<div class="input-group">
<input type="text" class="form-control meeting-url"/>
<span class="input-group-btn">
<div class="meeting-url-group">
<input type="text" class="form-control meeting-url"/>
<% if params[:action] == 'index' %>
<button type="button" class="btn btn-default generate-link has-tooltip"
title="<%= t('url_refresh_hint') %>"
>
<i class="fa fa-refresh" aria-hidden="true"></i>
</button>
<i class="fa fa-refresh generate-link has-tooltip" aria-hidden="true"
title="<%= t('url_refresh_hint') %>"
></i>
<% end %>
</div>
<span class="input-group-btn">
<button type="button" class="btn btn-default meeting-url-copy has-tooltip"
title="<%= t('url_copy_explanation') %>"
data-copied-hint="<%= t('copied') %>"
@ -32,6 +34,22 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
>
<i class="fa fa-paperclip" aria-hidden="true"></i>
</button>
<% if current_user %>
<% body = t('meeting_invite.signed_in.body', user: current_user.name) %>
<% subject = t('meeting_invite.signed_in.subject', user: current_user.name) %>
<% else %>
<% body = t('meeting_invite.not_signed_in.body') %>
<% subject = t('meeting_invite.not_signed_in.subject') %>
<% end %>
<button type="button" class="btn btn-default meeting-invite has-tooltip"
title="<%= t('meeting_invite.explanation') %>"
data-invite-body="<%= body %>"
data-invite-subject="<%= subject %>"
>
<i class="fa fa-envelope-o" aria-hidden="true"></i>
</button>
</span>
</div>
</div>

View File

@ -37,5 +37,39 @@ module Greenlight
# BigBlueButton
config.bigbluebutton_endpoint = ENV['BIGBLUEBUTTON_ENDPOINT']
config.bigbluebutton_secret = ENV['BIGBLUEBUTTON_SECRET']
config.use_webhooks = ENV['GREENLIGHT_USE_WEBHOOKS'] == "true"
config.mail_notifications = ENV['GREENLIGHT_MAIL_NOTIFICATIONS'] == "true"
# SMTP and action mailer
if config.mail_notifications
config.smtp_from = ENV['SMTP_FROM']
config.smtp_server = ENV['SMTP_SERVER']
config.smtp_domain = ENV['SMTP_DOMAIN']
config.smtp_port = ENV['SMTP_PORT'] || 587
config.smtp_username = ENV['SMTP_USERNAME']
config.smtp_password = ENV['SMTP_PASSWORD']
config.smtp_auth = ENV['SMTP_AUTH'] || "login"
config.smtp_starttls_auto = ENV['SMTP_STARTTLS_AUTO'].nil? ? true : ENV['SMTP_STARTTLS_AUTO']
config.smtp_tls = ENV['SMTP_TLS'].nil? ? false : ENV['SMTP_TLS']
config.action_mailer.default_url_options = { host: ENV['GREENLIGHT_DOMAIN'] }
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.smtp_settings = {
address: config.smtp_server,
domain: config.smtp_domain,
port: config.smtp_port,
user_name: config.smtp_username,
password: config.smtp_password,
authentication: config.smtp_auth,
enable_starttls_auto: config.smtp_starttls_auto,
tls: config.smtp_tls
}
config.action_mailer.default_options = {
from: config.smtp_from
}
end
end
end

View File

@ -38,6 +38,8 @@
en-US:
actions: Actions
are_you: Are you %{name}?
are_you_sure: Are you sure?
change_recording_visibility: "Change visibility to:"
client:
are_you_sure: Are you sure?
delete_recording: Delete recording
@ -46,10 +48,14 @@ 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
recording_unpublished: Recording was unpublished
unpublish_recording: Hide recording
user_waiting_body: "%{user} is waiting to join your room!"
user_waiting_title: A user is waiting
copied: Copied
copy_error: Use Ctrl-c to copy
create_session: Create a Session
@ -67,18 +73,35 @@ en-US:
join_session_user: Join %{name} session
login: login
logout: logout
meeting_invite:
explanation: Send an email with an invitation to this meeting
not_signed_in:
body: "You have been invited to a meeting.\n\nPlease open the following page in your web browser: &&URL&&"
subject: "Invitation to a meeting"
signed_in:
body: "You have been invited by %{user} to a meeting.\n\nPlease open the following page in your web browser: &&URL&&"
subject: "%{user} invited you to a meeting"
my_room: my room
no: No
notification_mailer:
recording_ready_email:
hi: "Hi, %{name}"
phrase1: "One of your recordings has just been made available."
phrase2: "Access the following website to view it and publish to other users: %{url}"
subject: "Your recording is ready!"
oauth_signup: Signup for customized sessions
past_recordings: Past Recordings
footer_html: "%{greenlight_link} build %{version}, Powered by %{bbb_link}"
presentation: Presentation
previously_joined_meetings: Previously Joined Sessions
published: Published
return_to_room: Return to your personal room
session_url_explanation: The session will be taking place using the following URL
start: Start
your_personal_room: Your Personal Room
thumbnails: Thumbnails
unlisted: Unlisted
unpublished: Unpublished
url_copy_explanation: Copy this URL to invite others to the meeting
url_refresh_hint: Generate a new meeting URL
video: Video

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

View File

@ -0,0 +1,22 @@
# 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 AddEmailToUser < ActiveRecord::Migration[5.0]
def change
add_column :users, :email, :string
add_index :users, :email, unique: true
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161108224701) do
ActiveRecord::Schema.define(version: 20161208185458) do
create_table "users", force: :cascade do |t|
t.string "provider", null: false
@ -20,6 +20,8 @@ ActiveRecord::Schema.define(version: 20161108224701) do
t.datetime "updated_at", null: false
t.string "username"
t.string "encrypted_id", null: false
t.string "email"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["encrypted_id"], name: "index_users_on_encrypted_id", unique: true
t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true
t.index ["provider"], name: "index_users_on_provider"

View File

@ -7,6 +7,33 @@ RAILS_ENV=development
BIGBLUEBUTTON_ENDPOINT=http://test-install.blindsidenetworks.com/bigbluebutton/
BIGBLUEBUTTON_SECRET=8cd8ef52e8e101574e400365b55e11a6
# If "true", GreenLight will register a webhook callback for each meeting
# created. This callback is called for all events that happen in the meeting,
# including the processing of its recording. These events are used to update
# the web page dynamically as things happen in the server.
# If not "true", the application will add a metadata to the meetings with this same
# callback URL. Scripts can then be added to BigBlueButton to call this callback
# URL and send specific events to GreenLight (e.g. a post publish script to warn
# the application when recordings are done).
GREENLIGHT_USE_WEBHOOKS=false
# The web site domain, needed for emails mostly
GREENLIGHT_DOMAIN=localhost
# Enable email notifications
GREENLIGHT_MAIL_NOTIFICATIONS=true
# Email configurations
SMTP_FROM=email@gmail.com
SMTP_SERVER=smtp.gmail.com
SMTP_DOMAIN=gmail.com
SMTP_PORT=587
SMTP_USERNAME=email@gmail.com
SMTP_PASSWORD=my-secret-password
# SMTP_TLS=false
# SMTP_AUTH=login
# SMTP_STARTTLS_AUTO=true
# OmniAuth
TWITTER_ID=
TWITTER_SECRET=

View File

@ -0,0 +1,102 @@
#!/usr/bin/ruby
##################################################################
# Make sure the dependencies of gems are met
#
# gem install jwt
# gem install java_properties
##################################################################
#
# Example of a post publish script to send an event to GreenLight
# whenever a recording is published in the BigBlueButton server.
#
# Uses the same data format and checksum calculation method used by
# the webhooks module.
#
require "trollop"
require "net/http"
require "jwt"
require "java_properties"
require "json"
require "digest/sha1"
require "uri"
require File.expand_path('../../../lib/recordandplayback', __FILE__)
logger = Logger.new("/var/log/bigbluebutton/post_process.log", 'weekly')
logger.level = Logger::INFO
BigBlueButton.logger = logger
opts = Trollop::options do
opt :meeting_id, "Meeting id to archive", :type => String
end
meeting_id = opts[:meeting_id]
meeting_metadata = BigBlueButton::Events.get_meeting_metadata("/var/bigbluebutton/recording/raw/#{meeting_id}/events.xml")
BigBlueButton.logger.info("Post Process: Recording Notify for [#{meeting_id}] starts")
begin
callback_url = meeting_metadata["gl-webhooks-callback-url"]
unless callback_url.nil?
BigBlueButton.logger.info("Making callback for recording ready notification")
props = JavaProperties::Properties.new("/var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties")
secret = props[:securitySalt]
timestamp = Time.now.to_i
event = {
"header": {
"name": "publish_ended"
},
"payload":{
"metadata": meeting_metadata,
"meeting_id": meeting_id
}
}
payload = {
event: event,
timestamp: timestamp
}
checksum_str = "#{callback_url}#{payload.to_json}#{secret}"
checksum = Digest::SHA1.hexdigest(checksum_str)
BigBlueButton.logger.info("Got checksum #{checksum} for #{checksum_str}")
separator = URI.parse(callback_url).query ? "&" : "?"
uri = URI.parse("#{callback_url}#{separator}checksum=#{checksum}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
request.body = payload.to_json
BigBlueButton.logger.info("Posted event to #{callback_url}")
response = http.request(request)
code = response.code.to_i
if code == 410
BigBlueButton.logger.info("Notified for deleted meeting: #{meeting_id}")
# TODO: should we automatically delete the recording here?
elsif code == 404
BigBlueButton.logger.warn("404 error when notifying for recording: #{meeting_id}, ignoring")
elsif code < 200 || code >= 300
BigBlueButton.logger.debug("Callback HTTP request failed: #{response.code} #{response.message} (code #{code})")
else
BigBlueButton.logger.debug("Recording notifier successful: #{meeting_id} (code #{code})")
end
else
BigBlueButton.logger.info("Blank callback URL, aborting.")
end
rescue => e
BigBlueButton.logger.info("Rescued")
BigBlueButton.logger.info(e.to_s)
end
BigBlueButton.logger.info("Post Process: Recording Notify ends")
exit 0

View File

@ -0,0 +1,6 @@
# Preview all emails at http://localhost:3000/rails/mailers/example_mailer
class NotificationMailerPreview < ActionMailer::Preview
def recording_ready_email_preview
NotificationMailer.recording_ready_email(User.first)
end
end