├── .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 |
--------------------------------------------------------------------------------