canvas-lms/app/models/big_blue_button_conference.rb

187 lines
6.2 KiB
Ruby

#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class BigBlueButtonConference < WebConference
user_setting_field :record, {
name: ->{ t('recording_setting', 'Recording') },
description: ->{ t('recording_setting_description', 'Record this conference') },
type: :boolean,
default: false,
visible: ->{ WebConference.config(BigBlueButtonConference.to_s)[:recording_enabled] },
}
def initiate_conference
return conference_key if conference_key && !retouch?
unless self.conference_key
self.conference_key = "instructure_#{self.feed_code}".gsub(/[^a-zA-Z0-9_]/, "_")
chars = ('a'..'z').to_a + ('0'..'9').to_a
# create user/admin passwords for this conference. we may want to show
# the admin passwords in the ui in case moderators need them for any
# admin-specific functionality within the BBB ui (or we could provide
# ui for them to specify the password/key)
settings[:user_key] = 8.times.map{ chars[chars.size * rand] }.join
settings[:admin_key] = 8.times.map{ chars[chars.size * rand] }.join until settings[:admin_key] && settings[:admin_key] != settings[:user_key]
end
settings[:record] &&= config[:recording_enabled]
send_request(:create, {
:meetingID => conference_key,
:name => title,
:voiceBridge => "%020d" % self.global_id,
:attendeePW => settings[:user_key],
:moderatorPW => settings[:admin_key],
:logoutURL => (settings[:default_return_url] || "http://www.instructure.com"),
:record => settings[:record] ? "true" : "false",
}) or return nil
save
conference_key
end
def conference_status
if (result = send_request(:isMeetingRunning, :meetingID => conference_key)) && result[:running] == 'true'
:active
else
:closed
end
end
def admin_join_url(user, return_to = "http://www.instructure.com")
join_url(user, :admin)
end
def participant_join_url(user, return_to = "http://www.instructure.com")
join_url(user)
end
def recordings
fetch_recordings.map do |recording|
recording_format = recording.fetch(:playback, {}).fetch(:format, {})
{
recording_id: recording[:recordID],
duration_minutes: recording_format[:length].to_i,
playback_url: recording_format[:url],
}
end
end
private
def retouch?
# by default, BBB will remove chat rooms that have been idle for more than
# an hour. so if an admin creates a room and then leaves, and then a user
# tries to join more than an hour later, we need to make sure we recreate
# the room before we redirect the user. there's no harm in "creating" a
# room that already exists, the api will just return the room info.
updated_at < 30.minutes.ago
end
def join_url(user, type = :user)
generate_request :join,
:fullName => user.name,
:meetingID => conference_key,
:password => settings[(type == :user ? :user_key : :admin_key)],
:userID => user.id
end
def fetch_recordings
return [] unless conference_key
response = send_request(:getRecordings, {
:meetingID => conference_key,
})
result = response[:recordings] if response
Array(result)
end
def delete_recording(recording_id)
response = send_request(:deleteRecordings, {
:recordID => recording_id,
})
response[:deleted] if response
end
def generate_request(action, options)
query_string = options.to_query
query_string << ("&checksum=" + Digest::SHA1.hexdigest(action.to_s + query_string + config[:secret_dec]))
"http://#{config[:domain]}/bigbluebutton/api/#{action}?#{query_string}"
end
def send_request(action, options)
uri = URI.parse(generate_request(action, options))
res = nil
Net::HTTP.start(uri.host, uri.port) do |http|
http.read_timeout = 10
5.times do # follow redirects, but not forever
logger.debug "big blue button api call: #{uri.path}?#{uri.query}"
res = http.request_get("#{uri.path}?#{uri.query}")
break unless res.is_a?(Net::HTTPRedirection)
url = res['location']
uri = URI.parse(url)
end
end
case res
when Net::HTTPSuccess
response = xml_to_hash(res.body)
if response[:returncode] == 'SUCCESS'
return response
else
logger.error "big blue button api error #{response[:message]} (#{response[:messageKey]})"
end
else
logger.error "big blue button http error #{res}"
end
nil
rescue Timeout::Error
logger.error "big blue button timeout error"
nil
rescue
logger.error "big blue button unhandled exception #{$!}"
nil
end
def xml_to_hash(xml_string)
doc = Nokogiri::XML(xml_string)
# assumes the top level value will be a hash
xml_to_value(doc.root)
end
def xml_to_value(node)
child_elements = node.element_children
# if there are no children at all, then this is an empty node
if node.children.empty?
nil
# If no child_elements, this is probably a text node, so just return its content
elsif child_elements.empty?
node.content
# The BBB API follows the pattern where a plural element (ie <bars>)
# contains many singular elements (ie <bar>) and nothing else. Detect this
# and return an array to be assigned to the plural element.
elsif node.name.singularize == child_elements.first.name
child_elements.map { |child| xml_to_value(child) }
# otherwise, make a hash of the child elements
else
child_elements.reduce({}) do |hash, child|
hash[child.name.to_sym] = xml_to_value(child)
hash
end
end
end
end