class BigbluebuttonRoom < ActiveRecord::Base belongs_to :server, :class_name => 'BigbluebuttonServer' belongs_to :owner, :polymorphic => true has_many :recordings, :class_name => 'BigbluebuttonRecording', :foreign_key => 'room_id', :dependent => :nullify has_many :metadata, :class_name => 'BigbluebuttonMetadata', :as => :owner, :dependent => :destroy, :inverse_of => :owner accepts_nested_attributes_for :metadata, :allow_destroy => true, :reject_if => :all_blank validates :meetingid, :presence => true, :uniqueness => true, :length => { :minimum => 1, :maximum => 100 } validates :name, :presence => true, :uniqueness => true, :length => { :minimum => 1, :maximum => 150 } validates :welcome_msg, :length => { :maximum => 250 } validates :private, :inclusion => { :in => [true, false] } validates :voice_bridge, :presence => true, :uniqueness => true validates :record, :inclusion => { :in => [true, false] } validates :duration, :presence => true, :numericality => { :only_integer => true, :greater_than_or_equal_to => 0 } validates :param, :presence => true, :uniqueness => true, :length => { :minimum => 1 }, :format => { :with => /^([a-zA-Z\d_]|[a-zA-Z\d_]+[a-zA-Z\d_-]*[a-zA-Z\d_]+)$/, :message => I18n.t('bigbluebutton_rails.rooms.errors.param_format') } # Passwords are 16 character strings # See http://groups.google.com/group/bigbluebutton-dev/browse_thread/thread/9be5aae1648bcab?pli=1 validates :attendee_password, :length => { :maximum => 16 } validates :moderator_password, :length => { :maximum => 16 } validates :attendee_password, :presence => true, :if => :private? validates :moderator_password, :presence => true, :if => :private? attr_accessible :name, :server_id, :meetingid, :attendee_password, :moderator_password, :welcome_msg, :owner, :server, :private, :logout_url, :dial_number, :voice_bridge, :max_participants, :owner_id, :owner_type, :external, :param, :record, :duration, :metadata_attributes # Note: these params need to be fetched from the server before being accessed attr_accessor :running, :participant_count, :moderator_count, :attendees, :has_been_forcibly_ended, :start_time, :end_time after_initialize :init before_validation :set_param before_validation :set_passwords # the full logout_url used when logout_url is a relative path attr_accessor :full_logout_url # HTTP headers that will be passed to the BigBlueButtonApi object to send # in all GET/POST requests to a webconf server. # Currently used to send the client's IP to the load balancer. attr_accessor :request_headers # Convenience method to access the attribute running def is_running? @running end # Fetches info from BBB about this room. # The response is parsed and stored in the model. You can access it using attributes such as: # # room.participant_count # room.attendees[0].full_name # # The attributes changed are: # * participant_count # * moderator_count # * running # * has_been_forcibly_ended # * start_time # * end_time # * attendees (array of BigbluebuttonAttendee) # # Triggers API call: getMeetingInfo. def fetch_meeting_info require_server response = self.server.api.get_meeting_info(self.meetingid, self.moderator_password) @participant_count = response[:participantCount] @moderator_count = response[:moderatorCount] @running = response[:running] @has_been_forcibly_ended = response[:hasBeenForciblyEnded] @start_time = response[:startTime] @end_time = response[:endTime] @attendees = [] response[:attendees].each do |att| attendee = BigbluebuttonAttendee.new attendee.from_hash(att) @attendees << attendee end response end # Fetches the BBB server to see if the meeting is running. Sets running # # Triggers API call: isMeetingRunning. def fetch_is_running? require_server @running = self.server.api.is_meeting_running?(self.meetingid) end # Sends a call to the BBB server to end the meeting. # # Triggers API call: end. def send_end require_server self.server.api.end_meeting(self.meetingid, self.moderator_password) end # Sends a call to the BBB server to create the meeting. # 'username' is the name of the user that is creating the meeting. # 'userid' is the id of the user that is creating the meeting. # # Will trigger 'select_server' to select a server where the meeting # will be created. If a server is selected, the model is saved. # # With the response, updates the following attributes: # * attendee_password # * moderator_password # # Triggers API call: create. def send_create(username=nil, userid=nil) # updates the server whenever a meeting will be created and guarantees it has a meetingid self.server = select_server self.meetingid = unique_meetingid() if self.meetingid.nil? self.save unless self.new_record? require_server response = do_create_meeting(username, userid) unless response.nil? self.attendee_password = response[:attendeePW] self.moderator_password = response[:moderatorPW] self.save unless self.new_record? end response end # Returns the URL to join this room. # username:: Name of the user # role:: Role of the user in this room. Can be [:moderator, :attendee] # password:: Password to be use (in case role == nil) # # Uses the API but does not require a request to the server. def join_url(username, role, password=nil) require_server case role when :moderator self.server.api.join_meeting_url(self.meetingid, username, self.moderator_password) when :attendee self.server.api.join_meeting_url(self.meetingid, username, self.attendee_password) else self.server.api.join_meeting_url(self.meetingid, username, password) end end # Returns the role of the user based on the password given. # The return value can be :moderator, :attendee, or # nil if the password given does not match any of the room passwords. # params:: Hash with a key :password def user_role(params) role = nil if params.has_key?(:password) if self.moderator_password == params[:password] role = :moderator elsif self.attendee_password == params[:password] role = :attendee end end role end # Compare the instance variables of two models to define if they are equal # Returns a hash with the variables with different values or an empty hash # if they are have all equal values. # From: http://alicebobandmallory.com/articles/2009/11/02/comparing-instance-variables-in-ruby def instance_variables_compare(o) vars = [ :@running, :@participant_count, :@moderator_count, :@attendees, :@has_been_forcibly_ended, :@start_time, :@end_time ] Hash[*vars.map { |v| self.instance_variable_get(v)!=o.instance_variable_get(v) ? [v,o.instance_variable_get(v)] : []}.flatten] end # A more complete equal? method, comparing also the attibutes and # the instance variables def attr_equal?(o) self == o and self.instance_variables_compare(o).empty? and self.attributes == o.attributes end def to_param self.param end # The create logic. # Will create the meeting in this room unless it is already running. # Returns true if the meeting was created. def create_meeting(username, userid=nil, request=nil) fetch_is_running? unless is_running? add_domain_to_logout_url(request.protocol, request.host_with_port) unless request.nil? send_create(username, userid) true else false end end # add a domain name and/or protocol to the logout_url if needed # it doesn't save in the db, just updates the instance def add_domain_to_logout_url(protocol, host) unless logout_url.nil? url = logout_url.downcase unless url.nil? or url =~ /^[a-z]+:\/\// # matches the protocol unless url =~ /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*/ # matches the host domain url = host + url end url = protocol + url end self.full_logout_url = url.downcase end end def unique_meetingid # GUID # Has to be globally unique in case more that one bigbluebutton_rails application is using # the same web conference server. "#{SecureRandom.uuid}-#{Time.now.to_i}" end protected # Every room needs a server to be used. # The server of a room can change during the room's lifespan, but # it should not change if the room is running or if it was created # but not yet ended. # Any action that requires a server should call 'require_server' before # anything else. def require_server if self.server.nil? msg = I18n.t('bigbluebutton_rails.rooms.errors.server.not_set') raise BigbluebuttonRails::ServerRequired.new(msg) end end # This method can be overridden to change the way the server is selected # before a room is created # This one selects the server with less rooms in it def select_server BigbluebuttonServer. select("bigbluebutton_servers.*, count(bigbluebutton_rooms.id) as room_count"). joins(:rooms).group(:server_id).order("room_count ASC").first end def init self[:meetingid] ||= unique_meetingid self[:voice_bridge] ||= random_voice_bridge @request_headers = {} # fetched attributes @participant_count = 0 @moderator_count = 0 @running = false @has_been_forcibly_ended = false @start_time = nil @end_time = nil @attendees = [] end def random_voice_bridge value = (70000 + SecureRandom.random_number(9999)).to_s count = 1 while not BigbluebuttonRoom.find_by_voice_bridge(value).nil? and count < 10 count += 1 value = (70000 + SecureRandom.random_number(9999)).to_s end value end def do_create_meeting(username=nil, userid=nil) opts = { :record => self.record, :duration => self.duration, :moderatorPW => self.moderator_password, :attendeePW => self.attendee_password, :welcome => self.welcome_msg.blank? ? default_welcome_message : self.welcome_msg, :dialNumber => self.dial_number, :logoutURL => self.full_logout_url || self.logout_url, :maxParticipants => self.max_participants, :voiceBridge => self.voice_bridge }.merge(self.get_metadata_for_create) # Add information about the user that is creating the meeting (if any) opts.merge!({ "meta_#{BigbluebuttonRails.metadata_user_id}" => userid }) unless userid.nil? opts.merge!({ "meta_#{BigbluebuttonRails.metadata_user_name}" => username }) unless username.nil? self.server.api.request_headers = @request_headers # we need the client's IP self.server.api.create_meeting(self.name, self.meetingid, opts) end # Returns the default welcome message to be shown in a conference in case # there's no message set in this room. # Can be used to easily set a default message format for all rooms. def default_welcome_message I18n.t('bigbluebutton_rails.rooms.default_welcome_msg', :name => self.name, :voice_number => self.voice_bridge) end # if :param wasn't set, sets it as :name downcase and parameterized def set_param if self.param.blank? self.param = self.name.parameterize.downcase unless self.name.nil? end end # When setting a room as private we generate passwords in case they don't exist. def set_passwords if self.private_changed? and self.private if self.moderator_password.blank? self.moderator_password = SecureRandom.hex(4) end if self.attendee_password.blank? self.attendee_password = SecureRandom.hex(4) end end end def get_metadata_for_create self.metadata.inject({}) { |result, meta| result["meta_#{meta.name}"] = meta.content; result } end end