├── .ruby-version ├── .rspec ├── .gitignore ├── extras └── test-presentation.pdf ├── spec ├── support │ └── forgery │ │ └── forgeries │ │ ├── url.rb │ │ └── random_name.rb ├── spec_helper.rb ├── bigbluebutton_exception_spec.rb ├── bigbluebutton_api_0.9_spec.rb ├── data │ └── hash_to_xml_complex.xml ├── bigbluebutton_modules_spec.rb ├── bigbluebutton_hash_to_xml_spec.rb ├── bigbluebutton_formatter_spec.rb └── bigbluebutton_api_spec.rb ├── .travis.yml ├── Gemfile ├── docker-compose.yml ├── features ├── support │ ├── env.rb │ ├── hooks.rb │ └── features_helpers.rb ├── config.yml.example ├── step_definitions │ ├── pre_upload_slides_steps.rb │ ├── recordings_steps.rb │ ├── join_meetings_steps.rb │ ├── create_meetings_steps.rb │ ├── end_meetings_steps.rb │ ├── common_steps.rb │ └── check_status_steps.rb ├── end_meetings.feature ├── pre_upload_slides.feature ├── create_meetings.feature ├── join_meetings.feature ├── recordings.feature └── check_status.feature ├── lib ├── bigbluebutton_exception.rb ├── bigbluebutton_hash_to_xml.rb ├── bigbluebutton_modules.rb ├── bigbluebutton_formatter.rb └── bigbluebutton_api.rb ├── Dockerfile ├── examples ├── get_version_example.rb ├── create.rb ├── prepare.rb ├── join_example.rb └── overall_example.rb ├── .github └── workflows │ └── publish-gem-on-tag-push.yml ├── Rakefile ├── bigbluebutton-api-ruby.gemspec ├── Gemfile.lock ├── LICENSE_003 ├── LICENSE ├── README.md └── CHANGELOG.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.5 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | pkg 3 | *~ 4 | features/config.yml 5 | logs/bbb.log 6 | rdoc/ 7 | logs/* 8 | .bundle/ 9 | .volumes/ -------------------------------------------------------------------------------- /extras/test-presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/bigbluebutton-api-ruby/HEAD/extras/test-presentation.pdf -------------------------------------------------------------------------------- /spec/support/forgery/forgeries/url.rb: -------------------------------------------------------------------------------- 1 | class Forgery::Internet < Forgery 2 | def self.url 3 | "http://" + domain_name + top_level_domain + cctld + "/" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | script: "bundle exec rake spec" 4 | cache: bundler 5 | rvm: 6 | - 2.2.0 7 | - 2.1.2 8 | - 2.0.0 9 | - 1.9.3 10 | - 1.9.2 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby file: '.ruby-version' 4 | 5 | gemspec 6 | 7 | group :development, :test do 8 | gem 'rspec' 9 | gem 'forgery' 10 | gem 'rake' 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/forgery/forgeries/random_name.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | class Forgery::Basic < Forgery 4 | def self.random_name(base) 5 | base + "-" + SecureRandom.hex(4) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | test: 5 | build: . 6 | tty: true 7 | volumes: 8 | - $PWD:/usr/src/app 9 | - .volumes/bundle/:/usr/local/bundle 10 | command: bundle exec rake 11 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', '..', 'lib') 2 | 3 | require 'bigbluebutton_api' 4 | require 'bigbluebutton_bot' 5 | require 'forgery' 6 | Dir["#{File.dirname(__FILE__)}/../../spec/support/forgery/**/*.rb"].each { |f| require f } 7 | -------------------------------------------------------------------------------- /lib/bigbluebutton_exception.rb: -------------------------------------------------------------------------------- 1 | module BigBlueButton 2 | 3 | class BigBlueButtonException < StandardError 4 | attr_accessor :key 5 | 6 | def to_s 7 | s = super.to_s 8 | s += ", messageKey: #{key.to_s}" unless key.nil? or key.to_s.empty? 9 | s 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'forgery' 2 | 3 | # Load support files 4 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 5 | 6 | # Load Factories 7 | #require 'factory_girl' 8 | # Dir["#{ File.dirname(__FILE__)}/factories/*.rb"].each { |f| require f } 9 | 10 | 11 | RSpec.configure do |config| 12 | config.mock_with :rspec 13 | end 14 | 15 | require "bigbluebutton_api" 16 | -------------------------------------------------------------------------------- /features/support/hooks.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | # stores the global configurations in variables that are easier to access 3 | BigBlueButton::Features::Configs.load 4 | @config = BigBlueButton::Features::Configs.cfg 5 | @config_server = BigBlueButton::Features::Configs.cfg_server 6 | @req = BigBlueButton::Features::Configs.req 7 | end 8 | 9 | After do |scenario| 10 | BigBlueButtonBot.finalize 11 | end 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.4.5 2 | 3 | ENV app=/usr/src/app 4 | 5 | # Create app directory 6 | RUN mkdir -p $app 7 | WORKDIR $app 8 | 9 | # Bundle app source 10 | COPY . $app 11 | 12 | # Set the app directory as safe in Git, to avoid 'detected dubious ownership in repository' errors 13 | RUN git config --global --add safe.directory ${app} 14 | 15 | # Install app dependencies 16 | RUN gem install bundler -v 2.6.9 17 | RUN bundle install 18 | -------------------------------------------------------------------------------- /examples/get_version_example.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') 3 | 4 | require 'bigbluebutton_api' 5 | require 'prepare' 6 | 7 | begin 8 | prepare 9 | 10 | puts 11 | puts "---------------------------------------------------" 12 | puts "The version of your BBB server is: #{@api.get_api_version}" 13 | rescue Exception => ex 14 | puts "Failed with error #{ex.message}" 15 | puts ex.backtrace 16 | end 17 | -------------------------------------------------------------------------------- /features/config.yml.example: -------------------------------------------------------------------------------- 1 | logger: nil 2 | 3 | # maximum wait for a response to any API request (secs) 4 | timeout_req: 60 5 | 6 | # maximum wait for a meeting to be ended after end_meeting is called (secs) 7 | timeout_ending: 30 8 | 9 | # maximum wait until the bot starts (secs) 10 | timeout_bot_start: 60 11 | 12 | servers: 13 | test-install: 14 | url: 'http://test-install.blindsidenetworks.com/bigbluebutton/api' 15 | secret: '8cd8ef52e8e101574e400365b55e11a6' 16 | bbb-other: 17 | url: 'http://yourserver.com/bigbluebutton/api' 18 | secret: 'lka98f52e8akdlsoie400365b55e98s7' 19 | -------------------------------------------------------------------------------- /features/step_definitions/pre_upload_slides_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the user creates a meeting pre\-uploading the following presentations:$/ do |table| 2 | modules = BigBlueButton::BigBlueButtonModules.new 3 | table.hashes.each do |pres| 4 | modules.add_presentation(pres["type"].to_sym, pres["presentation"]) 5 | end 6 | 7 | @req.id = Forgery(:basic).random_name("test-pre-upload") 8 | @req.name = @req.id 9 | @req.method = :create 10 | @req.opts = {} 11 | @req.response = @api.create_meeting(@req.id, @req.name, @req.opts, modules) 12 | @req.mod_pass = @req.response[:moderatorPW] 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/publish-gem-on-tag-push.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem on tag push 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | - '!v**-mconf*' 7 | # Tags that will trigger: v1.9.0; v1.9.0-beta1 8 | # Tags excluded (with the '!'): v1.9.0-mconf; v1.9.0-mconf-beta1 9 | 10 | jobs: 11 | publish-gem: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Publish 17 | uses: dawidd6/action-publish-gem@v1.2.0 18 | with: 19 | # Optional, will publish to RubyGems if specified 20 | api_key: ${{secrets.RUBYGEMS_API_KEY}} 21 | -------------------------------------------------------------------------------- /features/end_meetings.feature: -------------------------------------------------------------------------------- 1 | Feature: End rooms 2 | To stop a meeting using the API 3 | One needs to be able to call 'end' to this meeting 4 | 5 | @version-all @need-bot 6 | Scenario: End a meeting 7 | Given that a meeting was created 8 | And the meeting is running 9 | When the method to end the meeting is called 10 | Then the response is successful and well formatted 11 | And the meeting should be ended 12 | 13 | @version-all 14 | Scenario: Try to end a meeting that is not running 15 | Given that a meeting was created 16 | When the method to end the meeting is called 17 | Then the response is successful 18 | And the response has the messageKey "sentEndMeetingRequest" 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rdoc/task' 3 | require 'rubygems/package_task' 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Default: run tests.' 7 | task :default => :spec 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | 11 | RDoc::Task.new do |rdoc| 12 | rdoc.rdoc_files.include('README.md', 'LICENSE', 'LICENSE_003', 'CHANGELOG.md', 'lib/**/*.rb') 13 | rdoc.main = "README.md" 14 | rdoc.title = "bigbluebutton-api-ruby Docs" 15 | rdoc.rdoc_dir = 'rdoc' 16 | end 17 | 18 | eval("$specification = begin; #{IO.read('bigbluebutton-api-ruby.gemspec')}; end") 19 | Gem::PackageTask.new $specification do |pkg| 20 | pkg.need_tar = true 21 | pkg.need_zip = true 22 | end 23 | 24 | task :notes do 25 | puts `grep -r 'OPTIMIZE\\|FIXME\\|TODO' lib/ spec/ features/` 26 | end 27 | -------------------------------------------------------------------------------- /features/pre_upload_slides.feature: -------------------------------------------------------------------------------- 1 | @version-all 2 | Feature: Pre-upload slides 3 | To have presentations ready in the meeting when the users join 4 | One needs to pre-upload these presentations when the meeting is created 5 | 6 | Scenario: Pre-upload presentations 7 | Given the default BigBlueButton server 8 | When the user creates a meeting pre-uploading the following presentations: 9 | | type | presentation | 10 | | url | http://www.samplepdf.com/sample.pdf | 11 | | file | extras/test-presentation.pdf | 12 | Then the response is successful and well formatted 13 | # OPTIMIZE: There's no way to check if the presentation is really in the meeting 14 | # And these presentations should be available in the meeting as it begins 15 | -------------------------------------------------------------------------------- /spec/bigbluebutton_exception_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BigBlueButton::BigBlueButtonException do 4 | 5 | describe "#key" do 6 | subject { BigBlueButton::BigBlueButtonException.new } 7 | it { should respond_to(:key) } 8 | it { should respond_to("key=") } 9 | end 10 | 11 | describe "#to_s" do 12 | context "when key is set" do 13 | let(:api) { BigBlueButton::BigBlueButtonException.new("super-msg") } 14 | before { api.key = "key-msg" } 15 | it { api.to_s.should == "super-msg, messageKey: key-msg" } 16 | end 17 | 18 | context "when key is not set" do 19 | let(:api) { BigBlueButton::BigBlueButtonException.new("super-msg") } 20 | before { api.key = nil } 21 | it { api.to_s.should == "super-msg" } 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /bigbluebutton-api-ruby.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "bigbluebutton-api-ruby" 5 | s.version = "2.0.0" 6 | s.licenses = ["MIT"] 7 | s.extra_rdoc_files = ["README.md", "LICENSE", "LICENSE_003", "CHANGELOG.md"] 8 | s.summary = "BigBlueButton integration for ruby" 9 | s.description = "Provides methods to access BigBlueButton in a ruby application through its API" 10 | s.authors = ["Mconf", "Leonardo Crauss Daronco"] 11 | s.email = ["contact@mconf.org", "leonardodaronco@gmail.com"] 12 | s.homepage = "https://github.com/mconf/bigbluebutton-api-ruby/" 13 | s.bindir = "bin" 14 | s.files = `git ls-files`.split("\n") 15 | s.require_paths = ["lib"] 16 | s.required_ruby_version = '>= 3.2.0' 17 | 18 | s.add_runtime_dependency('xml-simple', '~> 1.1') 19 | s.add_runtime_dependency('base64', '>= 0.1.0') 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | bigbluebutton-api-ruby (2.0.0) 5 | base64 (>= 0.1.0) 6 | xml-simple (~> 1.1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | base64 (0.3.0) 12 | diff-lcs (1.6.2) 13 | forgery (0.5.0) 14 | rake (12.3.3) 15 | rexml (3.4.2) 16 | rspec (3.13.1) 17 | rspec-core (~> 3.13.0) 18 | rspec-expectations (~> 3.13.0) 19 | rspec-mocks (~> 3.13.0) 20 | rspec-core (3.13.5) 21 | rspec-support (~> 3.13.0) 22 | rspec-expectations (3.13.5) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.13.0) 25 | rspec-mocks (3.13.5) 26 | diff-lcs (>= 1.2.0, < 2.0) 27 | rspec-support (~> 3.13.0) 28 | rspec-support (3.13.5) 29 | xml-simple (1.1.9) 30 | rexml 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | bigbluebutton-api-ruby! 37 | forgery 38 | rake 39 | rspec 40 | 41 | RUBY VERSION 42 | ruby 3.4.5p51 43 | 44 | BUNDLED WITH 45 | 2.6.9 46 | -------------------------------------------------------------------------------- /examples/create.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') 3 | 4 | require 'bigbluebutton_api' 5 | require 'prepare' 6 | 7 | begin 8 | prepare 9 | 10 | num = rand(1000) 11 | meeting_name = "Test Meeting #{num}" 12 | meeting_id = "test-meeting-#{num}" 13 | moderator_name = "House" 14 | attendee_name = "Cameron" 15 | puts "---------------------------------------------------" 16 | options = { :moderatorPW => "54321", 17 | :attendeePW => "12345", 18 | :welcome => 'Welcome to my meeting', 19 | :dialNumber => '1-800-000-0000x00000#', 20 | :logoutURL => 'https://github.com/mconf/bigbluebutton-api-ruby', 21 | :maxParticipants => 25 } 22 | response = @api.create_meeting(meeting_name, meeting_id, options) 23 | puts "The meeting has been created with the response:" 24 | puts response.inspect 25 | 26 | rescue Exception => ex 27 | puts "Failed with error #{ex.message}" 28 | puts ex.backtrace 29 | end 30 | -------------------------------------------------------------------------------- /spec/bigbluebutton_api_0.9_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests for BBB API version 0.81 4 | describe BigBlueButton::BigBlueButtonApi do 5 | 6 | # default variables and API object for all tests 7 | let(:url) { "http://server.com" } 8 | let(:secret) { "1234567890abcdefghijkl" } 9 | let(:version) { "0.9" } 10 | let(:api) { BigBlueButton::BigBlueButtonApi.new(url, secret, version) } 11 | 12 | describe "#create_meeting" do 13 | context "accepts the new parameters" do 14 | let(:req_params) { 15 | { :name => "name", :meetingID => "meeting-id", 16 | :moderatorOnlyMessage => "my-msg", :autoStartRecording => "false", 17 | :allowStartStopRecording => "true" 18 | } 19 | } 20 | 21 | before { api.should_receive(:send_api_request).with(:create, req_params) } 22 | it { 23 | options = { 24 | :moderatorOnlyMessage => "my-msg", 25 | :autoStartRecording => "false", 26 | :allowStartStopRecording => "true" 27 | } 28 | api.create_meeting("name", "meeting-id", options) 29 | } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE_003: -------------------------------------------------------------------------------- 1 | == bigbluebutton 2 | 3 | Copyright (c) 2010 Joe Kinsella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/bigbluebutton_hash_to_xml.rb: -------------------------------------------------------------------------------- 1 | require 'xmlsimple' 2 | 3 | module BigBlueButton 4 | class BigBlueButtonHash < Hash 5 | class << self 6 | def from_xml(xml_io) 7 | begin 8 | # we'll not use 'KeyToSymbol' because it doesn't symbolize the keys for node attributes 9 | opts = { 'ForceArray' => false, 'ForceContent' => false } # 10 | hash = XmlSimple.xml_in(xml_io, opts) 11 | return symbolize_keys(hash) 12 | rescue Exception => e 13 | exception = BigBlueButtonException.new("Impossible to convert XML to hash. Error: #{e.message}") 14 | exception.key = 'XMLConversionError' 15 | raise exception 16 | end 17 | end 18 | 19 | def symbolize_keys(arg) 20 | case arg 21 | when Array 22 | arg.map { |elem| symbolize_keys elem } 23 | when Hash 24 | Hash[ 25 | arg.map { |key, value| 26 | k = key.is_a?(String) ? key.to_sym : key 27 | v = symbolize_keys value 28 | [k,v] 29 | }] 30 | else 31 | arg 32 | end 33 | end 34 | 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /examples/prepare.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') 3 | 4 | require 'bigbluebutton_api' 5 | require 'yaml' 6 | 7 | def prepare 8 | config_file = File.join(File.dirname(__FILE__), '..', 'features', 'config.yml') 9 | unless File.exist? config_file 10 | puts config_file + " does not exists. Copy the example and configure your server." 11 | puts "cp features/config.yml.example features/config.yml" 12 | puts 13 | Kernel.exit! 14 | end 15 | @config = YAML.load_file(config_file) 16 | 17 | puts "** Config:" 18 | @config.each do |k,v| 19 | puts k + ": " + v.to_s 20 | end 21 | puts 22 | 23 | if ARGV.size > 0 24 | unless @config['servers'].has_key?(ARGV[0]) 25 | throw Exception.new("Server #{ARGV[0]} does not exists in your configuration file.") 26 | end 27 | server = @config['servers'][ARGV[0]] 28 | else 29 | key = @config['servers'].keys.first 30 | server = @config['servers'][key] 31 | end 32 | 33 | puts "** Using the server:" 34 | puts server.inspect 35 | puts 36 | 37 | @api = BigBlueButton::BigBlueButtonApi.new(server['url'], server['secret'], server['version'].to_s, true) 38 | end 39 | -------------------------------------------------------------------------------- /features/create_meetings.feature: -------------------------------------------------------------------------------- 1 | Feature: Create rooms 2 | To be able to use BigBlueButton 3 | One needs to create a webconference room first 4 | 5 | @version-all 6 | Scenario: Create a new room 7 | When the create method is called with ALL the optional arguments 8 | Then the response is successful and well formatted 9 | And the meeting exists in the server 10 | 11 | @version-all 12 | Scenario: Create a new room with default parameters 13 | When the create method is called with NO optional arguments 14 | Then the response is successful and well formatted 15 | And the meeting exists in the server 16 | 17 | @version-all 18 | Scenario: Try to create a room with a duplicated meeting id 19 | When the create method is called with a duplicated meeting id 20 | Then the response is an error with the key "idNotUnique" 21 | 22 | @version-all @need-bot 23 | Scenario: Try to recreate a previously ended meeting 24 | Given the create method is called 25 | And the meeting is running 26 | And the meeting is forcibly ended 27 | When the create method is called again with the same meeting id 28 | Then the response is successful and well formatted 29 | And the meeting exists in the server 30 | -------------------------------------------------------------------------------- /features/join_meetings.feature: -------------------------------------------------------------------------------- 1 | Feature: Join meeting 2 | To participate in a meeting 3 | The user needs to be able to join a created meeting 4 | 5 | @version-all 6 | Scenario: Join a meeting as moderator 7 | Given that a meeting was created 8 | When the user tries to access the link to join the meeting as moderator 9 | Then he is redirected to the BigBlueButton client 10 | # can't really check if the user is in the session because in bbb he will 11 | # only be listed as an attendee after stabilishing a rtmp connection 12 | 13 | @version-all 14 | Scenario: Join a meeting as attendee 15 | Given that a meeting was created 16 | When the user tries to access the link to join the meeting as attendee 17 | Then he is redirected to the BigBlueButton client 18 | 19 | @version-all 20 | Scenario: Join a non created meeting 21 | Given the default BigBlueButton server 22 | When the user tries to access the link to join a meeting that was not created 23 | Then the response is an xml with the error "invalidMeetingIdentifier" 24 | 25 | @version-all 26 | Scenario: Try to join with the wrong password 27 | Given that a meeting was created 28 | When the user tries to access the link to join the meeting using a wrong password 29 | Then the response is an xml with the error "invalidPassword" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016 Mconf (http://mconf.org) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | This project is developed as part of Mconf (http://mconf.org). 21 | Contact information: 22 | Mconf: A scalable opensource multiconference system for web and mobile devices 23 | PRAV Labs - UFRGS - Porto Alegre - Brazil 24 | http://www.inf.ufrgs.br/prav/gtmconf 25 | -------------------------------------------------------------------------------- /features/step_definitions/recordings_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the user creates a meeting with the record flag$/ do 2 | steps %Q{ When the default BigBlueButton server } 3 | 4 | @req.id = Forgery(:basic).random_name("test-recordings") 5 | @req.name = @req.id 6 | @req.method = :create 7 | @req.opts = { :record => true } 8 | @req.response = @api.create_meeting(@req.id, @req.name, @req.opts) 9 | @req.mod_pass = @req.response[:moderatorPW] 10 | end 11 | 12 | When /^the meeting is set to be recorded$/ do 13 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 14 | @req.response[:returncode].should be true 15 | @req.response[:recording].should be true 16 | end 17 | 18 | When /^the user creates a meeting without the record flag$/ do 19 | steps %Q{ When the default BigBlueButton server } 20 | 21 | @req.id = Forgery(:basic).random_name("test-recordings") 22 | @req.name = @req.id 23 | @req.method = :create 24 | @req.opts = {} 25 | @req.response = @api.create_meeting(@req.id, @req.name) 26 | @req.mod_pass = @req.response[:moderatorPW] 27 | end 28 | 29 | When /^the meeting is not set to be recorded$/i do 30 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 31 | @req.response[:returncode].should be true 32 | @req.response[:recording].should be false 33 | end 34 | 35 | When /^the user calls the get_recordings method$/ do 36 | @req.method = :get_recordings 37 | @req.response = @api.get_recordings 38 | end 39 | -------------------------------------------------------------------------------- /features/recordings.feature: -------------------------------------------------------------------------------- 1 | @version-all 2 | Feature: Record a meeting and manage recordings 3 | To record a meeting or manage the recorded meeting 4 | One needs be able to list the recordings, publish and unpublish them 5 | 6 | # We don't check if meetings will really be recorded 7 | # To record a meeting we need at least audio in the session 8 | # And also it would probably that a long time to record and process test meetings 9 | # For now we'll have only basic tests in this feature 10 | 11 | Scenario: Set a meeting to be recorded 12 | Given the default BigBlueButton server 13 | When the user creates a meeting with the record flag 14 | Then the response is successful and well formatted 15 | And the meeting is set to be recorded 16 | 17 | Scenario: By default a meeting will not be recorded 18 | Given the default BigBlueButton server 19 | When the user creates a meeting without the record flag 20 | Then the response is successful and well formatted 21 | And the meeting is NOT set to be recorded 22 | 23 | Scenario: List the available recordings in a server with no recordings 24 | Given the default BigBlueButton server 25 | When the user calls the get_recordings method 26 | Then the response is successful and well formatted 27 | And the response has the messageKey "noRecordings" 28 | 29 | # Possible scenarios to test in the future 30 | # Scenario: Record a meeting # not only set to be recorded 31 | # Scenario: List the available recordings 32 | # Scenario: Publish a recording 33 | # Scenario: Unpublish a recording 34 | # Scenario: Remove a recording 35 | -------------------------------------------------------------------------------- /features/step_definitions/join_meetings_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the user tries to access the link to join the meeting as (.*)$/ do |role| 2 | case role.downcase.to_sym 3 | when :moderator 4 | @req.response = @api.join_meeting_url(@req.id, "any-mod", @req.mod_pass) 5 | when :attendee 6 | @req.response = @api.join_meeting_url(@req.id, "any-attendee", @req.response[:attendeePW]) 7 | end 8 | end 9 | 10 | When /^he is redirected to the BigBlueButton client$/ do 11 | # requests the join url and expects a redirect 12 | uri = URI(@req.response) 13 | response = Net::HTTP.get_response(uri) 14 | response.should be_a(Net::HTTPFound) 15 | response.code.should == "302" 16 | 17 | # check redirect to the correct bbb client page 18 | bbb_client_url = @api.url.gsub(URI(@api.url).path, "") + "/client/BigBlueButton.html" 19 | response["location"].should match(/#{bbb_client_url}/) 20 | end 21 | 22 | When /^the user tries to access the link to join a meeting that was not created$/ do 23 | @req.response = @api.join_meeting_url("should-not-exist-in-server", "any", "any") 24 | end 25 | 26 | When /^the response is an xml with the error "(.*)"$/ do |error| 27 | # requests the join url and expects an ok with an xml in the response body 28 | uri = URI(@req.response) 29 | response = Net::HTTP.get_response(uri) 30 | response.should be_a(Net::HTTPOK) 31 | response.code.should == "200" 32 | response["content-type"].should match(/text\/xml/) 33 | response.body.should match(/#{error}/) 34 | end 35 | 36 | When /^the user tries to access the link to join the meeting using a wrong password$/ do 37 | @req.response = @api.join_meeting_url(@req.id, "any-attendee", @req.mod_pass + "is wrong") 38 | end 39 | 40 | -------------------------------------------------------------------------------- /examples/join_example.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') 3 | 4 | require 'bigbluebutton_api' 5 | require 'prepare' 6 | 7 | begin 8 | prepare 9 | 10 | meeting_name = "Test Meeting" 11 | meeting_id = "test-meeting" 12 | moderator_name = "House" 13 | unless @api.is_meeting_running?(meeting_id) 14 | puts "---------------------------------------------------" 15 | options = { :moderatorPW => "54321", 16 | :attendeePW => "12345", 17 | :welcome => 'Welcome to my meeting', 18 | :dialNumber => '1-800-000-0000x00000#', 19 | :logoutURL => 'https://github.com/mconf/bigbluebutton-api-ruby', 20 | :maxParticipants => 25 } 21 | @api.create_meeting(meeting_name, meeting_id, options) 22 | puts "The meeting has been created. Please open a web browser and enter the meeting using either of the URLs below." 23 | 24 | puts 25 | puts "---------------------------------------------------" 26 | url = @api.join_meeting_url(meeting_id, moderator_name, options[:moderatorPW]) 27 | puts "1) Moderator URL = #{url}" 28 | 29 | puts 30 | puts "---------------------------------------------------" 31 | puts "Waiting 30 seconds for you to enter via browser" 32 | sleep(30) 33 | end 34 | 35 | unless @api.is_meeting_running?(meeting_id) 36 | puts "You have NOT entered the meeting" 37 | Kernel.exit! 38 | end 39 | puts "You have successfully entered the meeting" 40 | 41 | puts 42 | puts "---------------------------------------------------" 43 | response = @api.get_meeting_info(meeting_id, options[:moderatorPW]) 44 | puts "Meeting info:" 45 | puts response.inspect 46 | 47 | rescue Exception => ex 48 | puts "Failed with error #{ex.message}" 49 | puts ex.backtrace 50 | end 51 | -------------------------------------------------------------------------------- /features/step_definitions/create_meetings_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the create method is called with all the optional arguments$/i do 2 | steps %Q{ When that a meeting was created with all the optional arguments } 3 | end 4 | 5 | When /^the create method is called with no optional arguments$/i do 6 | steps %Q{ When the default BigBlueButton server } 7 | steps %Q{ When that a meeting was created } 8 | end 9 | 10 | When /^the create method is called with a duplicated meeting id$/ do 11 | steps %Q{ When the default BigBlueButton server } 12 | 13 | @req.id = Forgery(:basic).random_name("test-create") 14 | @req.name = @req.id 15 | 16 | # first meeting 17 | @req.method = :create 18 | @api.create_meeting(@req.id, @req.name) 19 | 20 | begin 21 | # duplicated meeting to be tested 22 | @req.method = :create 23 | @req.response = @api.create_meeting(@req.id, @req.name) 24 | rescue Exception => @req.exception 25 | end 26 | end 27 | 28 | When /^the create method is called$/ do 29 | steps %Q{ When the default BigBlueButton server } 30 | 31 | @req.id = Forgery(:basic).random_name("test-create") 32 | @req.name = @req.id 33 | @req.method = :create 34 | @req.response = @api.create_meeting(@req.id, @req.name) 35 | @req.mod_pass = @req.response[:moderatorPW] 36 | end 37 | 38 | When /^the meeting is forcibly ended$/ do 39 | @req.response = @api.end_meeting(@req.id, @req.mod_pass) 40 | end 41 | 42 | When /^the create method is called again with the same meeting id$/ do 43 | begin 44 | @req.method = :create 45 | @req.response = @api.create_meeting(@req.id, @req.name) 46 | rescue Exception => @req.exception 47 | end 48 | end 49 | 50 | When /^the meeting exists in the server$/ do 51 | @req.response = @api.get_meetings 52 | @req.response[:meetings].reject!{ |m| m[:meetingID] != @req.id } 53 | @req.response[:meetings].count.should == 1 54 | end 55 | -------------------------------------------------------------------------------- /features/step_definitions/end_meetings_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the method to end the meeting is called$/ do 2 | begin 3 | @req.method = :end 4 | @req.response = @api.end_meeting(@req.id, @req.mod_pass) 5 | rescue Exception => @req.exception 6 | end 7 | end 8 | 9 | When /^the meeting should be ended$/ do 10 | # the meeting only ends when everybody closes the session 11 | BigBlueButtonBot.finalize 12 | 13 | # wait for the meeting to end 14 | Timeout::timeout(@config['timeout_ending']) do 15 | running = true 16 | while running 17 | sleep 1 18 | response = @api.get_meetings 19 | selected = response[:meetings].reject!{ |m| m[:meetingID] != @req.id } 20 | running = selected[0].nil? ? false : selected[0][:running] 21 | end 22 | end 23 | 24 | end 25 | 26 | When /^the flag hasBeenForciblyEnded should be set$/ do 27 | @req.response[:hasBeenForciblyEnded].should be true 28 | end 29 | 30 | When /^the information returned by get_meeting_info is correct$/ do 31 | # check only what is different in a meeting that WAS ENDED 32 | # the rest is checked in other scenarios 33 | 34 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 35 | @req.response[:running].should be false 36 | @req.response[:hasBeenForciblyEnded].should be true 37 | @req.response[:participantCount].should == 0 38 | @req.response[:moderatorCount].should == 0 39 | @req.response[:attendees].should == [] 40 | 41 | # start and end times should be within half an hour from now 42 | @req.response[:startTime].should be_a(DateTime) 43 | @req.response[:startTime].should < DateTime.now + (0.5/24.0) 44 | @req.response[:startTime].should >= DateTime.now - (0.5/24.0) 45 | @req.response[:endTime].should be_a(DateTime) 46 | @req.response[:endTime].should < DateTime.now + (0.5/24.0) 47 | @req.response[:endTime].should >= DateTime.now - (0.5/24.0) 48 | @req.response[:endTime].should > @req.response[:startTime] 49 | end 50 | -------------------------------------------------------------------------------- /spec/data/hash_to_xml_complex.xml: -------------------------------------------------------------------------------- 1 | 2 | SUCCESS 3 | 4 | 5 | 7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268 6 | bd1811beecd20f24314819a52ec202bf446ab94b 7 | 8 | true 9 | Fri Nov 18 12:10:23 UTC 2011 10 | Fri Nov 18 12:12:25 UTC 2011 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | slides 19 | http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=7f5745 20 | 3 21 | 22 | 23 | 24 | 25 | 6c1d35b82e2552bb254d239540e4f994c4a77367-1316717270941 26 | 585a44eb32b526b100e12b7b755d971fbbd19ab0 27 | 28 | false 29 | 2011-09-22 18:47:55 UTC 30 | 2011-09-22 19:08:35 UTC 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | slides 39 | http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=6c1d35 40 | 0 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /features/support/features_helpers.rb: -------------------------------------------------------------------------------- 1 | module BigBlueButton 2 | module Features 3 | 4 | # Test object that stores information about an API request 5 | class APIRequest 6 | attr_accessor :opts # options hash 7 | attr_accessor :id # meetind id 8 | attr_accessor :mod_pass # moderator password 9 | attr_accessor :name # meeting name 10 | attr_accessor :method # last api method called 11 | attr_accessor :response # last api response 12 | attr_accessor :exception # last exception 13 | end 14 | 15 | # Global configurations 16 | module Configs 17 | class << self 18 | attr_accessor :cfg # configuration file 19 | attr_accessor :cfg_server # shortcut to the choosen server configs 20 | attr_accessor :req # api request 21 | 22 | def initialize_cfg 23 | config_file = File.join(File.dirname(__FILE__), '..', 'config.yml') 24 | unless File.exist? config_file 25 | throw Exception.new(config_file + " does not exists. Copy the example and configure your server.") 26 | end 27 | config = YAML.load_file(config_file) 28 | config 29 | end 30 | 31 | def initialize_cfg_server 32 | if ENV['SERVER'] 33 | unless self.cfg['servers'].has_key?(ENV['SERVER']) 34 | throw Exception.new("Server #{ENV['SERVER']} does not exists in your configuration file.") 35 | end 36 | server = self.cfg['servers'][ENV['SERVER']] 37 | else 38 | server = self.cfg['servers'].first[1] 39 | end 40 | server['version'] = '0.81' unless server.has_key?('version') 41 | server 42 | end 43 | 44 | def load 45 | self.cfg = initialize_cfg 46 | self.cfg_server = initialize_cfg_server 47 | self.req = BigBlueButton::Features::APIRequest.new 48 | end 49 | 50 | end 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /features/check_status.feature: -------------------------------------------------------------------------------- 1 | Feature: Check meeting configurations and status 2 | To be able to monitor BigBlueButton 3 | One needs to check the current meetings 4 | and the status and configurations of a meeting 5 | 6 | @version-all @need-bot 7 | Scenario: Check that a meeting is running 8 | Given that a meeting was created 9 | And the meeting is running 10 | Then the method isMeetingRunning informs that the meeting is running 11 | 12 | @version-all 13 | Scenario: Check that a meeting is NOT running 14 | Given that a meeting was created 15 | Then the method isMeetingRunning informs that the meeting is NOT running 16 | 17 | @version-all 18 | Scenario: Check the information of a meeting 19 | Given that a meeting was created 20 | When calling the method get_meeting_info 21 | Then the response is successful with no messages 22 | And it shows all the information of the meeting that was created 23 | 24 | # to make sure that getMeetingInfo is returning the proper info used in create 25 | @version-all 26 | Scenario: Check the information of a meeting created with optional parameters 27 | Given that a meeting was created with ALL the optional arguments 28 | When calling the method get_meeting_info 29 | Then the response is successful with no messages 30 | And it shows all the information of the meeting that was created 31 | 32 | @version-all @need-bot 33 | Scenario: Check the information of a meeting that is running and has attendees 34 | Given that a meeting was created 35 | And the meeting is running with 2 attendees 36 | When calling the method get_meeting_info 37 | Then the response is successful with no messages 38 | And it shows the 2 attendees in the list 39 | 40 | @version-all 41 | Scenario: List the meetings in a server 42 | Given that a meeting was created 43 | When calling the method get_meetings 44 | Then the response is successful with no messages 45 | And the created meeting should be listed in the response with proper information 46 | -------------------------------------------------------------------------------- /lib/bigbluebutton_modules.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | module BigBlueButton 4 | 5 | # A class to store the modules configuration to be passed in BigBlueButtonApi#create_meeting(). 6 | # 7 | # === Usage example: 8 | # 9 | # modules = BigBlueButton::BigBlueButtonModules.new 10 | # 11 | # # adds presentations by URL 12 | # modules.add_presentation(:url, "http://www.samplepdf.com/sample.pdf") 13 | # modules.add_presentation(:url, "http://www.samplepdf.com/sample2.pdf") 14 | # 15 | # # adds presentations from a local file 16 | # # the file will be opened and encoded in base64 17 | # modules.add_presentation(:file, "presentations/class01.ppt") 18 | # 19 | # # adds a base64 encoded presentation 20 | # modules.add_presentation(:base64, "JVBERi0xLjQKJ....[clipped here]....0CiUlRU9GCg==", "first-class.pdf") 21 | # 22 | class BigBlueButtonModules 23 | 24 | attr_accessor :presentation_urls 25 | attr_accessor :presentation_files 26 | attr_accessor :presentation_base64s 27 | 28 | def initialize 29 | @presentation_urls = [] 30 | @presentation_files = [] 31 | @presentation_base64s = [] 32 | end 33 | 34 | def add_presentation(type, value, name=nil) 35 | case type 36 | when :url 37 | @presentation_urls.push(value) 38 | when :file 39 | @presentation_files.push(value) 40 | when :base64 41 | @presentation_base64s.push([name, value]) 42 | end 43 | end 44 | 45 | def to_xml 46 | unless has_presentations? 47 | "" 48 | else 49 | xml = xml_header 50 | xml << presentations_to_xml 51 | xml << xml_footer 52 | end 53 | end 54 | 55 | private 56 | 57 | def has_presentations? 58 | !@presentation_urls.empty? or 59 | !@presentation_files.empty? or 60 | !@presentation_base64s.empty? 61 | end 62 | 63 | def xml_header 64 | "" 65 | end 66 | 67 | def xml_footer 68 | "" 69 | end 70 | 71 | def presentations_to_xml 72 | xml = "" 73 | @presentation_urls.each { |url| xml << "" } 74 | @presentation_base64s.each do |name, data| 75 | xml << "" 76 | xml << data 77 | xml << "" 78 | end 79 | @presentation_files.each do |filename| 80 | xml << "" 81 | File.open(filename, "r") do |file| 82 | xml << Base64.encode64(file.read) 83 | end 84 | xml << "" 85 | end 86 | xml << "" 87 | end 88 | 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /spec/bigbluebutton_modules_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | describe BigBlueButton::BigBlueButtonModules do 5 | let(:modules) { BigBlueButton::BigBlueButtonModules.new } 6 | 7 | describe "#presentation_urls" do 8 | subject { modules } 9 | it { should respond_to(:presentation_urls) } 10 | it { should respond_to("presentation_urls=") } 11 | end 12 | 13 | describe "#presentation_files" do 14 | subject { modules } 15 | it { should respond_to(:presentation_files) } 16 | it { should respond_to("presentation_files=") } 17 | end 18 | 19 | describe "#presentation_base64s" do 20 | subject { modules } 21 | it { should respond_to(:presentation_base64s) } 22 | it { should respond_to("presentation_base64s=") } 23 | end 24 | 25 | describe "#add_presentation" do 26 | context "when type = :url" do 27 | before { 28 | modules.add_presentation(:url, "http://anything") 29 | modules.add_presentation(:url, "http://anything2") 30 | } 31 | it { modules.presentation_urls.size.should == 2 } 32 | it { modules.presentation_urls.first.should == "http://anything" } 33 | it { modules.presentation_urls.last.should == "http://anything2" } 34 | end 35 | 36 | context "when type = :file" do 37 | before { 38 | modules.add_presentation(:file, "myfile.ppt") 39 | modules.add_presentation(:file, "myfile2.ppt") 40 | } 41 | it { modules.presentation_files.size.should == 2 } 42 | it { modules.presentation_files.first.should == "myfile.ppt" } 43 | it { modules.presentation_files.last.should == "myfile2.ppt" } 44 | end 45 | 46 | context "when type = :base64" do 47 | before { 48 | modules.add_presentation(:base64, "1234567890", "file1.pdf") 49 | modules.add_presentation(:base64, "0987654321", "file2.pdf") 50 | } 51 | it { modules.presentation_base64s.size.should == 2 } 52 | it { modules.presentation_base64s.first.should == ["file1.pdf", "1234567890"] } 53 | it { modules.presentation_base64s.last.should == ["file2.pdf", "0987654321"] } 54 | end 55 | end 56 | 57 | describe "#to_xml" do 58 | context "when nothing was added" do 59 | it { modules.to_xml.should == "" } 60 | end 61 | 62 | context "with presentations" do 63 | let(:file) { 64 | f = Tempfile.new(['file1', '.pdf']) 65 | f.write("First\nSecond") 66 | f.close 67 | f 68 | } 69 | let(:file_encoded) { 70 | File.open(file.path, "r") do |f| 71 | Base64.encode64(f.read) 72 | end 73 | } 74 | let(:xml) { 75 | "" + 76 | "" + 77 | "" + 78 | "" + 79 | "" + 80 | "1234567890" + 81 | "#{file_encoded}" + 82 | "" + 83 | "" 84 | } 85 | before { 86 | modules.add_presentation(:url, "http://anything") 87 | modules.add_presentation(:url, "http://anything2") 88 | modules.add_presentation(:base64, "1234567890", "file1.pdf") 89 | modules.add_presentation(:file, file.path) 90 | } 91 | it { modules.to_xml.should == xml } 92 | end 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /examples/overall_example.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') 3 | 4 | require 'bigbluebutton_api' 5 | require 'prepare' 6 | require 'securerandom' 7 | 8 | begin 9 | prepare 10 | 11 | puts 12 | puts "---------------------------------------------------" 13 | if @api.test_connection 14 | puts "Connection successful! continuing..." 15 | else 16 | puts "Connection failed! The server might be unreachable. Exiting..." 17 | Kernel.exit! 18 | end 19 | 20 | puts 21 | puts "---------------------------------------------------" 22 | version = @api.get_api_version 23 | puts "The API version of your server is #{version}" 24 | 25 | puts 26 | puts "---------------------------------------------------" 27 | response = @api.get_meetings 28 | puts "Existent meetings in your server:" 29 | response[:meetings].each do |m| 30 | puts " " + m[:meetingID] + ": " + m.inspect 31 | end 32 | 33 | puts 34 | puts "---------------------------------------------------" 35 | response = @api.get_recordings 36 | puts "Existent recordings in your server:" 37 | response[:recordings].each do |m| 38 | puts " " + m[:recordID] + ": " + m.inspect 39 | end 40 | 41 | puts 42 | puts "---------------------------------------------------" 43 | meeting_id = SecureRandom.hex(4) 44 | meeting_name = meeting_id 45 | moderator_name = "House" 46 | attendee_name = "Cameron" 47 | options = { :moderatorPW => "54321", 48 | :attendeePW => "12345", 49 | :welcome => 'Welcome to my meeting', 50 | :dialNumber => '1-800-000-0000x00000#', 51 | :voiceBridge => 70000 + rand(9999), 52 | :webVoice => SecureRandom.hex(4), 53 | :logoutURL => 'https://github.com/mconf/bigbluebutton-api-ruby', 54 | :maxParticipants => 25 } 55 | 56 | @api.create_meeting(meeting_name, meeting_id, options) 57 | puts "The meeting has been created. Please open a web browser and enter the meeting using either of the URLs below." 58 | 59 | puts 60 | puts "---------------------------------------------------" 61 | url = @api.join_meeting_url(meeting_id, moderator_name, options[:moderatorPW]) 62 | puts "1) Moderator URL = #{url}" 63 | puts "" 64 | url = @api.join_meeting_url(meeting_id, attendee_name, options[:attendeePW]) 65 | puts "2) Attendee URL = #{url}" 66 | 67 | puts 68 | puts "---------------------------------------------------" 69 | puts "Waiting 30 seconds for you to enter via browser" 70 | sleep(30) 71 | 72 | unless @api.is_meeting_running?(meeting_id) 73 | puts "You have NOT entered the meeting" 74 | Kernel.exit! 75 | end 76 | puts "You have successfully entered the meeting" 77 | 78 | puts 79 | puts "---------------------------------------------------" 80 | response = @api.get_meeting_info(meeting_id, options[:moderatorPW]) 81 | puts "Meeting info:" 82 | puts response.inspect 83 | 84 | puts 85 | puts "---------------------------------------------------" 86 | puts "Attendees:" 87 | response[:attendees].each do |m| 88 | puts " " + m[:fullName] + " (" + m[:userID] + "): " + m.inspect 89 | end 90 | 91 | 92 | puts 93 | puts "---------------------------------------------------" 94 | @api.end_meeting(meeting_id, options[:moderatorPW]) 95 | puts "The meeting has been ended" 96 | 97 | rescue Exception => ex 98 | puts "Failed with error #{ex.message}" 99 | puts ex.backtrace 100 | end 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bigbluebutton-api-ruby [](http://travis-ci.org/mconf/bigbluebutton-api-ruby) 2 | 3 | This is a ruby gem that provides access to the API of 4 | [BigBlueButton](http://bigbluebutton.org). See the documentation of the API 5 | [here](http://code.google.com/p/bigbluebutton/wiki/API). 6 | 7 | It enables a ruby application to interact with BigBlueButton by calling ruby 8 | methods instead of HTTP requests, making it a lot easier to interact with 9 | BigBlueButton. It also formats the responses to a ruby-friendly format and 10 | includes helper classes to deal with more complicated API calls, such as the 11 | pre-upload of slides. 12 | 13 | A few features it has: 14 | 15 | * Provides methods to perform all API calls and get the responses; 16 | * Converts the XML responses to ruby hashes, that are easier to work with; 17 | * Converts the string values returned to native ruby types. For instance: 18 | * Dates are converted DateTime objects (e.g. "Thu Sep 01 17:51:42 UTC 2011"); 19 | * Response codes are converted to boolean (e.g. "SUCCESS" becomes `true`); 20 | * Deals with errors (e.g. timeouts) throwing `BigBlueButtonException` exceptions; 21 | * Support to multiple BigBlueButton API versions (see below). 22 | 23 | ## Supported versions 24 | 25 | This gem is mainly used with [Mconf-Web](https://github.com/mconf/mconf-web) through 26 | [BigbluebuttonRails](https://github.com/mconf/bigbluebutton_rails). 27 | You can always use it as a reference for verions of dependencies and examples of how 28 | to use the gem. 29 | 30 | ### BigBlueButton 31 | 32 | The current version of this gem supports *all* the following versions of 33 | BigBlueButton: 34 | 35 | * 1.0 36 | * 0.9 (includes all 0.9.x) 37 | * 0.81 38 | * 0.8 39 | 40 | Older versions: 41 | 42 | * 0.7 (including 0.7, 0.71 and 0.71a): The last version with support to 0.7* 43 | is [version 44 | 1.2.0](https://github.com/mconf/bigbluebutton-api-ruby/tree/v1.2.0). It 45 | supports versions 0.7 and 0.8. 46 | * 0.64: see the branch `api-0.64`. The last version with support to 0.64 is 47 | [version 48 | 0.0.10](https://github.com/mconf/bigbluebutton-api-ruby/tree/v0.0.10). It 49 | supports versions 0.64 and 0.7. 50 | 51 | ### Ruby 52 | 53 | Tested in rubies: 54 | 55 | * ruby-2.2 **recommended** 56 | * ruby-2.1 57 | * ruby-2.0 (p353) 58 | * ruby-1.9.3 (p484) 59 | * ruby-1.9.2 (p290) 60 | 61 | Use these versions to be sure it will work. Other patches and patch versions of these 62 | rubies (e.g. ruby 1.9.3-p194 or 2.1.2) should work as well. 63 | 64 | ## Releases 65 | 66 | For a list of releases and release notes see 67 | [CHANGELOG.md](https://github.com/mconf/bigbluebutton-api-ruby/blob/master/CHANGELOG.md). 68 | 69 | ## Development 70 | 71 | Information for developers of `bigbluebutton-api-ruby` can be found in [our 72 | wiki](https://github.com/mconf/bigbluebutton-api-ruby/wiki). 73 | 74 | The development of this gem is guided by the requirements of the project 75 | Mconf. To know more about it visit the [project's 76 | wiki](https://github.com/mconf/wiki/wiki). 77 | 78 | ## License 79 | 80 | Distributed under The MIT License (MIT). See 81 | [LICENSE](https://github.com/mconf/bigbluebutton-api-ruby/blob/master/LICENSE) 82 | for the latest license, valid for all versions after 0.0.4 (including it), and 83 | [LICENSE_003](https://github.com/mconf/bigbluebutton-api-ruby/blob/master/LICENSE_003) 84 | for version 0.0.3 and all the previous versions. 85 | 86 | ## Contact 87 | 88 | This project is developed as part of Mconf (http://mconf.org). 89 | 90 | Mailing list: 91 | * mconf-dev@googlegroups.com 92 | 93 | Contact: 94 | * Mconf: A scalable opensource multiconference system for web and mobile devices 95 | * PRAV Labs - UFRGS - Porto Alegre - Brazil 96 | * http://www.inf.ufrgs.br/prav/gtmconf 97 | -------------------------------------------------------------------------------- /features/step_definitions/common_steps.rb: -------------------------------------------------------------------------------- 1 | # Common steps, used in several features 2 | 3 | When /^the default BigBlueButton server$/ do 4 | @api = BigBlueButton::BigBlueButtonApi.new(@config_server['url'], 5 | @config_server['secret'], 6 | @config_server['version'].to_s, 7 | @config['logger']) 8 | @api.timeout = @config['timeout_req'] 9 | end 10 | 11 | # default create call, with no optional parameters (only the mod pass) 12 | When /^that a meeting was created$/ do 13 | steps %Q{ When the default BigBlueButton server } 14 | 15 | @req.id = Forgery(:basic).random_name("test") 16 | @req.name = @req.id 17 | @req.mod_pass = Forgery(:basic).password 18 | @req.opts = { :moderatorPW => @req.mod_pass } 19 | @req.method = :create 20 | @req.response = @api.create_meeting(@req.id, @req.name, @req.opts) 21 | end 22 | 23 | When /^that a meeting was created with all the optional arguments$/i do 24 | steps %Q{ When the default BigBlueButton server } 25 | 26 | @req.id = Forgery(:basic).random_name("test-create") 27 | @req.name = @req.id 28 | @req.mod_pass = Forgery(:basic).password 29 | @req.opts = { :moderatorPW => @req.mod_pass, 30 | :attendeePW => Forgery(:basic).password, 31 | :welcome => Forgery(:lorem_ipsum).words(10), 32 | :dialNumber => Forgery(:basic).number(:at_most => 999999999).to_s, 33 | :logoutURL => Forgery(:internet).url, 34 | :voiceBridge => Forgery(:basic).number(:at_least => 70000, :at_most => 79999), 35 | :webVoice => Forgery(:basic).text, 36 | :maxParticipants => Forgery(:basic).number } 37 | if @api.version >= "0.8" 38 | @req.opts.merge!( { :record => false, 39 | :duration => Forgery(:basic).number(:at_least => 10, :at_most => 60), 40 | :meta_one => "one", :meta_TWO => "TWO" } ) 41 | end 42 | @req.method = :create 43 | @req.response = @api.create_meeting(@req.id, @req.name, @req.opts) 44 | end 45 | 46 | When /^the meeting is running$/ do 47 | steps %Q{ When the meeting is running with 1 attendees } 48 | end 49 | 50 | When /^the meeting is running with (\d+) attendees$/ do |count| 51 | BigBlueButtonBot.new(@api, @req.id, nil, count.to_i, @config['timeout_bot_start']) 52 | end 53 | 54 | When /^the response is an error with the key "(.*)"$/ do |key| 55 | @req.exception.should_not be_nil 56 | @req.exception.key.should == key 57 | end 58 | 59 | When /^the response is successful$/ do 60 | @req.response[:returncode].should be true 61 | end 62 | 63 | When /^the response is successful with no messages$/ do 64 | @req.response[:returncode].should be true 65 | @req.response[:messageKey].should == "" 66 | @req.response[:message].should == "" 67 | end 68 | 69 | When /^the response has the messageKey "(.*)"$/ do |key| 70 | @req.response[:messageKey].should == key 71 | @req.response[:message].should_not be_empty 72 | end 73 | 74 | When /^the response is successful and well formatted$/ do 75 | case @req.method 76 | when :create 77 | steps %Q{ When the response to the create method is successful and well formatted } 78 | when :end 79 | steps %Q{ When the response to the end method is successful and well formatted } 80 | when :get_recordings 81 | steps %Q{ When the response to the get_recordings method is successful and well formatted } 82 | end 83 | end 84 | 85 | When /^the response to the create method is successful and well formatted$/ do 86 | @req.response[:returncode].should be true 87 | @req.response[:meetingID].should == @req.id 88 | @req.response[:hasBeenForciblyEnded].should be false 89 | @req.response[:messageKey].should == "" 90 | @req.response[:message].should == "" 91 | 92 | @req.opts = {} if @req.opts.nil? 93 | if @req.opts.has_key?(:attendeePW) 94 | @req.response[:attendeePW].should == @req.opts[:attendeePW] 95 | else # auto generated password 96 | @req.response[:attendeePW].should be_a(String) 97 | @req.response[:attendeePW].should_not be_empty 98 | @req.opts[:attendeePW] = @req.response[:attendeePW] 99 | end 100 | if @req.opts.has_key?(:moderatorPW) 101 | @req.response[:moderatorPW].should == @req.opts[:moderatorPW] 102 | else # auto generated password 103 | @req.response[:moderatorPW].should be_a(String) 104 | @req.response[:moderatorPW].should_not be_empty 105 | @req.opts[:moderatorPW] = @req.response[:moderatorPW] 106 | end 107 | 108 | if @api.version >= "0.8" 109 | @req.response[:createTime].should be_a(Numeric) 110 | end 111 | end 112 | 113 | When /^the response to the end method is successful and well formatted$/ do 114 | @req.response[:returncode].should be true 115 | @req.response[:messageKey].should == "sentEndMeetingRequest" 116 | @req.response[:message].should_not be_empty 117 | end 118 | 119 | When /^the response to the get_recordings method is successful and well formatted$/ do 120 | @req.response[:returncode].should be true 121 | @req.response[:recordings].should == [] 122 | end 123 | -------------------------------------------------------------------------------- /features/step_definitions/check_status_steps.rb: -------------------------------------------------------------------------------- 1 | When /^the method isMeetingRunning informs that the meeting is running$/ do 2 | @req.response = @api.is_meeting_running?(@req.id) 3 | @req.response.should be true 4 | end 5 | 6 | When /^the method isMeetingRunning informs that the meeting is not running$/i do 7 | @req.response = @api.is_meeting_running?(@req.id) 8 | @req.response.should be false 9 | end 10 | 11 | When /^calling the method get_meetings$/ do 12 | @req.response = @api.get_meetings 13 | end 14 | 15 | When /^calling the method get_meeting_info$/ do 16 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 17 | end 18 | 19 | When /^the created meeting should be listed in the response with proper information$/ do 20 | @req.response[:meetings].size.should >= 1 21 | 22 | # the created meeting is in the list and has only 1 occurance 23 | found = @req.response[:meetings].reject{ |m| m[:meetingID] != @req.id } 24 | found.should_not be_nil 25 | found.size.should == 1 26 | 27 | # proper information in the meeting hash 28 | found = found[0] 29 | found[:attendeePW].should be_a(String) 30 | found[:attendeePW].should_not be_empty 31 | found[:moderatorPW].should == @req.mod_pass 32 | found[:hasBeenForciblyEnded].should be false 33 | found[:running].should be false 34 | if @api.version >= "0.8" 35 | found[:meetingName].should == @req.id 36 | found[:createTime].should be_a(Numeric) 37 | end 38 | end 39 | 40 | When /^it shows all the information of the meeting that was created$/ do 41 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 42 | @req.response[:meetingID].should == @req.id 43 | @req.response[:running].should be false 44 | @req.response[:hasBeenForciblyEnded].should be false 45 | @req.response[:startTime].should be_nil 46 | @req.response[:endTime].should be_nil 47 | @req.response[:participantCount].should == 0 48 | @req.response[:moderatorCount].should == 0 49 | @req.response[:attendees].should == [] 50 | @req.response[:messageKey].should == "" 51 | @req.response[:message].should == "" 52 | if @req.opts.has_key?(:attendeePW) 53 | @req.response[:attendeePW].should == @req.opts[:attendeePW] 54 | else # auto generated password 55 | @req.response[:attendeePW].should be_a(String) 56 | @req.response[:attendeePW].should_not be_empty 57 | @req.opts[:attendeePW] = @req.response[:attendeePW] 58 | end 59 | if @req.opts.has_key?(:moderatorPW) 60 | @req.response[:moderatorPW].should == @req.opts[:moderatorPW] 61 | else # auto generated password 62 | @req.response[:moderatorPW].should be_a(String) 63 | @req.response[:moderatorPW].should_not be_empty 64 | @req.opts[:moderatorPW] = @req.response[:moderatorPW] 65 | end 66 | 67 | if @api.version >= "0.8" 68 | @req.response[:meetingName].should == @req.id 69 | @req.response[:createTime].should be_a(Numeric) 70 | 71 | @req.opts.has_key?(:record) ? 72 | (@req.response[:recording].should == @req.opts[:record]) : 73 | (@req.response[:recording].should be false) 74 | @req.opts.has_key?(:maxParticipants) ? 75 | (@req.response[:maxUsers].should == @req.opts[:maxParticipants]) : 76 | (@req.response[:maxUsers].should == 20) 77 | @req.opts.has_key?(:voiceBridge) ? 78 | (@req.response[:voiceBridge].should == @req.opts[:voiceBridge]) : 79 | (@req.response[:voiceBridge].should be_a(Numeric)) 80 | 81 | if @req.opts.has_key?(:meta_one) 82 | @req.response[:metadata].size.should == 2 83 | @req.response[:metadata].should be_a(Hash) 84 | @req.response[:metadata].should include(:one => "one") 85 | @req.response[:metadata].should include(:two => "TWO") 86 | else 87 | @req.response[:metadata].should == {} 88 | end 89 | 90 | # note: the duration passed in the api call is not returned (so it won't be checked) 91 | end 92 | end 93 | 94 | Then /^it shows the (\d+) attendees in the list$/ do |count| 95 | # check only what is different in a meeting that is RUNNING 96 | # the rest is checked in other scenarios 97 | 98 | @req.response = @api.get_meeting_info(@req.id, @req.mod_pass) 99 | participants = count.to_i 100 | 101 | @req.response[:running].should be true 102 | @req.response[:moderatorCount].should > 0 103 | @req.response[:hasBeenForciblyEnded].should be false 104 | @req.response[:participantCount].should == participants 105 | @req.response[:attendees].size.should == 2 106 | 107 | # check the start time that should be within 2 hours from now 108 | # we check with this 2-hour time window to prevent errors if the 109 | # server clock is different 110 | @req.response[:startTime].should be_a(DateTime) 111 | @req.response[:startTime].should < DateTime.now + (1/24.0) 112 | @req.response[:startTime].should >= DateTime.now - (1/24.0) 113 | @req.response[:endTime].should be_nil 114 | 115 | # in the bot being used, bots are always moderators with these names 116 | @req.response[:attendees].sort! { |h1,h2| h1[:fullName] <=> h2[:fullName] } 117 | @req.response[:attendees][0][:fullName].should == "Bot 1" 118 | @req.response[:attendees][0][:role].should == :moderator 119 | @req.response[:attendees][1][:fullName].should == "Bot 2" 120 | @req.response[:attendees][1][:role].should == :moderator 121 | end 122 | -------------------------------------------------------------------------------- /spec/bigbluebutton_hash_to_xml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BigBlueButton::BigBlueButtonHash do 4 | 5 | describe ".from_xml" do 6 | it "simple example" do 7 | xml = "1" 8 | hash = { :returncode => "1" } 9 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 10 | end 11 | 12 | it "maintains all values as strings" do 13 | xml = "" \ 14 | " 1" \ 15 | " string" \ 16 | " true" \ 17 | "" 18 | hash = { :node1 => "1", :node2 => "string", :node3 => "true" } 19 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 20 | end 21 | 22 | it "works for xmls with multiple levels" do 23 | xml = "" \ 24 | " " \ 25 | " " \ 26 | " true" \ 27 | " " \ 28 | " " \ 29 | "" 30 | hash = { :node1 => { :node2 => { :node3 => "true" } } } 31 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 32 | end 33 | 34 | it "transforms CDATA fields to string" do 35 | xml = "" \ 36 | " " \ 37 | " " \ 38 | "" 39 | hash = { :name => "Evening Class", :course => "Advanced Ruby" } 40 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 41 | end 42 | 43 | it "transforms duplicated keys in arrays" do 44 | xml = "" \ 45 | " " \ 46 | " 1" \ 47 | " 2" \ 48 | "
3
" \ 49 | " 4" \ 50 | "
" \ 51 | "
" 52 | hash = { :meetings => { :meeting => [ "1", "2", { :details => "3" } ], 53 | :other => "4" } } 54 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 55 | end 56 | 57 | it "works with attributes" do 58 | xml = "" \ 59 | " 1" \ 60 | "" 61 | hash = { :meeting => { :attr1 => "v1", :content => "1" } } 62 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 63 | end 64 | 65 | it "complex real example" do 66 | xml = File.open("spec/data/hash_to_xml_complex.xml") 67 | 68 | hash = { :returncode => "SUCCESS", 69 | :recordings => 70 | { :recording => [ 71 | { :recordID => "7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268", 72 | :meetingID => "bd1811beecd20f24314819a52ec202bf446ab94b", 73 | :name => "Evening Class1", 74 | :published => "true", 75 | :startTime => "Fri Nov 18 12:10:23 UTC 2011", 76 | :endTime => "Fri Nov 18 12:12:25 UTC 2011", 77 | :metadata => 78 | { :course => "Fundamentals Of JAVA", 79 | :description => "List of recordings", 80 | :activity => "Evening Class1" }, 81 | :playback => 82 | { :format => 83 | { :type => "slides", 84 | :url => "http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=7f5745", 85 | :length => "3" } 86 | } 87 | }, 88 | { :recordID => "6c1d35b82e2552bb254d239540e4f994c4a77367-1316717270941", 89 | :meetingID => "585a44eb32b526b100e12b7b755d971fbbd19ab0", 90 | :name => "Test de fonctionnalité", 91 | :published => "false", 92 | :startTime => "2011-09-22 18:47:55 UTC", 93 | :endTime => "2011-09-22 19:08:35 UTC", 94 | :metadata => 95 | { :course => "Ressources technologiques", 96 | :activity => "Test de fonctionnalité", 97 | :recording => "true" }, 98 | :playback => 99 | { :format => 100 | { :type => "slides", 101 | :url => "http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=6c1d35", 102 | :length => "0" } 103 | } 104 | } ] 105 | } 106 | } 107 | BigBlueButton::BigBlueButtonHash.from_xml(xml).should == hash 108 | end 109 | end 110 | 111 | describe ".symbolize_keys" do 112 | it "converts string-keys to symbols" do 113 | before = { "one" => 1, "two" => 2, "three" => 3 } 114 | after = { :one => 1, :two => 2, :three => 3 } 115 | BigBlueButton::BigBlueButtonHash.symbolize_keys(before).should == after 116 | end 117 | 118 | it "maintains case" do 119 | before = { "One" => 1, "tWo" => 2, "thrEE" => 3 } 120 | after = { :One => 1, :tWo => 2, :thrEE => 3 } 121 | BigBlueButton::BigBlueButtonHash.symbolize_keys(before).should == after 122 | end 123 | 124 | it "works with multilevel hashes" do 125 | before = { "l1" => { "l2" => { "l3" => 1 } }, "l1b" => 2 } 126 | after = { :l1 => { :l2 => { :l3 => 1 } }, :l1b => 2 } 127 | BigBlueButton::BigBlueButtonHash.symbolize_keys(before).should == after 128 | end 129 | 130 | it "works with arrays" do 131 | before = { "a1" => [ "b1" => 1, "b2" => 2 ], "b2" => 2 } 132 | after = { :a1 => [ :b1 => 1, :b2 => 2 ], :b2 => 2 } 133 | BigBlueButton::BigBlueButtonHash.symbolize_keys(before).should == after 134 | end 135 | 136 | it "doesn't convert values" do 137 | before = { "a" => "a", "b" => "b" } 138 | after = { :a => "a", :b => "b" } 139 | BigBlueButton::BigBlueButtonHash.symbolize_keys(before).should == after 140 | end 141 | end 142 | 143 | end 144 | -------------------------------------------------------------------------------- /lib/bigbluebutton_formatter.rb: -------------------------------------------------------------------------------- 1 | module BigBlueButton 2 | 3 | # Helper class to format the response hash received when the BigBlueButtonApi makes API calls 4 | class BigBlueButtonFormatter 5 | attr_accessor :hash 6 | 7 | def initialize(hash) 8 | @hash = hash || {} 9 | end 10 | 11 | # converts a value in the @hash to boolean 12 | def to_boolean(key) 13 | unless @hash.has_key?(key) 14 | false 15 | else 16 | @hash[key] = @hash[key].downcase == "true" 17 | end 18 | end 19 | 20 | # converts a value in the @hash to int 21 | def to_int(key) 22 | unless @hash.has_key?(key) 23 | 0 24 | else 25 | @hash[key] = @hash[key].to_i rescue 0 26 | end 27 | end 28 | 29 | # converts a value in the @hash to string 30 | def to_string(key) 31 | @hash[key] = @hash[key].to_s 32 | end 33 | 34 | # converts a value in the @hash to DateTime 35 | def to_datetime(key) 36 | unless @hash.has_key?(key) and @hash[key] 37 | nil 38 | else 39 | # BBB >= 0.8 uses the unix epoch for all time related values 40 | # older versions use strings 41 | 42 | # a number but in a String class 43 | if (@hash[key].class == String && @hash[key].to_i.to_s == @hash[key]) 44 | value = @hash[key].to_i 45 | else 46 | value = @hash[key] 47 | end 48 | 49 | if value.is_a?(Numeric) 50 | result = value == 0 ? nil : DateTime.parse(Time.at(value/1000.0).to_s) 51 | else 52 | if (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? 53 | result = nil 54 | elsif value.is_a?(String) && (value.empty? || value.downcase == 'null') 55 | result = nil 56 | else 57 | # note: just in case the value comes as a string in the format: "Thu Sep 01 17:51:42 UTC 2011" 58 | result = DateTime.parse(value) 59 | end 60 | end 61 | 62 | @hash[key] = result 63 | end 64 | end 65 | 66 | # converts a value in the @hash to a symbol 67 | def to_sym(key) 68 | unless @hash.has_key?(key) 69 | "" 70 | else 71 | if @hash[key].instance_of?(Symbol) 72 | @hash[key] 73 | elsif @hash[key].empty? 74 | "" 75 | else 76 | @hash[key] = @hash[key].downcase.to_sym 77 | end 78 | end 79 | end 80 | 81 | # Default formatting for all responses given by a BBB server 82 | def default_formatting 83 | response = @hash 84 | 85 | # Adjust some values. There will always be a returncode, a message and a messageKey in the hash. 86 | response[:returncode] = response[:returncode].downcase == "success" # true instead of "SUCCESS" 87 | response[:messageKey] = "" if !response.has_key?(:messageKey) or response[:messageKey].empty? # "" instead of {} 88 | response[:message] = "" if !response.has_key?(:message) or response[:message].empty? # "" instead of {} 89 | 90 | @hash = response 91 | end 92 | 93 | # Default formatting for a meeting hash 94 | def self.format_meeting(meeting) 95 | f = BigBlueButtonFormatter.new(meeting) 96 | f.to_string(:meetingID) 97 | f.to_string(:meetingName) 98 | f.to_string(:moderatorPW) 99 | f.to_string(:attendeePW) 100 | f.to_boolean(:hasBeenForciblyEnded) 101 | f.to_boolean(:running) 102 | f.to_int(:createTime) if meeting.has_key?(:createTime) 103 | f.to_string(:dialNumber) 104 | f.to_int(:voiceBridge) 105 | f.to_int(:participantCount) 106 | f.to_int(:listenerCount) 107 | f.to_int(:videoCount) 108 | meeting 109 | end 110 | 111 | # Default formatting for an attendee hash 112 | def self.format_attendee(attendee) 113 | f = BigBlueButtonFormatter.new(attendee) 114 | f.to_string(:userID) 115 | f.to_sym(:role) 116 | attendee 117 | end 118 | 119 | # Default formatting for a recording hash 120 | def self.format_recording(rec) 121 | f = BigBlueButtonFormatter.new(rec) 122 | f.to_string(:recordID) 123 | f.to_string(:meetingID) 124 | f.to_string(:name) 125 | f.to_boolean(:published) 126 | f.to_datetime(:startTime) 127 | f.to_datetime(:endTime) 128 | if rec[:playback] and rec[:playback][:format] 129 | if rec[:playback][:format].is_a?(Hash) 130 | f2 = BigBlueButtonFormatter.new(rec[:playback][:format]) 131 | f2.to_int(:length) 132 | elsif rec[:playback][:format].is_a?(Array) 133 | rec[:playback][:format].each do |format| 134 | f2 = BigBlueButtonFormatter.new(format) 135 | f2.to_int(:length) 136 | end 137 | end 138 | end 139 | if rec[:metadata] 140 | rec[:metadata].each do |key, value| 141 | if value.nil? or value.empty? or value.split.empty? 142 | # removes any no {}s, []s, or " "s, should always be empty string 143 | rec[:metadata][key] = "" 144 | end 145 | end 146 | end 147 | rec 148 | end 149 | 150 | # Simplifies the XML-styled hash node 'first'. Its value will then always be an Array. 151 | # 152 | # For example, if the current hash is: 153 | # { :name => "Test", :attendees => { :attendee => [ { :name => "attendee1" }, { :name => "attendee2" } ] } } 154 | # 155 | # Calling: 156 | # flatten_objects(:attendees, :attendee) 157 | # 158 | # The hash will become: 159 | # { :name => "Test", :attendees => [ { :name => "attendee1" }, { :name => "attendee2" } ] } 160 | # 161 | # Other examples: 162 | # 163 | # # Hash: 164 | # { :name => "Test", :attendees => {} } 165 | # # Result: 166 | # { :name => "Test", :attendees => [] } 167 | # 168 | # # Hash: 169 | # { :name => "Test", :attendees => { :attendee => { :name => "attendee1" } } } 170 | # # Result: 171 | # { :name => "Test", :attendees => [ { :name => "attendee1" } ] } 172 | # 173 | def flatten_objects(first, second) 174 | if !@hash[first] or @hash[first].empty? 175 | collection = [] 176 | else 177 | node = @hash[first][second] 178 | if node.kind_of?(Array) 179 | collection = node 180 | else 181 | collection = [] 182 | collection << node 183 | end 184 | end 185 | @hash[first] = collection 186 | @hash 187 | end 188 | 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.9.1] - 2023-03-31 4 | * [#60] Prevent problems with double slashes on API calls [thanks to [@farhatahmad](https://github.com/farhatahmad)] 5 | 6 | [#60]: https://github.com/mconf/bigbluebutton-api-ruby/pull/60 7 | [1.9.1]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.9.0...v1.9.1 8 | 9 | ## [1.9.0] - 2022-05-03 10 | * [#56] Add support for checksum using SHA256 11 | 12 | [#56]: https://github.com/mconf/bigbluebutton-api-ruby/pull/56 13 | [1.9.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.8.0...v1.9.0 14 | 15 | ## [1.8.0] - 2021-12-06 16 | * [#43] Add keys to every `BigBlueButtonException`, to better identify them. 17 | * [#42] Change `BigBlueButtonException` to inherit from `StandardError` instead of `Exception`. 18 | * [#50] [BREAKING-CHANGE] Replace `debug` flag with a optional Logger. The application using the 19 | gem can pass its own logger as argument for the `BigBlueButtonApi` initialization. 20 | If none is passed, the gem will use its own default logger on `STDOUT` with `INFO` level. 21 | * [#40] Fix issue preventing documents from being preuploaded using the create call. 22 | * Fix parse of recordings with invalid times. It would break at `getRecordings` if one 23 | of them had an empty `startTime` or `endTime`. 24 | * Add a Dockerfile and compose to help run tests. 25 | * Fix deprecated `TimeoutError` constant. 26 | * [#35] Make `get_recordings` accept multiple `state` params. 27 | * [#34] Update `rubyzip` gem to the newest version with no vulnerability, from 1.2.2 to 1.3.0 28 | * [#33] Upgrade dependencies: 29 | - `childprocess` from 0.3.2 to 1.0.1 30 | - `ffi` from 1.0.11 to 1.9.24 31 | - `json` from 1.8.3 to 1.8.6 32 | - `nokogiri` from 1.6.6.2 to 1.10.4 33 | - `rack` from 1.4.1 to 1.6.11 34 | - `rdoc` from 3.12 to 3.12.1 35 | - `rubyzip` from 0.9.8 to 1.2.2 36 | 37 | ## [1.7.0] - 2018-08-17 38 | 39 | * [#29] Add support to the API call `updateRecordings` introduced in BigBlueButton 1.1. 40 | * [#31] Fixed issue with length=nil breaking multiple recording formats. 41 | * Call `setConfigXML` via POST and change encoding method. Fixes issues with special 42 | characters (such as `*`) in the config.xml. 43 | * Add method to return the URL to `/check`. 44 | 45 | ## [1.6.0] - 2016-06-15 46 | 47 | * Rename BigBlueButtonApi#salt to #secret 48 | 49 | ## [1.5.0] - 2016-04-07 50 | 51 | * Add 1.0 as a supported version of BigBlueButton. 52 | * [#1686] Automatically set the version number of a server by fetching it from 53 | its API. 54 | * [#1686] Fix comparison of config.xml strings that would sometimes thing XMLs 55 | were different in cases when they were not. 56 | * [#1695] Add support for servers that use HTTPS. 57 | 58 | ## [1.4.0] - 2015-07-20 59 | 60 | * Updated default ruby version to 2.2.0. 61 | * Add support for BigBlueButton 0.9 (includes all 0.9.x). Consists only in 62 | accepting the version "0.9", since this new API version doesn't break 63 | compatibility with the previous version and also doesn't add new API 64 | calls. 65 | 66 | ## [1.3.0] - 2014-05-11 67 | 68 | * Drop support to BigBlueButton 0.7 and add BigBlueButton to 0.81. Includes 69 | support to the new API calls `getDefaultConfigXML` and `setConfigXML`. 70 | * Reviewed all methods responses (some changed a bit) and documentation. 71 | * Allow non standard options to be passed to **all** API calls. 72 | * Removed method `join_meeting` that was usually not used. Should always use 73 | `join_meeting_url`. 74 | * Moved the hash extension method `from_xml` to its own class to prevent 75 | conflicts with Rails. See 76 | https://github.com/mconf/bigbluebutton-api-ruby/pull/6. 77 | 78 | ## [1.2.0] - 2013-03-13 79 | 80 | * Allow non standard options to be passed to some API calls. These API calls are: create_meeting, join_meeting_url, join_meeting, get_recordings. 81 | Useful for development of for custom versions of BigBlueButton. 82 | * Accept :record as boolean in create_meeting 83 | * Better formatting of data returned by get_recordings 84 | 85 | ## [1.1.1] - 2013-01-30 86 | 87 | * BigBlueButtonApi can now receive http headers to be sent in all get/post 88 | requests 89 | 90 | ## [1.1.0] - 2012-05-04 91 | 92 | * Updated ruby to 1.9.3-194. 93 | * Support to BigBlueButton 0.4 rc1. 94 | 95 | ## [1.0.0] - 2012-05-04 96 | 97 | * Version 0.1.0 renamed to 1.0.0. 98 | 99 | ## [0.1.0] - 2011-11-25 100 | 101 | * Support to BigBlueButton 0.8: 102 | * New methods for recordings: get_recordings, publish_recordings, 103 | delete_recordings 104 | * Pre-upload of slides in create_meeting 105 | * New parameters added in the already existent methods 106 | * For more information see BigBlueButton docs at 107 | http://code.google.com/p/bigbluebutton/wiki/API#Version_0.8 108 | * Method signature changes: create_meeting, join_meeting_url and 109 | join_meeting. Optional parameters are now passed using a hash. 110 | * Integration tests for the entire library using cucumber. 111 | * Changed the XML parser to xml-simple (especially to solve issues with 112 | CDATA values). 113 | 114 | ## [0.0.11] - 2011-09-01 115 | 116 | * The file "bigbluebutton-api" was renamed to "bigbluebutton_api". All 117 | "require" calls must be updated. 118 | * Splitted the library in more files (more modular) and created rspec tests 119 | for it. 120 | * Added a BigBlueButtonApi.timeout attribute to timeout get requests and 121 | avoid blocks when the server is down. Defaults to 2 secs. 122 | * New method last_http_response to access the last HTTP response object. 123 | * Automatically detects the BBB server version if not informed by the user. 124 | 125 | ## [0.0.10] - 2011-04-28 126 | 127 | * Returning hash now will **always** have these 3 values: :returncode 128 | (boolean), :messageKey (string) and :message (string). 129 | * Some values in the hash are now converted to a fixed variable type to 130 | avoid inconsistencies: 131 | * :meetingID (string) 132 | * :attendeePW (string) 133 | * :moderatorPW (string) 134 | * :running (boolean) 135 | * :hasBeenForciblyEnded (boolean) 136 | * :endTime and :startTime (DateTime or nil) 137 | 138 | ## [0.0.9] - 2011-04-08 139 | 140 | * Simplied "attendees" part of the hash returned in get_meeting_info. Same 141 | thing done for get_meetings. 142 | 143 | ## [0.0.8] - 2011-04-06 144 | 145 | * New method get_api_version that returns the version of the server API (>= 0.7). 146 | * New simplified hash syntax for get_meetings. See docs for details. 147 | 148 | ## [0.0.7] - 2011-04-06 149 | 150 | ## [0.0.6] - 2011-04-05 151 | 152 | * New method test_connection. 153 | * Added comparison method for APIs. 154 | * Methods attendee_url and moderator_url deprecated. Use join_meeting_url. 155 | * Better exception throwing when the user is unreachable or the response is incorrect. 156 | * BigBlueButtonException has now a "key" attribute to store the 157 | "messageKey" returned by BBB in failures. 158 | 159 | ## 0.0.4 160 | 161 | * Added support for BigBlueButton 0.7. 162 | * Gem renamed from 'bigbluebutton' to 'bigbluebutton-api-ruby'. 163 | * API functions now return a hash and instead of the XML returned by BBB. 164 | The XML is converted to a hash that uses symbols as keys and groups keys 165 | with the same name. 166 | 167 | ## 0.0.3 168 | 169 | * Fixes module issue preventing proper throwing of exceptions. 170 | 171 | ## 0.0.1 172 | 173 | * This is the first version of this gem. It provides an implementation of 174 | the 0.64 bbb API, with the following exceptions: 175 | * Does not implement meeting token, and instead relies on meeting id as 176 | the unique identifier for a meeting. 177 | * Documentation suggests there is way to call join_meeting as API call 178 | (instead of browser URL). This call currently does not work as 179 | documented. 180 | 181 | 182 | [#50]: https://github.com/mconf/bigbluebutton-api-ruby/pull/50 183 | [#43]: https://github.com/mconf/bigbluebutton-api-ruby/pull/43 184 | [#42]: https://github.com/mconf/bigbluebutton-api-ruby/pull/42 185 | [#40]: https://github.com/mconf/bigbluebutton-api-ruby/pull/40 186 | [#35]: https://github.com/mconf/bigbluebutton-api-ruby/pull/35 187 | [#34]: https://github.com/mconf/bigbluebutton-api-ruby/pull/34 188 | [#33]: https://github.com/mconf/bigbluebutton-api-ruby/pull/33 189 | [#31]: https://github.com/mconf/bigbluebutton-api-ruby/pull/31 190 | [#29]: https://github.com/mconf/bigbluebutton-api-ruby/pull/29 191 | 192 | 193 | [1.8.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.7.0...v1.8.0 194 | [1.7.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.6.0...v1.7.0 195 | [1.6.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.5.0...v1.6.0 196 | [1.5.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.4.0...v1.5.0 197 | [1.4.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.3.0...v1.4.0 198 | [1.3.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.2.0...v1.3.0 199 | [1.2.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.1.1...v1.2.0 200 | [1.1.1]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.1.0...v1.1.1 201 | [1.1.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v1.0.0...v1.1.0 202 | [1.0.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.1.0...v1.0.0 203 | [0.1.0]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.11...v0.1.0 204 | [0.0.11]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.10...v0.0.11 205 | [0.0.10]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.9...v0.0.10 206 | [0.0.9]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.8...v0.0.9 207 | [0.0.8]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.7...v0.0.8 208 | [0.0.7]: https://github.com/mconf/bigbluebutton-api-ruby/compare/v0.0.6...v0.0.7 209 | [0.0.6]: https://github.com/mconf/bigbluebutton-api-ruby/compare/b586c4726d32e9c30139357bcbe2067f868ff36c...v0.0.6 210 | -------------------------------------------------------------------------------- /spec/bigbluebutton_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BigBlueButton::BigBlueButtonFormatter do 4 | 5 | describe "#hash" do 6 | subject { BigBlueButton::BigBlueButtonFormatter.new({}) } 7 | it { subject.should respond_to(:hash) } 8 | it { subject.should respond_to(:"hash=") } 9 | end 10 | 11 | describe "#initialize" do 12 | context "with a hash" do 13 | let(:hash) { { :param1 => "123", :param2 => 123, :param3 => true } } 14 | subject { BigBlueButton::BigBlueButtonFormatter.new(hash) } 15 | it { subject.hash.should == hash } 16 | end 17 | 18 | context "without a hash" do 19 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 20 | it { subject.hash.should == { } } 21 | end 22 | end 23 | 24 | describe "#to_string" do 25 | let(:hash) { { :param1 => "123", :param2 => 123, :param3 => true } } 26 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(hash) } 27 | before { 28 | formatter.to_string(:param1) 29 | formatter.to_string(:param2) 30 | formatter.to_string(:param3) 31 | } 32 | it { hash[:param1].should == "123" } 33 | it { hash[:param2].should == "123" } 34 | it { hash[:param3].should == "true" } 35 | 36 | context "returns empty string if the param doesn't exists" do 37 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => 1 }) } 38 | it { subject.to_string(:inexistent).should == "" } 39 | end 40 | 41 | context "returns empty string if the hash is nil" do 42 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 43 | it { subject.to_string(:inexistent).should == "" } 44 | end 45 | end 46 | 47 | describe "#to_boolean" do 48 | let(:hash) { { :true1 => "TRUE", :true2 => "true", :false1 => "FALSE", :false2 => "false" } } 49 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(hash) } 50 | before { 51 | formatter.to_boolean(:true1) 52 | formatter.to_boolean(:true2) 53 | formatter.to_boolean(:false1) 54 | formatter.to_boolean(:false2) 55 | } 56 | it { hash[:true1].should be true } 57 | it { hash[:true2].should be true } 58 | it { hash[:false1].should be false } 59 | it { hash[:false2].should be false } 60 | 61 | context "returns false if the param doesn't exists" do 62 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => 1}) } 63 | it { subject.to_boolean(:inexistent).should == false } 64 | end 65 | 66 | context "returns false if the hash is nil" do 67 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 68 | it { subject.to_boolean(:inexistent).should == false } 69 | end 70 | end 71 | 72 | describe "#to_datetime" do 73 | let(:hash) { 74 | { :param1 => "Thu Sep 01 17:51:42 UTC 2011", 75 | :param2 => "Thu Sep 08", 76 | :param3 => 1315254777880, 77 | :param4 => "1315254777880", 78 | :param5 => "0", 79 | :param6 => 0, 80 | :param7 => "NULL", 81 | :param8 => nil 82 | } 83 | } 84 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(hash) } 85 | before { 86 | formatter.to_datetime(:param1) 87 | formatter.to_datetime(:param2) 88 | formatter.to_datetime(:param3) 89 | formatter.to_datetime(:param4) 90 | formatter.to_datetime(:param5) 91 | formatter.to_datetime(:param6) 92 | formatter.to_datetime(:param7) 93 | } 94 | it { hash[:param1].should == DateTime.parse("Thu Sep 01 17:51:42 UTC 2011") } 95 | it { hash[:param2].should == DateTime.parse("Thu Sep 08") } 96 | it { hash[:param3].should == DateTime.parse("2011-09-05 17:32:57 -0300") } 97 | it { hash[:param4].should == DateTime.parse("2011-09-05 17:32:57 -0300") } 98 | it { hash[:param5].should == nil } 99 | it { hash[:param6].should == nil } 100 | it { hash[:param7].should == nil } 101 | it { hash[:param8].should == nil } 102 | 103 | context "returns nil if" do 104 | context "the param doesn't exists" do 105 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => 1}) } 106 | it { subject.to_datetime(:inexistent).should == nil } 107 | end 108 | 109 | context "the hash is nil" do 110 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 111 | it { subject.to_datetime(:inexistent).should == nil } 112 | end 113 | 114 | context "the value is an empty string" do 115 | subject { BigBlueButton::BigBlueButtonFormatter.new({ param1: '' }) } 116 | it { subject.to_datetime(:param1).should == nil } 117 | end 118 | 119 | context "the value is nil" do 120 | subject { BigBlueButton::BigBlueButtonFormatter.new({ param1: nil }) } 121 | it { subject.to_datetime(:param1).should == nil } 122 | end 123 | 124 | context "the value is an empty hash" do 125 | subject { BigBlueButton::BigBlueButtonFormatter.new({ param1: {} }) } 126 | it { subject.to_datetime(:param1).should == nil } 127 | end 128 | 129 | context "the value is an empty array" do 130 | subject { BigBlueButton::BigBlueButtonFormatter.new({ param1: [] }) } 131 | it { subject.to_datetime(:param1).should == nil } 132 | end 133 | 134 | ['null', 'NULL'].each do |v| 135 | context "the value is '#{v}'" do 136 | subject { BigBlueButton::BigBlueButtonFormatter.new({ param1: v }) } 137 | it { subject.to_datetime(:param1).should == nil } 138 | end 139 | end 140 | end 141 | end 142 | 143 | describe "#to_sym" do 144 | let(:hash) { { :param1 => :sym1, :param2 => "sym2", :param3 => "SyM3" } } 145 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(hash) } 146 | before { 147 | formatter.to_sym(:param1) 148 | formatter.to_sym(:param2) 149 | formatter.to_sym(:param3) 150 | } 151 | it { hash[:param1].should == :sym1 } 152 | it { hash[:param2].should == :sym2 } 153 | it { hash[:param3].should == :sym3 } 154 | 155 | context "returns empty string if the param doesn't exists" do 156 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => 1 }) } 157 | it { subject.to_string(:inexistent).should == "" } 158 | end 159 | 160 | context "returns empty string if the hash is nil" do 161 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 162 | it { subject.to_string(:inexistent).should == "" } 163 | end 164 | 165 | context "returns empty string if the value to be converted is an empty string" do 166 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => "" }) } 167 | it { subject.to_string(:param).should == "" } 168 | end 169 | end 170 | 171 | describe "#to_int" do 172 | let(:hash) { { :param1 => 5, :param2 => "5" } } 173 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(hash) } 174 | before { 175 | formatter.to_int(:param1) 176 | formatter.to_int(:param2) 177 | } 178 | it { hash[:param1].should == 5 } 179 | it { hash[:param2].should == 5 } 180 | 181 | context "returns 0 if the param doesn't exists" do 182 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => 1 }) } 183 | it { subject.to_int(:inexistent).should == 0 } 184 | end 185 | 186 | context "returns 0 if the hash is nil" do 187 | subject { BigBlueButton::BigBlueButtonFormatter.new(nil) } 188 | it { subject.to_int(:inexistent).should == 0 } 189 | end 190 | 191 | context "returns 0 if the value to be converted is invalid" do 192 | subject { BigBlueButton::BigBlueButtonFormatter.new({ :param => "invalid" }) } 193 | it { subject.to_int(:param).should == 0 } 194 | end 195 | end 196 | 197 | describe "#default_formatting" do 198 | let(:input) { { :returncode => "SUCCESS", :messageKey => "mkey", :message => "m" } } 199 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new(input) } 200 | 201 | context "standard case" do 202 | let(:expected_output) { { :returncode => true, :messageKey => "mkey", :message => "m" } } 203 | subject { formatter.default_formatting } 204 | it { subject.should == expected_output } 205 | end 206 | 207 | context "when :returncode should be false" do 208 | before { input[:returncode] = "ERROR" } 209 | subject { formatter.default_formatting } 210 | it { subject[:returncode].should be false } 211 | end 212 | 213 | context "when :messageKey is empty" do 214 | before { input[:messageKey] = {} } 215 | subject { formatter.default_formatting } 216 | it { subject[:messageKey].should == "" } 217 | end 218 | 219 | context "when :messageKey is nil" do 220 | before { input.delete(:messageKey) } 221 | subject { formatter.default_formatting } 222 | it { subject[:messageKey].should == "" } 223 | end 224 | 225 | context "when :message is empty" do 226 | before { input[:message] = {} } 227 | subject { formatter.default_formatting } 228 | it { subject[:message].should == "" } 229 | end 230 | 231 | context "when there's no :message key" do 232 | before { input.delete(:message) } 233 | subject { formatter.default_formatting } 234 | it { subject[:message].should == "" } 235 | end 236 | end 237 | 238 | describe ".format_meeting" do 239 | let(:hash) { 240 | { :meetingID => 123, :meetingName => 123, :moderatorPW => 111, :attendeePW => 222, 241 | :hasBeenForciblyEnded => "FALSE", :running => "TRUE", :createTime => "123456789", 242 | :dialNumber => 1234567890, :voiceBridge => "12345", 243 | :participantCount => "10", :listenerCount => "3", :videoCount => "5" } 244 | } 245 | 246 | subject { BigBlueButton::BigBlueButtonFormatter.format_meeting(hash) } 247 | it { subject[:meetingID].should == "123" } 248 | it { subject[:meetingName].should == "123" } 249 | it { subject[:moderatorPW].should == "111" } 250 | it { subject[:attendeePW].should == "222" } 251 | it { subject[:hasBeenForciblyEnded].should == false } 252 | it { subject[:running].should == true } 253 | it { subject[:createTime].should == 123456789 } 254 | it { subject[:voiceBridge].should == 12345 } 255 | it { subject[:dialNumber].should == "1234567890" } 256 | it { subject[:participantCount].should == 10 } 257 | it { subject[:listenerCount].should == 3 } 258 | it { subject[:videoCount].should == 5 } 259 | end 260 | 261 | describe ".format_attendee" do 262 | let(:hash) { { :userID => 123, :fullName => "Cameron", :role => "VIEWER" } } 263 | 264 | subject { BigBlueButton::BigBlueButtonFormatter.format_attendee(hash) } 265 | it { subject[:userID].should == "123" } 266 | it { subject[:fullName].should == "Cameron" } 267 | it { subject[:role].should == :viewer } 268 | end 269 | 270 | describe ".format_recording" do 271 | let(:hash) { 272 | { :recordID => 123, :meetingID => 123, :name => 123, :published => "true", 273 | :startTime => "Thu Mar 04 14:05:56 UTC 2010", 274 | :endTime => "Thu Mar 04 15:01:01 UTC 2010", 275 | :metadata => { 276 | :title => "Test Recording", 277 | :empty1 => nil, 278 | :empty2 => {}, 279 | :empty3 => [], 280 | :empty4 => " ", 281 | :empty5 => "\n\t" 282 | }, 283 | :playback => { 284 | :format => [ 285 | { :type => "simple", 286 | :url => "http://server.com/simple/playback?recordID=183f0bf3a0982a127bdb8161-1", 287 | :length => "62" }, 288 | { :type => "simple", 289 | :url => "http://server.com/simple/playback?recordID=183f0bf3a0982a127bdb8161-1", 290 | :length => "48" } 291 | ] 292 | } 293 | } 294 | } 295 | 296 | context do 297 | subject { BigBlueButton::BigBlueButtonFormatter.format_recording(hash) } 298 | it { subject[:recordID].should == "123" } 299 | it { subject[:meetingID].should == "123" } 300 | it { subject[:name].should == "123" } 301 | it { subject[:startTime].should == DateTime.parse("Thu Mar 04 14:05:56 UTC 2010") } 302 | it { subject[:endTime].should == DateTime.parse("Thu Mar 04 15:01:01 UTC 2010") } 303 | it { subject[:playback][:format][0][:length].should == 62 } 304 | it { subject[:metadata][:empty1].should == "" } 305 | it { subject[:metadata][:empty2].should == "" } 306 | it { subject[:metadata][:empty3].should == "" } 307 | it { subject[:metadata][:empty4].should == "" } 308 | it { subject[:metadata][:empty5].should == "" } 309 | end 310 | 311 | context "doesn't fail without playback formats" do 312 | before { hash.delete(:playback) } 313 | subject { BigBlueButton::BigBlueButtonFormatter.format_recording(hash) } 314 | it { subject[:playback].should == nil } 315 | end 316 | end 317 | 318 | describe "#flatten_objects" do 319 | let(:formatter) { BigBlueButton::BigBlueButtonFormatter.new({ }) } 320 | 321 | context "standard case" do 322 | context "when the target key is empty" do 323 | let(:hash) { { :objects => {} } } 324 | before { formatter.hash = hash } 325 | subject { formatter.flatten_objects(:objects, :object) } 326 | it { subject.should == { :objects => [] } } 327 | end 328 | 329 | context "when the target key doesn't exist in the hash" do 330 | let(:hash) { { } } 331 | before { formatter.hash = hash } 332 | subject { formatter.flatten_objects(:objects, :object) } 333 | it { subject.should == { :objects => [] } } # adds the one the doesn't exist 334 | end 335 | 336 | context "when there's only one object in the list" do 337 | let(:object_hash) { { :id => 1 } } 338 | let(:hash) { { :objects => { :object => object_hash } } } 339 | before { formatter.hash = hash } 340 | subject { formatter.flatten_objects(:objects, :object) } 341 | it { subject.should == { :objects => [ object_hash ] } } 342 | end 343 | 344 | context "when there are several objects in the list" do 345 | let(:object_hash1) { { :id => 1 } } 346 | let(:object_hash2) { { :id => 2 } } 347 | let(:hash) { { :objects => { :object => [ object_hash1, object_hash2 ] } } } 348 | before { formatter.hash = hash } 349 | subject { formatter.flatten_objects(:objects, :object) } 350 | it { subject.should == { :objects => [ object_hash1, object_hash2 ] } } 351 | end 352 | end 353 | 354 | context "using different keys" do 355 | let(:hash1) { { :any => 1 } } 356 | let(:hash2) { { :any => 2 } } 357 | let(:collection_hash) { { :foos => { :bar => [ hash1, hash2 ] } } } 358 | before { formatter.hash = collection_hash } 359 | subject { formatter.flatten_objects(:foos, :bar) } 360 | it { subject.should == { :foos => [ hash1, hash2 ] } } 361 | end 362 | 363 | end 364 | 365 | 366 | end 367 | -------------------------------------------------------------------------------- /lib/bigbluebutton_api.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'rexml/document' 3 | require 'digest/sha1' 4 | require 'digest/sha2' 5 | require 'rubygems' 6 | require 'bigbluebutton_hash_to_xml' 7 | require 'bigbluebutton_exception' 8 | require 'bigbluebutton_formatter' 9 | require 'bigbluebutton_modules' 10 | require 'logger' 11 | 12 | module BigBlueButton 13 | 14 | # This class provides access to the BigBlueButton API. For more details see README. 15 | # 16 | # Sample usage of the API is as follows: 17 | # 1. Create a meeting with create_meeting; 18 | # 2. Redirect a user to the URL returned by join_meeting_url; 19 | # 3. Get information about the meetings with get_meetings and get_meeting_info; 20 | # 4. To force meeting to end, call end_meeting . 21 | # 22 | # Important info about the data returned by the methods: 23 | # * The XML returned by BigBlueButton is converted to a BigBlueButton::BigBlueButtonHash. See each method's documentation 24 | # for examples. 25 | # * Three values will *always* exist in the hash: 26 | # * :returncode (boolean) 27 | # * :messageKey (string) 28 | # * :message (string) 29 | # * Some of the values returned by BigBlueButton are converted to better represent the data. 30 | # Some of these are listed bellow. They will *always* have the type informed: 31 | # * :meetingID (string) 32 | # * :attendeePW (string) 33 | # * :moderatorPW (string) 34 | # * :running (boolean) 35 | # * :hasBeenForciblyEnded (boolean) 36 | # * :endTime and :startTime (DateTime or nil) 37 | # 38 | # For more information about the API, see the documentation at: 39 | # * http://code.google.com/p/bigbluebutton/wiki/API 40 | # 41 | class BigBlueButtonApi 42 | 43 | # URL to a BigBlueButton server (e.g. http://demo.bigbluebutton.org/bigbluebutton/api) 44 | attr_accessor :url 45 | 46 | # Shared secret for this server 47 | attr_accessor :secret 48 | 49 | # API version e.g. 0.81 50 | attr_accessor :version 51 | 52 | # logger to log reponses and infos 53 | attr_accessor :logger 54 | 55 | # Maximum wait time for HTTP requests (secs) 56 | attr_accessor :timeout 57 | 58 | # HTTP headers to be sent in all GET/POST requests 59 | attr_accessor :request_headers 60 | 61 | # Array with the version of BigBlueButton supported 62 | # TODO: do we really need an accessor? shouldn't be internal? 63 | attr_accessor :supported_versions 64 | 65 | # Initializes an instance 66 | # url:: URL to a BigBlueButton server (e.g. http://demo.bigbluebutton.org/bigbluebutton/api) 67 | # secret:: Shared secret for this server 68 | # version:: API version e.g. 0.81 69 | # logger:: Logger object to log actions (so apps can use their own loggers) 70 | # sha256:: Flag to use sha256 when hashing url contents for checksum 71 | def initialize(url, secret, version=nil, logger=nil, sha256=false) 72 | @supported_versions = ['0.8', '0.81', '0.9', '1.0'] 73 | @url = url.chomp('/') 74 | @secret = secret 75 | @timeout = 10 # default timeout for api requests 76 | @request_headers = {} # http headers sent in all requests 77 | @logger = logger 78 | @sha256 = sha256 79 | # If logger is not informed, it defaults to STDOUT with INFO level 80 | if logger.nil? 81 | @logger = Logger.new(STDOUT) 82 | @logger.level = Logger::INFO 83 | end 84 | 85 | version = nil if version && version.strip.empty? 86 | @version = nearest_version(version || get_api_version) 87 | unless @supported_versions.include?(@version) 88 | @logger.warn("BigBlueButtonAPI: detected unsupported version, using the closest one that is supported (#{@version})") 89 | end 90 | 91 | @logger.debug("BigBlueButtonAPI: Using version #{@version}") 92 | end 93 | 94 | # Creates a new meeting. Returns the hash with the response or throws BigBlueButtonException 95 | # on failure. 96 | # meeting_name (string):: Name for the meeting 97 | # meeting_id (string):: Unique identifier for the meeting 98 | # options (Hash):: Hash with additional parameters. The accepted parameters are: 99 | # moderatorPW (string), attendeePW (string), welcome (string), 100 | # dialNumber (int), logoutURL (string), maxParticipants (int), 101 | # voiceBridge (int), record (boolean), duration (int), redirectClient (string), 102 | # clientURL (string), and "meta" parameters (usually strings). 103 | # For details about each see BigBlueButton's API docs. 104 | # If you have a custom API with more parameters, you can simply pass them 105 | # in this hash and they will be added to the API call. 106 | # modules (BigBlueButtonModules):: Configuration for the modules. The modules are sent as an xml and the 107 | # request will use an HTTP POST instead of GET. Currently only the 108 | # "presentation" module is available. 109 | # See usage examples below. 110 | # 111 | # === Example 112 | # 113 | # options = { 114 | # :attendeePW => "321", 115 | # :moderatorPW => "123", 116 | # :welcome => "Welcome here!", 117 | # :dialNumber => 5190909090, 118 | # :voiceBridge => 76543, 119 | # :logoutURL => "http://mconf.org", 120 | # :record => true, 121 | # :duration => 0, 122 | # :maxParticipants => 25, 123 | # :meta_category => "Remote Class" 124 | # } 125 | # create_meeting("My Meeting", "my-meeting", options) 126 | # 127 | # === Example with modules (see BigBlueButtonModules docs for more) 128 | # 129 | # modules = BigBlueButton::BigBlueButtonModules.new 130 | # modules.add_presentation(:url, "http://www.samplepdf.com/sample.pdf") 131 | # modules.add_presentation(:url, "http://www.samplepdf.com/sample2.pdf") 132 | # modules.add_presentation(:file, "presentations/class01.ppt") 133 | # modules.add_presentation(:base64, "JVBERi0xLjQKJ....[clipped here]....0CiUlRU9GCg==", "first-class.pdf") 134 | # create_meeting("My Meeting", "my-meeting", nil, modules) 135 | # 136 | # === Example response for 0.81 137 | # 138 | # On successful creation: 139 | # 140 | # { 141 | # :returncode => true, 142 | # :meetingID => "0c521f3d", 143 | # :attendeePW => "12345", 144 | # :moderatorPW => "54321", 145 | # :createTime => 1389464535956, 146 | # :hasBeenForciblyEnded => false, 147 | # :messageKey => "", 148 | # :message => "" 149 | # } 150 | # 151 | # When creating a meeting that already exist: 152 | # 153 | # { 154 | # :returncode => true, 155 | # :meetingID => "7a1d614b", 156 | # :attendeePW => "12345", 157 | # :moderatorPW => "54321", 158 | # :createTime => 1389464682402, 159 | # :hasBeenForciblyEnded => false, 160 | # :messageKey => "duplicateWarning", 161 | # :message => "This conference was already in existence and may currently be in progress." 162 | # } 163 | # 164 | def create_meeting(meeting_name, meeting_id, options={}, modules=nil) 165 | params = { :name => meeting_name, :meetingID => meeting_id }.merge(options) 166 | 167 | # :record is passed as string, but we accept boolean as well 168 | if params[:record] and !!params[:record] == params[:record] 169 | params[:record] = params[:record].to_s 170 | end 171 | 172 | # with modules we send a post request 173 | if modules 174 | response = send_api_request(:create, params, modules.to_xml) 175 | else 176 | response = send_api_request(:create, params) 177 | end 178 | 179 | formatter = BigBlueButtonFormatter.new(response) 180 | formatter.to_string(:meetingID) 181 | formatter.to_string(:moderatorPW) 182 | formatter.to_string(:attendeePW) 183 | formatter.to_boolean(:hasBeenForciblyEnded) 184 | formatter.to_int(:createTime) 185 | 186 | response 187 | end 188 | 189 | # Ends an existing meeting. Throws BigBlueButtonException on failure. 190 | # meeting_id (string):: Unique identifier for the meeting 191 | # moderator_password (string):: Moderator password 192 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 193 | # parameters, but if you have a custom API with more parameters, you 194 | # can simply pass them in this hash and they will be added to the API call. 195 | # 196 | # === Return examples (for 0.81) 197 | # 198 | # On success: 199 | # 200 | # { 201 | # :returncode=>true, 202 | # :messageKey => "sentEndMeetingRequest", 203 | # :message => "A request to end the meeting was sent. Please wait a few seconds, and then use the getMeetingInfo or isMeetingRunning API calls to verify that it was ended." 204 | # } 205 | # 206 | def end_meeting(meeting_id, moderator_password, options={}) 207 | params = { :meetingID => meeting_id, :password => moderator_password }.merge(options) 208 | send_api_request(:end, params) 209 | end 210 | 211 | # Returns whether the meeting is running or not. A meeting is only running after at least 212 | # one participant has joined. Returns true or false. 213 | # meeting_id (string):: Unique identifier for the meeting 214 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 215 | # parameters, but if you have a custom API with more parameters, you 216 | # can simply pass them in this hash and they will be added to the API call. 217 | def is_meeting_running?(meeting_id, options={}) 218 | params = { :meetingID => meeting_id }.merge(options) 219 | hash = send_api_request(:isMeetingRunning, params) 220 | BigBlueButtonFormatter.new(hash).to_boolean(:running) 221 | end 222 | 223 | # Returns a string with the url used to join the meeting 224 | # meeting_id (string):: Unique identifier for the meeting 225 | # user_name (string):: Name of the user 226 | # password (string):: Password for this meeting - will be used to decide if the user is a 227 | # moderator or attendee 228 | # options (Hash):: Hash with additional parameters. The accepted parameters are: 229 | # userID (string), webVoiceConf (string), createTime (int), 230 | # configToken (string), and avatarURL (string). 231 | # For details about each see BigBlueButton's API docs. 232 | # If you have a custom API with more parameters, you can simply pass them 233 | # in this hash and they will be added to the API call. 234 | def join_meeting_url(meeting_id, user_name, password, options={}) 235 | params = { :meetingID => meeting_id, :password => password, :fullName => user_name }.merge(options) 236 | url, data = get_url(:join, params) 237 | url 238 | end 239 | 240 | # Returns a hash object containing the information of a meeting. 241 | # 242 | # meeting_id (string):: Unique identifier for the meeting 243 | # password (string):: Moderator password for this meeting 244 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 245 | # parameters, but if you have a custom API with more parameters, you 246 | # can simply pass them in this hash and they will be added to the API call. 247 | # 248 | # === Example responses for 0.81 249 | # 250 | # Running with attendees and metadata: 251 | # 252 | # 253 | # { 254 | # :returncode => true, 255 | # :meetingName => "e56ef2c5", 256 | # :meetingID => "e56ef2c5", 257 | # :createTime => 1389465592542, 258 | # :voiceBridge => 72519, 259 | # :dialNumber => "1-800-000-0000x00000#", 260 | # :attendeePW => "12345", 261 | # :moderatorPW => "54321", 262 | # :running => true, 263 | # :recording => false, 264 | # :hasBeenForciblyEnded => false, 265 | # :startTime => #, 266 | # :endTime => nil, 267 | # :participantCount => 2, 268 | # :maxUsers => 25, 269 | # :moderatorCount => 1, 270 | # :attendees => [ 271 | # { :userID => "wsfoiqtnugul", :fullName => "Cameron", :role => :viewer, :customdata => {} }, 272 | # { :userID => "qsaogaoqifjk", :fullName => "House", :role => :moderator, :customdata => {} } 273 | # ], 274 | # :metadata => { 275 | # :category => "Testing", 276 | # :anything => "Just trying it out" 277 | # }, 278 | # :messageKey => "", 279 | # :message => "" 280 | # } 281 | # 282 | # Created but not started yet: 283 | # 284 | # { 285 | # :returncode => true, 286 | # :meetingName => "fe3ea879", 287 | # :meetingID => "fe3ea879", 288 | # :createTime => 1389465320050, 289 | # :voiceBridge => 79666, 290 | # :dialNumber => "1-800-000-0000x00000#", 291 | # :attendeePW => "12345", 292 | # :moderatorPW => "54321", 293 | # :running => false, 294 | # :recording => false, 295 | # :hasBeenForciblyEnded => false, 296 | # :startTime => nil, 297 | # :endTime => nil, 298 | # :participantCount => 0, 299 | # :maxUsers => 25, 300 | # :moderatorCount => 0, 301 | # :attendees => [], 302 | # :metadata => {}, 303 | # :messageKey => "", 304 | # :message => "" 305 | # } 306 | # 307 | def get_meeting_info(meeting_id, password, options={}) 308 | params = { :meetingID => meeting_id, :password => password }.merge(options) 309 | response = send_api_request(:getMeetingInfo, params) 310 | 311 | formatter = BigBlueButtonFormatter.new(response) 312 | formatter.flatten_objects(:attendees, :attendee) 313 | response[:attendees].each { |a| BigBlueButtonFormatter.format_attendee(a) } 314 | 315 | formatter.to_string(:meetingID) 316 | formatter.to_string(:moderatorPW) 317 | formatter.to_string(:attendeePW) 318 | formatter.to_boolean(:hasBeenForciblyEnded) 319 | formatter.to_boolean(:running) 320 | formatter.to_datetime(:startTime) 321 | formatter.to_datetime(:endTime) 322 | formatter.to_int(:participantCount) 323 | formatter.to_int(:moderatorCount) 324 | formatter.to_string(:meetingName) 325 | formatter.to_int(:maxUsers) 326 | formatter.to_int(:voiceBridge) 327 | formatter.to_int(:createTime) 328 | formatter.to_boolean(:recording) 329 | 330 | response 331 | end 332 | 333 | # Returns a hash object with information about all the meetings currently created in the 334 | # server, either they are running or not. 335 | # 336 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 337 | # parameters, but if you have a custom API with more parameters, you 338 | # can simply pass them in this hash and they will be added to the API call. 339 | # 340 | # === Example responses for 0.81 341 | # 342 | # Server with one or more meetings: 343 | # 344 | # { 345 | # :returncode => true, 346 | # :meetings => [ 347 | # { :meetingID => "e66e88a3", 348 | # :meetingName => "e66e88a3", 349 | # :createTime => 1389466124414, 350 | # :voiceBridge => 78730, 351 | # :dialNumber=>"1-800-000-0000x00000#", 352 | # :attendeePW => "12345", 353 | # :moderatorPW => "54321", 354 | # :hasBeenForciblyEnded => false, 355 | # :running => false, 356 | # :participantCount => 0, 357 | # :listenerCount => 0, 358 | # :videoCount => 0 } 359 | # { :meetingID => "8f21cc63", 360 | # :meetingName => "8f21cc63", 361 | # :createTime => 1389466073245, 362 | # :voiceBridge => 78992, 363 | # :dialNumber => "1-800-000-0000x00000#", 364 | # :attendeePW => "12345", 365 | # :moderatorPW => "54321", 366 | # :hasBeenForciblyEnded => false, 367 | # :running => true, 368 | # :participantCount => 2, 369 | # :listenerCount => 0, 370 | # :videoCount => 0 } 371 | # ], 372 | # :messageKey => "", 373 | # :message => "" 374 | # } 375 | # 376 | # Server with no meetings: 377 | # 378 | # { 379 | # :returncode => true, 380 | # :meetings => [], 381 | # :messageKey => "noMeetings", 382 | # :message => "no meetings were found on this server" 383 | # } 384 | # 385 | def get_meetings(options={}) 386 | response = send_api_request(:getMeetings, options) 387 | 388 | formatter = BigBlueButtonFormatter.new(response) 389 | formatter.flatten_objects(:meetings, :meeting) 390 | response[:meetings].each { |m| BigBlueButtonFormatter.format_meeting(m) } 391 | response 392 | end 393 | 394 | # Returns the API version of the server as a string. Will return the version in the response 395 | # given by the BigBlueButton server, and not the version set by the user in the initialization 396 | # of this object! 397 | def get_api_version 398 | response = send_api_request(:index) 399 | response[:returncode] ? response[:version].to_s : "" 400 | end 401 | 402 | 403 | # 404 | # API calls since 0.8 405 | # 406 | 407 | # Retrieves the recordings that are available for playback for a given meetingID (or set of meeting IDs). 408 | # options (Hash):: Hash with additional parameters. The accepted parameters are: 409 | # :meetingID (string, Array). For details about each see BigBlueButton's 410 | # API docs. 411 | # Any of the following values are accepted for :meetingID : 412 | # :meetingID => "id1" 413 | # :meetingID => "id1,id2,id3" 414 | # :meetingID => ["id1"] 415 | # :meetingID => ["id1", "id2", "id3"] 416 | # If you have a custom API with more parameters, you can simply pass them 417 | # in this hash and they will be added to the API call. 418 | # 419 | # === Example responses 420 | # 421 | # { :returncode => true, 422 | # :recordings => [ 423 | # { 424 | # :recordID => "7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268", 425 | # :meetingID => "bd1811beecd20f24314819a52ec202bf446ab94b", 426 | # :name => "Evening Class1", 427 | # :published => true, 428 | # :startTime => #, 429 | # :endTime => #, 430 | # :metadata => { :course => "Fundamentals of JAVA", 431 | # :description => "List of recordings", 432 | # :activity => "Evening Class1" }, 433 | # :playback => { 434 | # :format => [ 435 | # { :type => "slides", 436 | # :url => "http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=125468758b24fa27551e7a065849dda3ce65dd32-1329872486268", 437 | # :length => 64 438 | # }, 439 | # { :type => "presentation", 440 | # :url => "http://test-install.blindsidenetworks.com/presentation/slides/playback.html?meetingId=125468758b24fa27551e7a065849dda3ce65dd32-1329872486268", 441 | # :length => 64 442 | # } 443 | # ] 444 | # } 445 | # }, 446 | # { :recordID => "1254kakap98sd09jk2lk2-1329872486234", 447 | # :recordID => "7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268", 448 | # :meetingID => "bklajsdoiajs9d8jo23id90", 449 | # :name => "Evening Class2", 450 | # :published => false, 451 | # :startTime => #, 452 | # :endTime => #, 453 | # :metadata => {}, 454 | # :playback => { 455 | # :format => { # notice that this is now a hash, not an array 456 | # :type => "slides", 457 | # :url => "http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=1254kakap98sd09jk2lk2-1329872486234", 458 | # :length => 64 459 | # } 460 | # } 461 | # } 462 | # ] 463 | # } 464 | # 465 | def get_recordings(options={}) 466 | # ["id1", "id2", "id3"] becomes "id1,id2,id3" 467 | if options.has_key?(:meetingID) 468 | options[:meetingID] = options[:meetingID].join(",") if options[:meetingID].instance_of?(Array) 469 | end 470 | 471 | if options.has_key?(:state) 472 | options[:state] = options[:state].join(",") if options[:state].instance_of?(Array) 473 | end 474 | 475 | response = send_api_request(:getRecordings, options) 476 | 477 | formatter = BigBlueButtonFormatter.new(response) 478 | formatter.flatten_objects(:recordings, :recording) 479 | response[:recordings].each { |r| BigBlueButtonFormatter.format_recording(r) } 480 | response 481 | end 482 | 483 | # Available since BBB v1.1 484 | # Update metadata (or other attributes depending on the API implementation) for a given recordID (or set of record IDs). 485 | # recordIDs (string, Array):: ID or IDs of the target recordings. 486 | # Any of the following values are accepted: 487 | # "id1" 488 | # "id1,id2,id3" 489 | # ["id1"] 490 | # ["id1", "id2", "id3"] 491 | # meta (String):: Pass one or more metadata values to be update (format is the same as in create call) 492 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 493 | # parameters, but if you have a custom API with more parameters, you 494 | # can simply pass them in this hash and they will be added to the API call. 495 | # 496 | # === Example responses 497 | # 498 | # { :returncode => success, :updated => true } 499 | # 500 | def update_recordings(recordIDs, meta=nil, options={}) 501 | recordIDs = recordIDs.join(",") if recordIDs.instance_of?(Array) # ["id1", "id2"] becomes "id1,id2" 502 | params = { :recordID => recordIDs, :meta => meta }.merge(options) 503 | send_api_request(:updateRecordings, params) 504 | end 505 | 506 | 507 | # Publish and unpublish recordings for a given recordID (or set of record IDs). 508 | # recordIDs (string, Array):: ID or IDs of the target recordings. 509 | # Any of the following values are accepted: 510 | # "id1" 511 | # "id1,id2,id3" 512 | # ["id1"] 513 | # ["id1", "id2", "id3"] 514 | # publish (boolean):: Whether to publish or unpublish the recording(s) 515 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 516 | # parameters, but if you have a custom API with more parameters, you 517 | # can simply pass them in this hash and they will be added to the API call. 518 | # 519 | # === Example responses 520 | # 521 | # { :returncode => true, :published => true } 522 | # 523 | def publish_recordings(recordIDs, publish, options={}) 524 | recordIDs = recordIDs.join(",") if recordIDs.instance_of?(Array) # ["id1", "id2"] becomes "id1,id2" 525 | params = { :recordID => recordIDs, :publish => publish.to_s }.merge(options) 526 | send_api_request(:publishRecordings, params) 527 | end 528 | 529 | # Delete one or more recordings for a given recordID (or set of record IDs). 530 | # recordIDs (string, Array):: ID or IDs of the target recordings. 531 | # Any of the following values are accepted: 532 | # "id1" 533 | # "id1,id2,id3" 534 | # ["id1"] 535 | # ["id1", "id2", "id3"] 536 | # options (Hash):: Hash with additional parameters. This method doesn't accept additional 537 | # parameters, but if you have a custom API with more parameters, you 538 | # can simply pass them in this hash and they will be added to the API call. 539 | # 540 | # === Example responses 541 | # 542 | # { :returncode => true, :deleted => true } 543 | # 544 | def delete_recordings(recordIDs, options={}) 545 | recordIDs = recordIDs.join(",") if recordIDs.instance_of?(Array) # ["id1", "id2"] becomes "id1,id2" 546 | params = { :recordID => recordIDs }.merge(options) 547 | send_api_request(:deleteRecordings, params) 548 | end 549 | 550 | # 551 | # Helper functions 552 | # 553 | 554 | # Make a simple request to the server to test the connection. 555 | def test_connection 556 | response = send_api_request(:index) 557 | response[:returncode] 558 | end 559 | 560 | def check_url 561 | url, data = get_url(:check) 562 | url 563 | end 564 | 565 | # API's are equal if all the following attributes are equal. 566 | def ==(other) 567 | r = true 568 | [:url, :supported_versions, :secret, :version, :logger].each do |param| 569 | r = r && self.send(param) == other.send(param) 570 | end 571 | r 572 | end 573 | 574 | # Returns the HTTP response object returned in the last API call. 575 | def last_http_response 576 | @http_response 577 | end 578 | 579 | # Returns the XML returned in the last API call. 580 | def last_xml_response 581 | @xml_response 582 | end 583 | 584 | # Formats an API call URL for the method 'method' using the parameters in 'params'. 585 | # method (symbol):: The API method to be called (:create, :index, :join, and others) 586 | # params (Hash):: The parameters to be passed in the URL 587 | def get_url(method, params={}) 588 | if method == :index 589 | return @url, nil 590 | elsif method == :check 591 | baseurl = URI.join(@url, "/").to_s 592 | return "#{baseurl}check", nil 593 | end 594 | 595 | # stringify and escape all params 596 | params.delete_if { |k, v| v.nil? } unless params.nil? 597 | # some API calls require the params to be sorted 598 | # first make all keys symbols, so the comparison works 599 | params = params.inject({}){ |memo,(k,v)| memo[k.to_sym] = v; memo } 600 | params = Hash[params.sort] 601 | params_string = "" 602 | params_string = params.map{ |k,v| "#{k}=" + URI.encode_www_form_component(v.to_s) unless k.nil? || v.nil? }.join("&") 603 | 604 | # checksum calc 605 | checksum_param = params_string + @secret 606 | checksum_param = method.to_s + checksum_param 607 | checksum = @sha256 ? Digest::SHA256.hexdigest(checksum_param) : Digest::SHA1.hexdigest(checksum_param) 608 | 609 | url = "#{@url}/#{method}?" 610 | url += "#{params_string}&" unless params_string.empty? 611 | url += "checksum=#{checksum}" 612 | return url, nil 613 | end 614 | 615 | # Performs an API call. 616 | # 617 | # Throws a BigBlueButtonException if something goes wrong (e.g. server offline). 618 | # Also throws an exception of the request was not successful (i.e. returncode == FAILED). 619 | # 620 | # Only formats the standard values in the response (the ones that exist in all responses). 621 | # 622 | # method (symbol):: The API method to be called (:create, :index, :join, and others) 623 | # params (Hash):: The parameters to be passed in the URL 624 | # data (string):: Data to be sent with the request. If set, the request will use an HTTP 625 | # POST instead of a GET and the data will be sent in the request body. 626 | # raw (boolean):: If true, returns the data as it was received. Will not parse it into a Hash, 627 | # check for errors or throw exceptions. 628 | def send_api_request(method, params={}, data=nil, raw=false) 629 | # if the method returns a body, use it as the data in the post request 630 | url, body = get_url(method, params) 631 | data = body if body 632 | 633 | @http_response = send_request(url, data) 634 | return {} if @http_response.body.empty? 635 | @xml_response = @http_response.body 636 | 637 | if raw 638 | result = @xml_response 639 | else 640 | 641 | # 'Hashify' the XML 642 | result = BigBlueButtonHash.from_xml(@xml_response) 643 | 644 | # simple validation of the xml body 645 | unless result.has_key?(:returncode) 646 | raise BigBlueButtonException.new("Invalid response body. Is the API URL correct? \"#{@url}\", version #{@version}") 647 | end 648 | 649 | # default cleanup in the response 650 | result = BigBlueButtonFormatter.new(result).default_formatting 651 | 652 | # if the return code is an error generates an exception 653 | unless result[:returncode] 654 | exception = BigBlueButtonException.new(result[:message]) 655 | exception.key = result.has_key?(:messageKey) ? result[:messageKey] : "" 656 | raise exception 657 | end 658 | end 659 | 660 | result 661 | end 662 | 663 | protected 664 | 665 | # :nodoc: 666 | # If data is set, uses a POST with data in the request body 667 | # Otherwise uses GET 668 | def send_request(url, data=nil) 669 | begin 670 | @logger.debug("BigBlueButtonAPI: URL request = #{url}") 671 | url_parsed = URI.parse(url) 672 | http = Net::HTTP.new(url_parsed.host, url_parsed.port) 673 | http.open_timeout = @timeout 674 | http.read_timeout = @timeout 675 | http.use_ssl = true if url_parsed.scheme.downcase == 'https' 676 | 677 | if data.nil? 678 | response = http.get(url_parsed.request_uri, @request_headers) 679 | else 680 | @logger.debug("BigBlueButtonAPI: Sending as a POST request with data.size = #{data.size}") 681 | opts = { 'Content-Type' => 'application/xml' }.merge @request_headers 682 | response = http.post(url_parsed.request_uri, data, opts) 683 | end 684 | @logger.info("BigBlueButtonAPI: request=#{url} response_status=#{response.class.name} response_code=#{response.code} message_key=#{response.message}") 685 | @logger.debug("BigBlueButtonAPI: URL response = #{response.body}") 686 | 687 | rescue Timeout::Error => error 688 | exception = BigBlueButtonException.new("Timeout error. Your server is probably down: \"#{@url}\". Error: #{error}") 689 | exception.key = 'TimeoutError' 690 | raise exception 691 | 692 | rescue Exception => error 693 | exception = BigBlueButtonException.new("Connection error. Your URL is probably incorrect: \"#{@url}\". Error: #{error}") 694 | exception.key = 'IncorrectUrlError' 695 | raise exception 696 | end 697 | 698 | response 699 | end 700 | 701 | def nearest_version(target) 702 | version = target 703 | 704 | # 0.81 in BBB is actually < than 0.9, but not when comparing here 705 | # so normalize x.xx versions to x.x.x 706 | match = version.match(/(\d)\.(\d)(\d)/) 707 | version = "#{match[1]}.#{match[2]}.#{match[3]}" if match 708 | 709 | # we don't allow older versions than the one supported, use an old version of the gem for that 710 | if Gem::Version.new(version) < Gem::Version.new(@supported_versions[0]) 711 | exception = BigBlueButtonException.new("BigBlueButton error: Invalid API version #{version}. Supported versions: #{@supported_versions.join(', ')}") 712 | exception.key = 'APIVersionError' 713 | raise exception 714 | 715 | # allow newer versions by using the newest one we support 716 | elsif Gem::Version.new(version) > Gem::Version.new(@supported_versions.last) 717 | @supported_versions.last 718 | 719 | else 720 | target 721 | end 722 | end 723 | 724 | end 725 | end 726 | -------------------------------------------------------------------------------- /spec/bigbluebutton_api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Note: Uses version 0.8 by default. For things that only exist in newer versions, 4 | # there are separate files with more tests. 5 | describe BigBlueButton::BigBlueButtonApi do 6 | 7 | shared_examples_for "BigBlueButtonApi" do |version| 8 | 9 | # default variables and API object for all tests 10 | let(:url) { "http://server.com" } 11 | let(:secret) { "1234567890abcdefghijkl" } 12 | let(:logger) { Logger.new(STDOUT) } 13 | let(:api) { BigBlueButton::BigBlueButtonApi.new(url, secret, version, logger) } 14 | before { logger.level = Logger::INFO } 15 | 16 | describe "#initialize" do 17 | context "standard initialization" do 18 | subject { BigBlueButton::BigBlueButtonApi.new(url, secret, version, logger) } 19 | it { subject.url.should == url } 20 | it { subject.secret.should == secret } 21 | it { subject.version.should == version } 22 | it { subject.logger.should == logger } 23 | it { subject.timeout.should == 10 } 24 | it { subject.supported_versions.should include("0.8") } 25 | it { subject.supported_versions.should include("0.81") } 26 | it { subject.supported_versions.should include("0.9") } 27 | it { subject.request_headers.should == {} } 28 | end 29 | 30 | context "when the version is not informed, get it from the BBB server" do 31 | before { BigBlueButton::BigBlueButtonApi.any_instance.should_receive(:get_api_version).and_return("0.8") } 32 | subject { BigBlueButton::BigBlueButtonApi.new(url, secret, nil) } 33 | it { subject.version.should == "0.8" } 34 | end 35 | 36 | context "when the version informed is empty, get it from the BBB server" do 37 | before { BigBlueButton::BigBlueButtonApi.any_instance.should_receive(:get_api_version).and_return("0.8") } 38 | subject { BigBlueButton::BigBlueButtonApi.new(url, secret, " ") } 39 | it { subject.version.should == "0.8" } 40 | end 41 | 42 | it "when the version is lower than the lowest supported, raise exception" do 43 | expect { 44 | BigBlueButton::BigBlueButtonApi.new(url, secret, "0.1", nil) 45 | }.to raise_error(BigBlueButton::BigBlueButtonException) 46 | end 47 | 48 | it "when the version is higher than thew highest supported, use the highest supported" do 49 | BigBlueButton::BigBlueButtonApi.new(url, secret, "5.0", nil).version.should eql('1.0') 50 | end 51 | 52 | it "compares versions in the format 'x.xx' properly" do 53 | expect { 54 | # if not comparing properly, 0.61 would be bigger than 0.9, for example 55 | # comparing the way BBB does, it is lower, so will raise an exception 56 | BigBlueButton::BigBlueButtonApi.new(url, secret, "0.61", nil) 57 | }.to raise_error(BigBlueButton::BigBlueButtonException) 58 | end 59 | 60 | context "current supported versions" do 61 | before { 62 | BigBlueButton::BigBlueButtonApi.any_instance.should_receive(:get_api_version).and_return("0.9") 63 | } 64 | subject { BigBlueButton::BigBlueButtonApi.new(url, secret) } 65 | it { subject.supported_versions.should == ["0.8", "0.81", "0.9", "1.0"] } 66 | end 67 | end 68 | 69 | describe "#create_meeting" do 70 | context "standard case" do 71 | let(:req_params) { 72 | { :name => "name", :meetingID => "meeting-id", :moderatorPW => "mp", :attendeePW => "ap", 73 | :welcome => "Welcome!", :dialNumber => 12345678, :logoutURL => "http://example.com", 74 | :maxParticipants => 25, :voiceBridge => 12345, :webVoice => "12345abc", :record => "true" } 75 | } 76 | let(:req_response) { 77 | { :meetingID => 123, :moderatorPW => 111, :attendeePW => 222, :hasBeenForciblyEnded => "FALSE" } 78 | } 79 | let(:final_response) { 80 | { :meetingID => "123", :moderatorPW => "111", :attendeePW => "222", :hasBeenForciblyEnded => false } 81 | } 82 | 83 | # ps: not mocking the formatter here because it's easier to just check the results (final_response) 84 | before { api.should_receive(:send_api_request).with(:create, req_params).and_return(req_response) } 85 | subject { 86 | options = { :moderatorPW => "mp", :attendeePW => "ap", :welcome => "Welcome!", 87 | :dialNumber => 12345678, :logoutURL => "http://example.com", :maxParticipants => 25, 88 | :voiceBridge => 12345, :webVoice => "12345abc", :record => "true" } 89 | api.create_meeting("name", "meeting-id", options) 90 | } 91 | it { subject.should == final_response } 92 | end 93 | 94 | context "accepts non standard options" do 95 | let(:params) { 96 | { :name => "name", :meetingID => "meeting-id", 97 | :moderatorPW => "mp", :attendeePW => "ap", :nonStandard => 1 } 98 | } 99 | before { api.should_receive(:send_api_request).with(:create, params) } 100 | it { api.create_meeting("name", "meeting-id", params) } 101 | end 102 | 103 | context "accepts :record as boolean" do 104 | let(:req_params) { 105 | { :name => "name", :meetingID => "meeting-id", 106 | :moderatorPW => "mp", :attendeePW => "ap", :record => "true" } 107 | } 108 | before { api.should_receive(:send_api_request).with(:create, req_params) } 109 | it { 110 | params = { :name => "name", :meetingID => "meeting-id", 111 | :moderatorPW => "mp", :attendeePW => "ap", :record => true } 112 | api.create_meeting("name", "meeting-id", params) 113 | } 114 | end 115 | 116 | context "with modules" do 117 | let(:req_params) { 118 | { :name => "name", :meetingID => "meeting-id", :moderatorPW => "mp", :attendeePW => "ap" } 119 | } 120 | let(:req_response) { 121 | { :meetingID => 123, :moderatorPW => 111, :attendeePW => 222, :hasBeenForciblyEnded => "FALSE", :createTime => "123123123" } 122 | } 123 | let(:final_response) { 124 | { :meetingID => "123", :moderatorPW => "111", :attendeePW => "222", :hasBeenForciblyEnded => false, :createTime => 123123123 } 125 | } 126 | let(:modules) { 127 | m = BigBlueButton::BigBlueButtonModules.new 128 | m.add_presentation(:url, "http://www.samplepdf.com/sample.pdf") 129 | m.add_presentation(:url, "http://www.samplepdf.com/sample2.pdf") 130 | m.add_presentation(:base64, "JVBERi0xLjQKJ....[clipped here]....0CiUlRU9GCg==", "first-class.pdf") 131 | m 132 | } 133 | 134 | before { 135 | api.should_receive(:send_api_request).with(:create, req_params, modules.to_xml). 136 | and_return(req_response) 137 | } 138 | subject { 139 | options = { :moderatorPW => "mp", :attendeePW => "ap" } 140 | api.create_meeting("name", "meeting-id", options, modules) 141 | } 142 | it { subject.should == final_response } 143 | end 144 | 145 | context "without modules" do 146 | let(:req_params) { 147 | { :name => "name", :meetingID => "meeting-id", :moderatorPW => "mp", :attendeePW => "ap", 148 | :welcome => "Welcome!", :dialNumber => 12345678, :logoutURL => "http://example.com", 149 | :maxParticipants => 25, :voiceBridge => 12345, :record => "true", :duration => 20, 150 | :meta_1 => "meta1", :meta_2 => "meta2" } 151 | } 152 | let(:req_response) { 153 | { :meetingID => 123, :moderatorPW => 111, :attendeePW => 222, :hasBeenForciblyEnded => "FALSE", :createTime => "123123123" } 154 | } 155 | let(:final_response) { 156 | { :meetingID => "123", :moderatorPW => "111", :attendeePW => "222", :hasBeenForciblyEnded => false, :createTime => 123123123 } 157 | } 158 | 159 | before { api.should_receive(:send_api_request).with(:create, req_params).and_return(req_response) } 160 | subject { 161 | options = { :moderatorPW => "mp", :attendeePW => "ap", :welcome => "Welcome!", :dialNumber => 12345678, 162 | :logoutURL => "http://example.com", :maxParticipants => 25, :voiceBridge => 12345, :record => true, 163 | :duration => 20, :meta_1 => "meta1", :meta_2 => "meta2" } 164 | api.create_meeting("name", "meeting-id", options) 165 | } 166 | it { subject.should == final_response } 167 | end 168 | end 169 | 170 | describe "#end_meeting" do 171 | let(:meeting_id) { "meeting-id" } 172 | let(:moderator_password) { "password" } 173 | 174 | context "standard case" do 175 | let(:params) { { :meetingID => meeting_id, :password => moderator_password } } 176 | let(:response) { "anything" } 177 | 178 | before { api.should_receive(:send_api_request).with(:end, params).and_return(response) } 179 | it { api.end_meeting(meeting_id, moderator_password).should == response } 180 | end 181 | 182 | context "accepts non standard options" do 183 | let(:params_in) { 184 | { :anything1 => "anything-1", :anything2 => 2 } 185 | } 186 | let(:params_out) { 187 | { :meetingID => meeting_id, :password => moderator_password, 188 | :anything1 => "anything-1", :anything2 => 2 } 189 | } 190 | before { api.should_receive(:send_api_request).with(:end, params_out) } 191 | it { api.end_meeting(meeting_id, moderator_password, params_in) } 192 | end 193 | end 194 | 195 | describe "#is_meeting_running?" do 196 | let(:meeting_id) { "meeting-id" } 197 | let(:params) { { :meetingID => meeting_id } } 198 | 199 | context "when the meeting is running" do 200 | let(:response) { { :running => "TRUE" } } 201 | before { api.should_receive(:send_api_request).with(:isMeetingRunning, params).and_return(response) } 202 | it { api.is_meeting_running?(meeting_id).should == true } 203 | end 204 | 205 | context "when the meeting is not running" do 206 | let(:response) { { :running => "FALSE" } } 207 | before { api.should_receive(:send_api_request).with(:isMeetingRunning, params).and_return(response) } 208 | it { api.is_meeting_running?(meeting_id).should == false } 209 | end 210 | 211 | context "accepts non standard options" do 212 | let(:params_in) { 213 | { :anything1 => "anything-1", :anything2 => 2 } 214 | } 215 | let(:params_out) { 216 | { :meetingID => meeting_id, :anything1 => "anything-1", :anything2 => 2 } 217 | } 218 | before { api.should_receive(:send_api_request).with(:isMeetingRunning, params_out) } 219 | it { api.is_meeting_running?(meeting_id, params_in) } 220 | end 221 | end 222 | 223 | describe "#join_meeting_url" do 224 | context "standard case" do 225 | let(:params) { 226 | { :meetingID => "meeting-id", :password => "pw", :fullName => "Name", 227 | :userID => "id123", :webVoiceConf => 12345678, :createTime => 9876543 } 228 | } 229 | 230 | before { api.should_receive(:get_url).with(:join, params).and_return(["test-url", nil]) } 231 | it { 232 | options = { :userID => "id123", :webVoiceConf => 12345678, :createTime => 9876543 } 233 | api.join_meeting_url("meeting-id", "Name", "pw", options).should == "test-url" 234 | } 235 | end 236 | 237 | context "accepts non standard options" do 238 | let(:params) { 239 | { :meetingID => "meeting-id", :password => "pw", 240 | :fullName => "Name", :userID => "id123", :nonStandard => 1 } 241 | } 242 | before { api.should_receive(:get_url).with(:join, params) } 243 | it { api.join_meeting_url("meeting-id", "Name", "pw", params) } 244 | end 245 | end 246 | 247 | describe "#get_meeting_info" do 248 | let(:meeting_id) { "meeting-id" } 249 | let(:password) { "password" } 250 | 251 | context "standard case" do 252 | let(:params) { { :meetingID => meeting_id, :password => password } } 253 | 254 | let(:attendee1) { { :userID => 123, :fullName => "Dexter Morgan", :role => "MODERATOR" } } 255 | let(:attendee2) { { :userID => "id2", :fullName => "Cameron", :role => "VIEWER" } } 256 | let(:response) { 257 | { :meetingID => 123, :moderatorPW => 111, :attendeePW => 222, :hasBeenForciblyEnded => "FALSE", 258 | :running => "TRUE", :startTime => "Thu Sep 01 17:51:42 UTC 2011", :endTime => "null", 259 | :returncode => true, :attendees => { :attendee => [ attendee1, attendee2 ] }, 260 | :messageKey => "mkey", :message => "m", :participantCount => "50", :moderatorCount => "3", 261 | :meetingName => "meeting-name", :maxUsers => "100", :voiceBridge => "12341234", :createTime => "123123123", 262 | :recording => "false", :meta_1 => "abc", :meta_2 => "2" } 263 | } # hash after the send_api_request call, before the formatting 264 | 265 | let(:expected_attendee1) { { :userID => "123", :fullName => "Dexter Morgan", :role => :moderator } } 266 | let(:expected_attendee2) { { :userID => "id2", :fullName => "Cameron", :role => :viewer } } 267 | let(:final_response) { 268 | { :meetingID => "123", :moderatorPW => "111", :attendeePW => "222", :hasBeenForciblyEnded => false, 269 | :running => true, :startTime => DateTime.parse("Thu Sep 01 17:51:42 UTC 2011"), :endTime => nil, 270 | :returncode => true, :attendees => [ expected_attendee1, expected_attendee2 ], 271 | :messageKey => "mkey", :message => "m", :participantCount => 50, :moderatorCount => 3, 272 | :meetingName => "meeting-name", :maxUsers => 100, :voiceBridge => 12341234, :createTime => 123123123, 273 | :recording => false, :meta_1 => "abc", :meta_2 => "2" } 274 | } # expected return hash after all the formatting 275 | 276 | # ps: not mocking the formatter here because it's easier to just check the results (final_response) 277 | before { api.should_receive(:send_api_request).with(:getMeetingInfo, params).and_return(response) } 278 | it { api.get_meeting_info(meeting_id, password).should == final_response } 279 | end 280 | 281 | context "accepts non standard options" do 282 | let(:params_in) { 283 | { :anything1 => "anything-1", :anything2 => 2 } 284 | } 285 | let(:params_out) { 286 | { :meetingID => meeting_id, :password => password, 287 | :anything1 => "anything-1", :anything2 => 2 } 288 | } 289 | before { api.should_receive(:send_api_request).with(:getMeetingInfo, params_out).and_return({}) } 290 | it { api.get_meeting_info(meeting_id, password, params_in) } 291 | end 292 | end 293 | 294 | describe "#get_meetings" do 295 | context "standard case" do 296 | let(:meeting_hash1) { { :meetingID => "Demo Meeting", :attendeePW => "ap", :moderatorPW => "mp", :hasBeenForciblyEnded => false, :running => true } } 297 | let(:meeting_hash2) { { :meetingID => "Ended Meeting", :attendeePW => "pass", :moderatorPW => "pass", :hasBeenForciblyEnded => true, :running => false } } 298 | let(:flattened_response) { 299 | { :returncode => true, :meetings => [ meeting_hash1, meeting_hash2 ], :messageKey => "mkey", :message => "m" } 300 | } # hash *after* the flatten_objects call 301 | 302 | before { 303 | api.should_receive(:send_api_request).with(:getMeetings, {}). 304 | and_return(flattened_response) 305 | formatter_double = double(BigBlueButton::BigBlueButtonFormatter) 306 | formatter_double.should_receive(:flatten_objects).with(:meetings, :meeting) 307 | BigBlueButton::BigBlueButtonFormatter.should_receive(:new).and_return(formatter_double) 308 | BigBlueButton::BigBlueButtonFormatter.should_receive(:format_meeting).with(meeting_hash1) 309 | BigBlueButton::BigBlueButtonFormatter.should_receive(:format_meeting).with(meeting_hash2) 310 | } 311 | it { api.get_meetings } 312 | end 313 | 314 | context "accepts non standard options" do 315 | let(:params) { 316 | { :anything1 => "anything-1", :anything2 => 2 } 317 | } 318 | before { api.should_receive(:send_api_request).with(:getMeetings, params).and_return({}) } 319 | it { api.get_meetings(params) } 320 | end 321 | end 322 | 323 | describe "#get_api_version" do 324 | context "returns the version returned by the server" do 325 | let(:hash) { { :returncode => true, :version => "0.8" } } 326 | before { api.should_receive(:send_api_request).with(:index).and_return(hash) } 327 | it { api.get_api_version.should == "0.8" } 328 | end 329 | 330 | context "returns an empty string when the server responds with an empty hash" do 331 | before { api.should_receive(:send_api_request).with(:index).and_return({}) } 332 | it { api.get_api_version.should == "" } 333 | end 334 | end 335 | 336 | describe "#test_connection" do 337 | context "returns the returncode returned by the server" do 338 | let(:hash) { { :returncode => "any-value" } } 339 | before { api.should_receive(:send_api_request).with(:index).and_return(hash) } 340 | it { api.test_connection.should == "any-value" } 341 | end 342 | end 343 | 344 | describe "#check_url" do 345 | context "when method = :check" do 346 | it { 347 | api.url = 'http://my-test-server.com/bigbluebutton/api' 348 | api.check_url.should == 'http://my-test-server.com/check' 349 | } 350 | end 351 | end 352 | 353 | describe "#==" do 354 | let(:api2) { BigBlueButton::BigBlueButtonApi.new(url, secret, version, logger) } 355 | 356 | context "compares attributes" do 357 | it { api.should == api2 } 358 | end 359 | 360 | context "differs #logger" do 361 | before { api2.logger = !api.logger } 362 | it { api.should_not == api2 } 363 | end 364 | 365 | context "differs #secret" do 366 | before { api2.secret = api.secret + "x" } 367 | it { api.should_not == api2 } 368 | end 369 | 370 | context "differs #version" do 371 | before { api2.version = api.version + "x" } 372 | it { api.should_not == api2 } 373 | end 374 | 375 | context "differs #supported_versions" do 376 | before { api2.supported_versions << "x" } 377 | it { api.should_not == api2 } 378 | end 379 | end 380 | 381 | describe "#last_http_response" do 382 | # we test this through a #test_connection call 383 | 384 | let(:request_double) { double } 385 | before { 386 | api.should_receive(:get_url) 387 | # this return value will be stored in @http_response 388 | api.should_receive(:send_request).and_return(request_double) 389 | # to return fast from #send_api_request 390 | request_double.should_receive(:body).and_return("") 391 | api.test_connection 392 | } 393 | it { api.last_http_response.should == request_double } 394 | end 395 | 396 | describe "#last_xml_response" do 397 | # we test this through a #test_connection call 398 | 399 | let(:request_double) { double } 400 | let(:expected_xml) { "SUCCESS" } 401 | before { 402 | api.should_receive(:get_url) 403 | api.should_receive(:send_request).and_return(request_double) 404 | request_double.should_receive(:body).at_least(1).and_return(expected_xml) 405 | api.test_connection 406 | } 407 | it { api.last_xml_response.should == expected_xml } 408 | end 409 | 410 | describe "#get_url" do 411 | 412 | context "when method = :index" do 413 | it { api.get_url(:index).should == [api.url, nil] } 414 | end 415 | 416 | context "when method = :check" do 417 | it { 418 | api.url = 'http://my-test-server.com/bigbluebutton/api' 419 | api.get_url(:check).should == ['http://my-test-server.com/check', nil] 420 | } 421 | end 422 | 423 | context "when method != :index" do 424 | context "validates the entire url" do 425 | context "with params" do 426 | let(:params) { { :param1 => "value1", :param2 => "value2" } } 427 | subject { api.get_url(:join, params)[0] } 428 | it { 429 | # the hash can be sorted differently depending on the ruby version 430 | if params.map{ |k,v| "#{k}" }.join =~ /^param1/ 431 | subject.should match(/#{url}\/join\?param1=value1¶m2=value2/) 432 | else 433 | subject.should match(/#{url}\/join\?param2=value2¶m1=value1/) 434 | end 435 | } 436 | end 437 | 438 | context "without params" do 439 | subject { api.get_url(:join)[0] } 440 | it { subject.should match(/#{url}\/join\?[^&]/) } 441 | end 442 | end 443 | 444 | context "discards params with nil value" do 445 | let(:params) { { :param1 => "value1", :param2 => nil } } 446 | subject { api.get_url(:join, params)[0] } 447 | it { subject.should_not match(/param2=/) } 448 | end 449 | 450 | context "escapes all params" do 451 | let(:params) { { :param1 => "value with spaces", :param2 => "@$" } } 452 | subject { api.get_url(:join, params)[0] } 453 | it { subject.should match(/param1=value\+with\+spaces/) } 454 | it { subject.should match(/param2=%40%24/) } 455 | end 456 | 457 | [ [' ', '+'], 458 | ['*', '*'] 459 | ].each do |values| 460 | context "escapes #{values[0].inspect} as #{values[1].inspect}" do 461 | let(:params) { { param1: "before#{values[0]}after" } } 462 | subject { api.get_url(:join, params)[0] } 463 | it { subject.should match(/param1=before#{Regexp.quote(values[1])}after/) } 464 | end 465 | end 466 | 467 | context "includes the checksum" do 468 | context "when @sha256 is false or nil" do 469 | let(:params) { { param1: "value1", param2: "value2" } } 470 | let(:checksum) { 471 | # the hash can be sorted differently depending on the ruby version 472 | if params.map{ |k, v| k }.join =~ /^param1/ 473 | "67882ae54f49600f56f358c10d24697ef7d8c6b2" 474 | else 475 | "85a54e28e4ec18bfdcb214a73f74d35b09a84176" 476 | end 477 | } 478 | subject { api.get_url(:join, params)[0] } 479 | it('uses SHA1') { subject.should match(/checksum=#{checksum}$/) } 480 | end 481 | 482 | context "when @sha256 flag is true" do 483 | let(:api) { BigBlueButton::BigBlueButtonApi.new(url, secret, version, logger, true) } 484 | let(:params) { { param1: "value1", param2: "value2" } } 485 | let(:checksum) { 486 | # the hash can be sorted differently depending on the ruby version 487 | if params.map{ |k,v| k }.join =~ /^param1/ 488 | "0e7b1611809fad890a114dddae1a37fecf14c28971afc10ee3eac432da5b8b41" 489 | else 490 | "21bf2d24c27251c4b2b2f0d5dd4b966a2f16fbfc7882e102b44c6d67f728f0c8" 491 | end 492 | } 493 | subject { api.get_url(:join, params)[0] } 494 | it('uses SHA256') { subject.should match(/checksum=#{checksum}$/) } 495 | end 496 | end 497 | end 498 | end 499 | 500 | describe "#send_api_request" do 501 | let(:method) { :join } 502 | let(:params) { { :param1 => "value1" } } 503 | let(:data) { "any data" } 504 | let(:url) { "http://test-server:8080?param1=value1&checksum=12345" } 505 | let(:make_request) { api.send_api_request(method, params, data) } 506 | let(:response_double) { double() } # mock of what send_request() would return 507 | 508 | before { api.should_receive(:get_url).with(method, params).and_return([url, nil]) } 509 | 510 | context "returns an empty hash if the response body is empty" do 511 | before do 512 | api.should_receive(:send_request).with(url, data).and_return(response_double) 513 | response_double.should_receive(:body).and_return("") 514 | end 515 | it { make_request.should == { } } 516 | end 517 | 518 | context "hashfies and validates the response body" do 519 | before do 520 | api.should_receive(:send_request).with(url, data).and_return(response_double) 521 | response_double.should_receive(:body).twice.and_return("response-body") 522 | end 523 | 524 | context "checking if it has a :response key" do 525 | before { BigBlueButton::BigBlueButtonHash.should_receive(:from_xml).with("response-body").and_return({ }) } 526 | it { expect { make_request }.to raise_error(BigBlueButton::BigBlueButtonException) } 527 | end 528 | 529 | context "checking if it the :response key has a :returncode key" do 530 | before { BigBlueButton::BigBlueButtonHash.should_receive(:from_xml).with("response-body").and_return({ :response => { } }) } 531 | it { expect { make_request }.to raise_error(BigBlueButton::BigBlueButtonException) } 532 | end 533 | end 534 | 535 | context "formats the response hash" do 536 | let(:response) { { :returncode => "SUCCESS" } } 537 | let(:formatted_response) { { :returncode => true, :messageKey => "", :message => "" } } 538 | before do 539 | api.should_receive(:send_request).with(url, data).and_return(response_double) 540 | response_double.should_receive(:body).twice.and_return("response-body") 541 | BigBlueButton::BigBlueButtonHash.should_receive(:from_xml).with("response-body").and_return(response) 542 | 543 | # here starts the validation 544 | # doesn't test the resulting format, only that the formatter was called 545 | formatter_double = double(BigBlueButton::BigBlueButtonFormatter) 546 | BigBlueButton::BigBlueButtonFormatter.should_receive(:new).with(response).and_return(formatter_double) 547 | formatter_double.should_receive(:default_formatting).and_return(formatted_response) 548 | end 549 | it { make_request } 550 | end 551 | 552 | context "raise an error if the formatted response has no :returncode" do 553 | let(:response) { { :returncode => true } } 554 | let(:formatted_response) { { } } 555 | before do 556 | api.should_receive(:send_request).with(url, data).and_return(response_double) 557 | response_double.should_receive(:body).twice.and_return("response-body") 558 | BigBlueButton::BigBlueButtonHash.should_receive(:from_xml).with("response-body").and_return(response) 559 | 560 | formatter_double = double(BigBlueButton::BigBlueButtonFormatter) 561 | BigBlueButton::BigBlueButtonFormatter.should_receive(:new).with(response).and_return(formatter_double) 562 | formatter_double.should_receive(:default_formatting).and_return(formatted_response) 563 | end 564 | it { expect { make_request }.to raise_error(BigBlueButton::BigBlueButtonException) } 565 | end 566 | end 567 | 568 | describe "#send_request" do 569 | let(:url) { "http://test-server:8080/res?param1=value1&checksum=12345" } 570 | let(:url_parsed) { URI.parse(url) } 571 | let(:res) { Net::HTTPResponse.new(1.0, '200', 'OK') } 572 | 573 | before do 574 | @http_double = double(Net::HTTP) 575 | @http_double.should_receive(:"open_timeout=").with(api.timeout) 576 | @http_double.should_receive(:"read_timeout=").with(api.timeout) 577 | Net::HTTP.should_receive(:new).with("test-server", 8080).and_return(@http_double) 578 | res.stub(:body) { "ok" } 579 | end 580 | 581 | context "standard case" do 582 | before { @http_double.should_receive(:get).with("/res?param1=value1&checksum=12345", {}).and_return(res) } 583 | it { api.send(:send_request, url).should == res } 584 | end 585 | 586 | context "handles a TimeoutError" do 587 | before { @http_double.should_receive(:get) { raise TimeoutError } } 588 | it { expect { api.send(:send_request, url) }.to raise_error(BigBlueButton::BigBlueButtonException) } 589 | end 590 | 591 | context "handles general Exceptions" do 592 | before { @http_double.should_receive(:get) { raise Exception } } 593 | it { expect { api.send(:send_request, url) }.to raise_error(BigBlueButton::BigBlueButtonException) } 594 | end 595 | 596 | context "post with data" do 597 | let(:data) { "any data" } 598 | before { 599 | path = "/res?param1=value1&checksum=12345" 600 | opts = { 'Content-Type' => 'application/xml' } 601 | @http_double.should_receive(:post).with(path, data, opts).and_return(res) 602 | } 603 | it { 604 | api.send(:send_request, url, data).should == res 605 | } 606 | end 607 | 608 | context "get with headers" do 609 | let(:headers_hash) { { :anything => "anything" } } 610 | before { @http_double.should_receive(:get).with("/res?param1=value1&checksum=12345", headers_hash).and_return(res) } 611 | it { 612 | api.request_headers = headers_hash 613 | api.send(:send_request, url).should == res 614 | } 615 | end 616 | 617 | context "get with headers" do 618 | let(:headers_hash) { { :anything => "anything" } } 619 | let(:data) { "any data" } 620 | before { 621 | path = "/res?param1=value1&checksum=12345" 622 | opts = { 'Content-Type' => 'application/xml', :anything => "anything" } 623 | @http_double.should_receive(:post).with(path, data, opts).and_return(res) 624 | } 625 | it { 626 | api.request_headers = headers_hash 627 | api.send(:send_request, url, data).should == res 628 | } 629 | end 630 | end 631 | 632 | describe "#get_recordings" do 633 | let(:recording1) { { :recordID => "id1", :meetindID => "meeting-id" } } # simplified "recording" node in the response 634 | let(:recording2) { { :recordID => "id2", :meetindID => "meeting-id" } } 635 | let(:response) { 636 | { :returncode => true, :recordings => { :recording => [ recording1, recording2 ] }, :messageKey => "mkey", :message => "m" } 637 | } 638 | let(:flattened_response) { 639 | { :returncode => true, :recordings => [ recording1, recording2 ], :messageKey => "mkey", :message => "m" } 640 | } # hash *after* the flatten_objects call 641 | 642 | context "accepts non standard options" do 643 | let(:params) { { :meetingID => "meeting-id", :nonStandard => 1 } } 644 | before { api.should_receive(:send_api_request).with(:getRecordings, params).and_return(response) } 645 | it { api.get_recordings(params) } 646 | end 647 | 648 | context "without meeting ID" do 649 | before { api.should_receive(:send_api_request).with(:getRecordings, {}).and_return(response) } 650 | it { api.get_recordings.should == response } 651 | end 652 | 653 | context "with one meeting ID" do 654 | context "in an array" do 655 | let(:options) { { :meetingID => ["meeting-id"] } } 656 | let(:req_params) { { :meetingID => "meeting-id" } } 657 | before { api.should_receive(:send_api_request).with(:getRecordings, req_params).and_return(response) } 658 | it { api.get_recordings(options).should == response } 659 | end 660 | 661 | context "in a string" do 662 | let(:options) { { :meetingID => "meeting-id" } } 663 | let(:req_params) { { :meetingID => "meeting-id" } } 664 | before { api.should_receive(:send_api_request).with(:getRecordings, req_params).and_return(response) } 665 | it { api.get_recordings(options).should == response } 666 | end 667 | end 668 | 669 | context "with several meeting IDs" do 670 | context "in an array" do 671 | let(:options) { { :meetingID => ["meeting-id-1", "meeting-id-2"] } } 672 | let(:req_params) { { :meetingID => "meeting-id-1,meeting-id-2" } } 673 | before { api.should_receive(:send_api_request).with(:getRecordings, req_params).and_return(response) } 674 | it { api.get_recordings(options).should == response } 675 | end 676 | 677 | context "in a string" do 678 | let(:options) { { :meetingID => "meeting-id-1,meeting-id-2" } } 679 | let(:req_params) { { :meetingID => "meeting-id-1,meeting-id-2" } } 680 | before { api.should_receive(:send_api_request).with(:getRecordings, req_params).and_return(response) } 681 | it { api.get_recordings(options).should == response } 682 | end 683 | end 684 | 685 | context "formats the response" do 686 | before { 687 | api.should_receive(:send_api_request).with(:getRecordings, anything).and_return(flattened_response) 688 | formatter_double = double(BigBlueButton::BigBlueButtonFormatter) 689 | formatter_double.should_receive(:flatten_objects).with(:recordings, :recording) 690 | BigBlueButton::BigBlueButtonFormatter.should_receive(:format_recording).with(recording1) 691 | BigBlueButton::BigBlueButtonFormatter.should_receive(:format_recording).with(recording2) 692 | BigBlueButton::BigBlueButtonFormatter.should_receive(:new).and_return(formatter_double) 693 | } 694 | it { api.get_recordings } 695 | end 696 | end 697 | 698 | describe "#publish_recordings" do 699 | 700 | context "publish is converted to string" do 701 | let(:recordIDs) { "any" } 702 | let(:req_params) { { :publish => "false", :recordID => "any" } } 703 | before { api.should_receive(:send_api_request).with(:publishRecordings, req_params) } 704 | it { api.publish_recordings(recordIDs, false) } 705 | end 706 | 707 | context "with one recording ID" do 708 | context "in an array" do 709 | let(:recordIDs) { ["id-1"] } 710 | let(:req_params) { { :publish => "true", :recordID => "id-1" } } 711 | before { api.should_receive(:send_api_request).with(:publishRecordings, req_params) } 712 | it { api.publish_recordings(recordIDs, true) } 713 | end 714 | 715 | context "in a string" do 716 | let(:recordIDs) { "id-1" } 717 | let(:req_params) { { :publish => "true", :recordID => "id-1" } } 718 | before { api.should_receive(:send_api_request).with(:publishRecordings, req_params) } 719 | it { api.publish_recordings(recordIDs, true) } 720 | end 721 | end 722 | 723 | context "with several recording IDs" do 724 | context "in an array" do 725 | let(:recordIDs) { ["id-1", "id-2"] } 726 | let(:req_params) { { :publish => "true", :recordID => "id-1,id-2" } } 727 | before { api.should_receive(:send_api_request).with(:publishRecordings, req_params) } 728 | it { api.publish_recordings(recordIDs, true) } 729 | end 730 | 731 | context "in a string" do 732 | let(:recordIDs) { "id-1,id-2,id-3" } 733 | let(:req_params) { { :publish => "true", :recordID => "id-1,id-2,id-3" } } 734 | before { api.should_receive(:send_api_request).with(:publishRecordings, req_params) } 735 | it { api.publish_recordings(recordIDs, true) } 736 | end 737 | end 738 | 739 | context "accepts non standard options" do 740 | let(:recordIDs) { ["id-1"] } 741 | let(:params_in) { 742 | { :anything1 => "anything-1", :anything2 => 2 } 743 | } 744 | let(:params_out) { 745 | { :publish => "true", :recordID => "id-1", 746 | :anything1 => "anything-1", :anything2 => 2 } 747 | } 748 | before { api.should_receive(:send_api_request).with(:publishRecordings, params_out) } 749 | it { api.publish_recordings(recordIDs, true, params_in) } 750 | end 751 | end 752 | 753 | describe "#delete_recordings" do 754 | 755 | context "with one recording ID" do 756 | context "in an array" do 757 | let(:recordIDs) { ["id-1"] } 758 | let(:req_params) { { :recordID => "id-1" } } 759 | before { api.should_receive(:send_api_request).with(:deleteRecordings, req_params) } 760 | it { api.delete_recordings(recordIDs) } 761 | end 762 | 763 | context "in a string" do 764 | let(:recordIDs) { "id-1" } 765 | let(:req_params) { { :recordID => "id-1" } } 766 | before { api.should_receive(:send_api_request).with(:deleteRecordings, req_params) } 767 | it { api.delete_recordings(recordIDs) } 768 | end 769 | end 770 | 771 | context "with several recording IDs" do 772 | context "in an array" do 773 | let(:recordIDs) { ["id-1", "id-2"] } 774 | let(:req_params) { { :recordID => "id-1,id-2" } } 775 | before { api.should_receive(:send_api_request).with(:deleteRecordings, req_params) } 776 | it { api.delete_recordings(recordIDs) } 777 | end 778 | 779 | context "in a string" do 780 | let(:recordIDs) { "id-1,id-2,id-3" } 781 | let(:req_params) { { :recordID => "id-1,id-2,id-3" } } 782 | before { api.should_receive(:send_api_request).with(:deleteRecordings, req_params) } 783 | it { api.delete_recordings(recordIDs) } 784 | end 785 | end 786 | 787 | context "accepts non standard options" do 788 | let(:recordIDs) { ["id-1"] } 789 | let(:params_in) { 790 | { :anything1 => "anything-1", :anything2 => 2 } 791 | } 792 | let(:params_out) { 793 | { :recordID => "id-1", :anything1 => "anything-1", :anything2 => 2 } 794 | } 795 | before { api.should_receive(:send_api_request).with(:deleteRecordings, params_out) } 796 | it { api.delete_recordings(recordIDs, params_in) } 797 | end 798 | end 799 | end 800 | 801 | it_should_behave_like "BigBlueButtonApi", "0.8" 802 | it_should_behave_like "BigBlueButtonApi", "0.81" 803 | it_should_behave_like "BigBlueButtonApi", "0.9" 804 | it_should_behave_like "BigBlueButtonApi", "1.0" 805 | end 806 | --------------------------------------------------------------------------------