From fbc8a56fc02e011e19c3b34495623044562c98a5 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 14 Sep 2017 15:59:42 -0400 Subject: [PATCH 1/2] add support for BBB 2.0 webhooks --- app/assets/javascripts/active_meetings.js | 119 +++++++++++----------- app/controllers/bbb_controller.rb | 33 +++--- app/controllers/landing_controller.rb | 35 ++++++- config/routes.rb | 2 +- 4 files changed, 109 insertions(+), 80 deletions(-) diff --git a/app/assets/javascripts/active_meetings.js b/app/assets/javascripts/active_meetings.js index 9a578b49..aef51e6a 100644 --- a/app/assets/javascripts/active_meetings.js +++ b/app/assets/javascripts/active_meetings.js @@ -31,22 +31,41 @@ var updatePreviousMeetings = function(){ }); } +// Ignore excess on either side of user_id. +var trimUserId = function(user_id){ + components = user_id.split('_') + return components.sort(function (a, b) {return b.length - a.length;})[0] +} + +// Finds a user by their user_id. +var findByUserId = function(users, user_id){ + for(i = 0; i < users.length; i++){ + if(trimUserId(users[i]['user_id']) == trimUserId(user_id)){ + return i + } + } + return undefined +} + // Adds a user to a meeting. var addUser = function(data){ if(data['role'] == 'MODERATOR'){ - MEETINGS[data['meeting']]['moderators'].push(data['user']) + MEETINGS[data['meeting']]['moderators'].push({'name': data['user'], 'user_id': data['user_id']}) } else { - MEETINGS[data['meeting']]['participants'].push(data['user']) + MEETINGS[data['meeting']]['participants'].push({'name':data['user'], 'user_id': data['user_id']}) } updateMeetingText(MEETINGS[data['meeting']]) } // Removes a user from a meeting. var removeUser = function(data){ - if(data['role'] == 'MODERATOR'){ - MEETINGS[data['meeting']]['moderators'].splice(MEETINGS[data['meeting']]['moderators'].indexOf(data['user']), 1); + user = findByUserId(MEETINGS[data['meeting']]['moderators'], data['user_id']) + if(user == undefined){ + user = findByUserId(MEETINGS[data['meeting']]['participants'], data['user_id']); + if(user == undefined){ return; } + MEETINGS[data['meeting']]['participants'].splice(user, 1); } else { - MEETINGS[data['meeting']]['participants'].splice(MEETINGS[data['meeting']]['participants'].indexOf(data['user']), 1); + MEETINGS[data['meeting']]['moderators'].splice(user, 1); } updateMeetingText(MEETINGS[data['meeting']]) } @@ -60,14 +79,15 @@ var updateMeetingText = function(m){ if(m['moderators'].length + m['participants'].length == 0){ list = '(empty)' } else { - list = m['moderators'].join('(mod), ') + (m['moderators'].length > 0 ? '(mod)' : '') + - (m['participants'].length > 0 && m['moderators'].length != 0 ? ', ' : '') + m['participants'].join(', ') + list = m['moderators'].map(function(x){ return x['name']; }).join('(mod), ') + + (m['moderators'].length > 0 ? '(mod)' : '') + + (m['participants'].length > 0 && m['moderators'].length != 0 ? ', ' : '') + + (m['participants'].map(function(x){ return x['name']; }).join(', ')) } body = '' + m['name'] + ': ' + list + '' // Otherwise it hasn't started (users waiting the join). } else { - body = '' + m['name'] + ' (not yet started): ' + - m['users'].join(', ') + '' + body = '' + m['name'] + ' (not yet started): ' + m['users'].join(', ') + '' } // If the item doesn't exist, add it and set up join meeting event. @@ -91,65 +111,41 @@ var initialPopulate = function(){ // Only populate on room resources. var chopped = window.location.href.split('/') if (!window.location.href.includes('rooms') || chopped[chopped.length - 2] == $('body').data('current-user')) { return; } - $.get((window.location.href + '/request').replace('#', ''), function(data){ - var meetings = data['active']['meetings'] - var waiting = data['waiting'] - - jQuery.each(waiting[$('body').data('current-user')], function(name, users){ - WAITING[name] = {'name': name, - 'users': users} - updateMeetingText(WAITING[name]) - }); - - for(var i = 0; i < meetings.length; i++){ - // Make sure the meeting actually belongs to the current user. - if(meetings[i]['metadata']['room-id'] != $('body').data('current-user')) { continue; } - var name = meetings[i]['meetingName'] + + $.post((window.location.href + '/statuses').replace('#', ''), {previously_joined: getPreviouslyJoined()}) + .done(function(data) { - var attendees; - if(meetings[i]['attendees']['attendee'] instanceof Array){ - attendees = meetings[i]['attendees']['attendee'] - } else { - attendees = [meetings[i]['attendees']['attendee']] - } + // Populate waiting meetings. + Object.keys(data['waiting']).forEach(function(key) { + WAITING[name] = {'name': key, 'users': data['waiting'][key]} + updateMeetingText(WAITING[name]) + }) - var participants = [] - var moderators = [] - - jQuery.each(attendees, function(i, attendee){ - // The API doesn't return a empty array when empty, just undefined. - if(attendee != undefined){ - if(attendee['role'] == "MODERATOR"){ - moderators.push(attendee['fullName']) - } else { - participants.push(attendee['fullName']) - } - } - }); - - // Create meeting. - MEETINGS[name] = {'name': name, - 'participants': participants, - 'moderators': moderators} - - if(isPreviouslyJoined(name)){ + // Add the meetings to the active meetings list. + for(var i = 0; i < data['active'].length; i++){ + var meeting = data['active'][i] + + var name = meeting['name'] + var participants = meeting['participants'] + var moderators = meeting['moderators'] + + // Create meeting. + MEETINGS[name] = {'name': name, 'participants': participants, 'moderators': moderators} updateMeetingText(MEETINGS[name]) } - } - - }).done(function(){ - // Remove from previous meetings if they are active. - updatePreviousMeetings(); - $('.hidden-list').show(); - $('.active-spinner').hide(); - }); + + // Remove from previous meetings if they are active. + updatePreviousMeetings(); + $('.hidden-list').show(); + $('.active-spinner').hide(); + }); } -// Checks if a meeting has been prveiously joined by the user. -var isPreviouslyJoined = function(meeting){ +// Gets a list of known previously joined meetings. +var getPreviouslyJoined = function(){ var joinedMeetings = localStorage.getItem('joinedRooms-' + $('body').data('current-user')); - if (joinedMeetings == '' || joinedMeetings == null){ return false; } - return joinedMeetings.split(',').indexOf(meeting) >= 0 + if (joinedMeetings == '' || joinedMeetings == null){ return []; } + return joinedMeetings.split(',') } // Removes an active meeting. @@ -183,7 +179,6 @@ var joinMeeting = function(meeting_name){ // Only need to register for logged in users. $(document).on('turbolinks:load', function(){ if($('body').data('current-user')){ - MEETINGS = {} // Ensure actives is empty. $('.actives').empty(); diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index 6134e320..01491635 100644 --- a/app/controllers/bbb_controller.rb +++ b/app/controllers/bbb_controller.rb @@ -259,7 +259,12 @@ class BbbController < ApplicationController end def treat_callback_event(event) - eventName = (event.present? && event['header'].present?) ? event['header']['name'] : nil + # Check if the event is a BigBlueButton 2.0 event. + if event.has_key?('envelope') + eventName = (event.present? && event['envelope'].present?) ? event['envelope']['name'] : nil + else # The event came from BigBlueButton 1.1 (or earlier). + eventName = (event.present? && event['header'].present?) ? event['header']['name'] : nil + end # a recording is ready if eventName == "publish_ended" @@ -286,11 +291,11 @@ class BbbController < ApplicationController else logger.error "Bad format for event #{event}, won't process" end - elsif eventName == "meeting_created_message" + elsif eventName == "meeting_created_message" || eventName == "MeetingCreatedEvtMsg" # Fire an Actioncable event that updates _previously_joined for the client. - actioncable_event('create', params[:id], params[:room_id]) - elsif eventName == "meeting_destroyed_event" - actioncable_event('destroy', params[:id], params[:room_id]) + actioncable_event('create') + elsif eventName == "meeting_destroyed_event" || eventName == "MeetingEndedEvtMsg" + actioncable_event('destroy') # Since the meeting is destroyed we have no way get the callback url to remove the meeting, so we must build it. remove_url = build_callback_url(params[:id], params[:room_id]) @@ -298,9 +303,13 @@ class BbbController < ApplicationController # Remove webhook for the meeting. webhook_remove(remove_url) elsif eventName == "user_joined_message" - actioncable_event('join', params[:id], params[:room_id], event['payload']['user']['name'], event['payload']['user']['role']) + actioncable_event('join', {user_id: event['payload']['user']['extern_userid'], user: event['payload']['user']['name'], role: event['payload']['user']['role']}) + elsif eventName == "UserJoinedMeetingEvtMsg" + actioncable_event('join', {user_id: event['core']['body']['intId'], user: event['core']['body']['name'], role: event['core']['body']['role']}) elsif eventName == "user_left_message" - actioncable_event('leave', params[:id], params[:room_id], event['payload']['user']['name'], event['payload']['user']['role']) + actioncable_event('leave', {user_id: event['payload']['user']['extern_userid']}) + elsif eventName == "UserLeftMeetingEvtMsg" + actioncable_event('leave', {user_id: event['core']['body']['intId']}) else logger.info "Callback event will not be treated. Event name: #{eventName}" end @@ -312,13 +321,9 @@ class BbbController < ApplicationController "#{request.base_url}#{relative_root}/rooms/#{room_id}/#{URI.encode(id)}/callback" end - def actioncable_event(method, id, room_id, user = 'none', role = 'none') - ActionCable.server.broadcast 'refresh_meetings', - method: method, - meeting: id, - room: room_id, - user: user, - role: role + def actioncable_event(method, data = {}) + data = {method: method, meeting: params[:id], room: params[:room_id]}.merge(data) + ActionCable.server.broadcast('refresh_meetings', data) end # Validates the checksum received in a callback call. diff --git a/app/controllers/landing_controller.rb b/app/controllers/landing_controller.rb index a6f6aa37..8cdeda16 100644 --- a/app/controllers/landing_controller.rb +++ b/app/controllers/landing_controller.rb @@ -44,9 +44,38 @@ class LandingController < ApplicationController # If someone tries to access the guest landing when guest access is enabled, just send them to root. redirect_to root_url unless Rails.configuration.disable_guest_access end - - def send_meetings_data - render json: {active: bbb.get_meetings, waiting: WaitingList.waiting} + + # Sends data on meetings that the current user has previously joined. + def get_previous_meeting_statuses + previously_joined = params[:previously_joined] + active_meetings = bbb.get_meetings[:meetings] + payload = {active: [], waiting: []} + # Find meetings that are owned by the current user and also active. + active_meetings.each do |m| + if m[:metadata].has_key?(:'room-id') + if previously_joined.include?(m[:meetingName])&& m[:metadata][:'room-id'] == current_user[:encrypted_id] + if m[:attendees] == {} + attendees = [] + else + attendees = m[:attendees][:attendee] + attendees = [attendees] unless attendees.is_a?(Array) + end + participants = [] + moderators = [] + attendees.each do |a| + if a[:role] == 'MODERATOR' + moderators << {name: a[:fullName], user_id: a[:userID]} + else + participants << {name: a[:fullName], user_id: a[:userID]} + end + end + payload[:active] << {name: m[:meetingName], moderators: moderators, participants: participants} + end + end + end + # Add the waiting meetings. + payload[:waiting] = WaitingList.waiting[current_user[:encrypted_id]] || {} + render json: payload end def wait_for_moderator diff --git a/config/routes.rb b/config/routes.rb index faa14a1f..8c854167 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,7 +51,7 @@ Rails.application.routes.draw do post '/:room_id/:id/callback', to: 'bbb#callback', :constraints => {:id => disallow_slash, :room_id => disallow_slash} # routes shared between meetings and rooms - get '/(:room_id)/request', to: 'landing#send_meetings_data', :defaults => { :format => 'xml' } + post '/(:room_id)/statuses', to: 'landing#get_previous_meeting_statuses' get '/(:room_id)/:id/join', to: 'bbb#join', defaults: {room_id: nil, format: 'json'}, :constraints => {:id => disallow_slash, :room_id => disallow_slash} get '/(:room_id)/:id', to: 'landing#resource', as: :meeting_room, defaults: {room_id: nil}, :constraints => {:id => disallow_slash, :room_id => disallow_slash} end From 40ee86a6a43062f8d77d0bc3b4668e3bfd39c30d Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 14 Sep 2017 16:14:47 -0400 Subject: [PATCH 2/2] add temp fix for recording length in milliseconds --- app/assets/javascripts/recordings.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/recordings.coffee b/app/assets/javascripts/recordings.coffee index eccd9fb3..d9edad85 100644 --- a/app/assets/javascripts/recordings.coffee +++ b/app/assets/javascripts/recordings.coffee @@ -224,7 +224,9 @@ class @Recordings table_api.column(COLUMN.ACTION).visible(false) table_api.column(COLUMN.VISIBILITY).visible(false) for recording in data.recordings - recording.duration = recording.length + # NOTE: Temporary fix for the minutes to milliseconds bug some recordings may have + # experienced when transitioning from BigBlueButton 1.1 to BigBlueButton 2.0. + recording.duration = if recording.duration < 1000 then recording.duration else parseInt(recording.length / 60000) data.recordings.sort (a,b) -> return new Date(b.start_time) - new Date(a.start_time) table_api.clear()