bigbluebutton-api-ruby 0.0.11 → 0.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +5 -2
  2. data/.rvmrc +1 -1
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG.rdoc +19 -8
  5. data/Gemfile +9 -1
  6. data/Gemfile.lock +62 -9
  7. data/LICENSE +5 -7
  8. data/LICENSE_003 +20 -0
  9. data/README.rdoc +42 -19
  10. data/Rakefile +31 -19
  11. data/bigbluebutton-api-ruby.gemspec +5 -5
  12. data/examples/get_version_example.rb +18 -0
  13. data/examples/join_example.rb +59 -0
  14. data/examples/overall_0.7_example.rb +92 -0
  15. data/examples/prepare.rb +38 -0
  16. data/extras/bigbluebutton_bot.rb +64 -0
  17. data/extras/download_bot_from.txt +1 -0
  18. data/extras/test-presentation.pdf +0 -0
  19. data/features/check_status.feature +45 -0
  20. data/features/config.yml.example +21 -0
  21. data/features/create_meetings.feature +29 -0
  22. data/features/end_meetings.feature +27 -0
  23. data/features/join_meetings.feature +29 -0
  24. data/features/pre_upload_slides.feature +14 -0
  25. data/features/recordings.feature +34 -0
  26. data/features/step_definitions/check_status_steps.rb +119 -0
  27. data/features/step_definitions/common_steps.rb +122 -0
  28. data/features/step_definitions/create_meetings_steps.rb +54 -0
  29. data/features/step_definitions/end_meetings_steps.rb +49 -0
  30. data/features/step_definitions/join_meetings_steps.rb +39 -0
  31. data/features/step_definitions/pre_upload_slides_steps.rb +13 -0
  32. data/features/step_definitions/recordings_steps.rb +38 -0
  33. data/features/support/api_tests/configs.rb +51 -0
  34. data/features/support/env.rb +7 -0
  35. data/features/support/hooks.rb +11 -0
  36. data/lib/bigbluebutton_api.rb +301 -97
  37. data/lib/bigbluebutton_formatter.rb +105 -19
  38. data/lib/bigbluebutton_modules.rb +92 -0
  39. data/lib/hash_to_xml.rb +22 -51
  40. data/spec/bigbluebutton_api_0.8_spec.rb +273 -0
  41. data/spec/bigbluebutton_api_spec.rb +211 -117
  42. data/spec/bigbluebutton_formatter_spec.rb +178 -29
  43. data/spec/bigbluebutton_modules_spec.rb +95 -0
  44. data/spec/data/hash_to_xml_complex.xml +45 -0
  45. data/spec/hash_to_xml_spec.rb +143 -0
  46. data/spec/spec_helper.rb +4 -2
  47. data/spec/support/forgery/forgeries/random_name.rb +7 -0
  48. data/spec/support/forgery/forgeries/url.rb +5 -0
  49. metadata +47 -12
  50. data/test/config.yml.example +0 -9
  51. data/test/test.rb +0 -154
@@ -0,0 +1,122 @@
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['salt'],
6
+ @config_server['version'].to_s,
7
+ @config['debug'])
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
+ mobile_salt = @config_server.has_key?('mobile_salt') ? @config_server['mobile_salt'] : ""
52
+ BigBlueButtonBot.new(@api, @req.id, mobile_salt, count.to_i, @config['timeout_bot_start'])
53
+ end
54
+
55
+ When /^the response is an error with the key "(.*)"$/ do |key|
56
+ @req.exception.should_not be_nil
57
+ @req.exception.key.should == key
58
+ end
59
+
60
+ When /^the response is successful$/ do
61
+ @req.response[:returncode].should be_true
62
+ end
63
+
64
+ When /^the response is successful with no messages$/ do
65
+ @req.response[:returncode].should be_true
66
+ @req.response[:messageKey].should == ""
67
+ @req.response[:message].should == ""
68
+ end
69
+
70
+ When /^the response has the messageKey "(.*)"$/ do |key|
71
+ @req.response[:messageKey].should == key
72
+ @req.response[:message].should_not be_empty
73
+ end
74
+
75
+ When /^the response is successful and well formatted$/ do
76
+ case @req.method
77
+ when :create
78
+ steps %Q{ When the response to the create method is successful and well formatted }
79
+ when :end
80
+ steps %Q{ When the response to the end method is successful and well formatted }
81
+ when :get_recordings
82
+ steps %Q{ When the response to the get_recordings method is successful and well formatted }
83
+ end
84
+ end
85
+
86
+ When /^the response to the create method is successful and well formatted$/ do
87
+ @req.response[:returncode].should be_true
88
+ @req.response[:meetingID].should == @req.id
89
+ @req.response[:hasBeenForciblyEnded].should be_false
90
+ @req.response[:messageKey].should == ""
91
+ @req.response[:message].should == ""
92
+
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
@@ -0,0 +1,54 @@
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
@@ -0,0 +1,49 @@
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][:running] unless selected.nil?
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 that should be within 2 hours from now
42
+ @req.response[:startTime].should be_a(DateTime)
43
+ @req.response[:startTime].should < DateTime.now
44
+ @req.response[:startTime].should >= DateTime.now - (2/24.0)
45
+ @req.response[:endTime].should be_a(DateTime)
46
+ @req.response[:endTime].should < DateTime.now
47
+ @req.response[:endTime].should >= DateTime.now - (2/24.0)
48
+ @req.response[:endTime].should > @req.response[:startTime]
49
+ end
@@ -0,0 +1,39 @@
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
+
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,51 @@
1
+ module BigBlueButtonAPITests
2
+
3
+ # Test object that stores information about an API request
4
+ class APIRequest
5
+ attr_accessor :opts # options hash
6
+ attr_accessor :id # meetind id
7
+ attr_accessor :mod_pass # moderator password
8
+ attr_accessor :name # meeting name
9
+ attr_accessor :method # last api method called
10
+ attr_accessor :response # last api response
11
+ attr_accessor :exception # last exception
12
+ end
13
+
14
+ # Global configurations
15
+ module Configs
16
+ class << self
17
+ attr_accessor :cfg # configuration file
18
+ attr_accessor :cfg_server # shortcut to the choosen server configs
19
+ attr_accessor :req # api request
20
+
21
+ def initialize_cfg
22
+ config_file = File.join(File.dirname(__FILE__), '..', '..', 'config.yml')
23
+ unless File.exist? config_file
24
+ throw Exception.new(config_file + " does not exists. Copy the example and configure your server.")
25
+ end
26
+ config = YAML.load_file(config_file)
27
+ config
28
+ end
29
+
30
+ def initialize_cfg_server
31
+ if ENV['SERVER']
32
+ unless self.cfg['servers'].has_key?(ENV['SERVER'])
33
+ throw Exception.new("Server #{ENV['SERVER']} does not exists in your configuration file.")
34
+ end
35
+ srv = self.cfg['servers'][ENV['SERVER']]
36
+ else
37
+ srv = self.cfg['servers'][self.cfg['servers'].keys.first]
38
+ end
39
+ srv['bbb_version'] = '0.7' unless srv.has_key?('bbb_version')
40
+ srv
41
+ end
42
+
43
+ def load
44
+ self.cfg = initialize_cfg
45
+ self.cfg_server = initialize_cfg_server
46
+ self.req = BigBlueButtonAPITests::APIRequest.new
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', '..', 'lib')
2
+
3
+ require 'bigbluebutton_api'
4
+ require 'forgery'
5
+ Dir["#{File.dirname(__FILE__)}/../../spec/support/forgery/**/*.rb"].each { |f| require f }
6
+
7
+ Dir["#{File.dirname(__FILE__)}/../../extras/**/*.rb"].each { |f| require f }
@@ -0,0 +1,11 @@
1
+ Before do
2
+ # stores the global configurations in variables that are easier to access
3
+ BigBlueButtonAPITests::Configs.load
4
+ @config = BigBlueButtonAPITests::Configs.cfg
5
+ @config_server = BigBlueButtonAPITests::Configs.cfg_server
6
+ @req = BigBlueButtonAPITests::Configs.req
7
+ end
8
+
9
+ After do |scenario|
10
+ BigBlueButtonBot.finalize
11
+ end
@@ -3,37 +3,27 @@ require 'cgi'
3
3
  require 'rexml/document'
4
4
  require 'digest/sha1'
5
5
  require 'rubygems'
6
- require 'nokogiri'
7
6
  require 'hash_to_xml'
8
7
  require 'bigbluebutton_exception'
9
8
  require 'bigbluebutton_formatter'
9
+ require 'bigbluebutton_modules'
10
10
 
11
11
  module BigBlueButton
12
12
 
13
- # This class provides access to the BigBlueButton API. BigBlueButton
14
- # is an open source project that provides web conferencing for distance
15
- # education (http://code.google.com/p/bigbluebutton/wiki/API). This API
16
- # was developed to support the following version of BBB: 0.7, 0.8 (soon)
13
+ # This class provides access to the BigBlueButton API. For more details see README.rdoc.
17
14
  #
18
15
  # Sample usage of the API is as follows:
19
- # 1) Create a meeting with the create_meeting call
20
- # 2) Direct a user to either join_meeting_url
21
- # 3) To force meeting to end, call end_meeting
16
+ # 1. Create a meeting with create_meeting;
17
+ # 2. Redirect a user to the URL returned by join_meeting_url;
18
+ # 3. Get information about the meetings with get_meetings and get_meeting_info;
19
+ # 4. To force meeting to end, call end_meeting .
22
20
  #
23
- # 0.0.4+:
24
- # Author:: Leonardo Crauss Daronco (mailto:leonardodaronco@gmail.com)
25
- # Copyright:: Copyright (c) 2011 Leonardo Crauss Daronco
26
- # Project:: GT-Mconf: Multiconference system for interoperable web and mobile @ PRAV Labs - UFRGS
27
- # License:: Distributes under same terms as Ruby
28
- #
29
- # 0.0.3 and below:
30
- # Author:: Joe Kinsella (mailto:joe.kinsella@gmail.com)
31
- # Copyright:: Copyright (c) 2010 Joe Kinsella
32
- # License:: Distributes under same terms as Ruby
33
- #
34
- # Considerations about the returning hash:
35
- # * The XML returned by BBB is converted to a Hash. See the desired method's documentation for examples.
36
- # * Three values will *always* exist in the hash: :returncode (boolean), :messageKey (string) and :message (string)
21
+ # Important info about the data returned by the methods:
22
+ # * The XML returned by BBB is converted to a Hash. See individual method's documentation for examples.
23
+ # * Three values will *always* exist in the hash:
24
+ # * :returncode (boolean)
25
+ # * :messageKey (string)
26
+ # * :message (string)
37
27
  # * Some of the values returned by BBB are converted to better represent the data. Some of these are listed
38
28
  # bellow. They will *always* have the type informed:
39
29
  # * :meetingID (string)
@@ -52,11 +42,11 @@ module BigBlueButton
52
42
  # salt:: Secret salt for this server
53
43
  # version:: API version: 0.7 (valid for 0.7, 0.71 and 0.71a)
54
44
  def initialize(url, salt, version='0.7', debug=false)
55
- @supported_versions = ['0.7']
45
+ @supported_versions = ['0.7', '0.8']
56
46
  @url = url
57
47
  @salt = salt
58
48
  @debug = debug
59
- @timeout = 2 # 2 seconds timeout for get requests
49
+ @timeout = 10 # default timeout for api requests
60
50
 
61
51
  @version = version || get_api_version
62
52
  unless @supported_versions.include?(@version)
@@ -66,74 +56,107 @@ module BigBlueButton
66
56
  puts "BigBlueButtonAPI: Using version #{@version}" if @debug
67
57
  end
68
58
 
69
- # Returns the url used to join the meeting
70
- # meeting_id:: Unique identifier for the meeting
71
- # user_name:: Name of the user
72
- # password:: Password for this meeting - used to set the user as moderator or attendee
73
- # user_id:: Unique identifier for this user
74
- # web_voice_conf:: Custom voice-extension for users using VoIP
75
- def join_meeting_url(meeting_id, user_name, password,
76
- user_id = nil, web_voice_conf = nil)
77
-
78
- params = { :meetingID => meeting_id, :password => password, :fullName => user_name,
79
- :userID => user_id, :webVoiceConf => web_voice_conf }
80
- get_url(:join, params)
81
- end
59
+
60
+ #
61
+ # API calls since 0.7
62
+ #
63
+
82
64
 
83
65
  # Creates a new meeting. Returns the hash with the response or
84
66
  # throws BigBlueButtonException on failure.
85
- # meeting_name:: Name for the meeting
86
- # meeting_id:: Unique identifier for the meeting
87
- # moderator_password:: Moderator password
88
- # attendee_password:: Attendee password
89
- # welcome_message:: Welcome message to display in chat window
90
- # dialin_number:: Dial in number for conference using a regular phone
91
- # logout_url:: URL to return user to after exiting meeting
92
- # voice_bridge:: Voice conference number
67
+ # meeting_name (string):: Name for the meeting
68
+ # meeting_id (string, integer):: Unique identifier for the meeting
69
+ # options (Hash):: Hash with optional parameters. The accepted parameters are:
70
+ # moderatorPW (string, int), attendeePW (string, int), welcome (string),
71
+ # dialNumber (int), logoutURL (string), maxParticipants (int),
72
+ # voiceBridge (int), record (boolean), duration (int) and "meta" parameters
73
+ # (usually strings). If a parameter passed in the hash is not supported it will
74
+ # simply be discarded. For details about each see BBB API docs.
75
+ # modules (BigBlueButtonModules):: Configuration for the modules. The modules are sent as an xml and the
76
+ # request will use an HTTP POST instead of GET. Currently only the
77
+ # "presentation" module is available. Only used for version > 0.8.
78
+ # See usage examples below.
93
79
  #
94
- # === Return examples (for 0.7)
80
+ # === Example
81
+ #
82
+ # options = { :moderatorPW => "123", :attendeePW => "321", :welcome => "Welcome here!",
83
+ # :dialNumber => 5190909090, :logoutURL => "http://mconf.org", :maxParticipants => 25,
84
+ # :voiceBridge => 76543, :record => "true", :duration => 0, :meta_category => "Remote Class" }
85
+ # create_meeting("My Meeting", "my-meeting", options)
86
+ #
87
+ # === Example with modules (see BigBlueButtonModules docs for more)
88
+ #
89
+ # modules = BigBlueButton::BigBlueButtonModules.new
90
+ # modules.add_presentation(:url, "http://www.samplepdf.com/sample.pdf")
91
+ # modules.add_presentation(:url, "http://www.samplepdf.com/sample2.pdf")
92
+ # modules.add_presentation(:file, "presentations/class01.ppt")
93
+ # modules.add_presentation(:base64, "JVBERi0xLjQKJ....[clipped here]....0CiUlRU9GCg==", "first-class.pdf")
94
+ # create_meeting("My Meeting", "my-meeting", nil, modules)
95
+ #
96
+ # === Example response for 0.7
95
97
  #
96
98
  # On successful creation:
97
99
  #
98
100
  # {
99
- # :returncode=>true, :meetingID=>"bigbluebutton-api-ruby-test",
100
- # :attendeePW=>"1234", :moderatorPW=>"4321", :hasBeenForciblyEnded=>false,
101
- # :messageKey=>"", :message=>""
101
+ # :returncode => true, :meetingID => "test",
102
+ # :attendeePW => "1234", :moderatorPW => "4321", :hasBeenForciblyEnded => false,
103
+ # :messageKey => "", :message => ""
102
104
  # }
103
105
  #
104
106
  # Meeting that was forcibly ended:
105
107
  #
106
108
  # {
107
- # :returncode=>true, :meetingID=>"bigbluebutton-api-ruby-test",
108
- # :attendeePW=>"1234", :moderatorPW=>"4321", :hasBeenForciblyEnded=>true,
109
- # :messageKey=>"duplicateWarning",
110
- # :message=>"This conference was already in existence and may currently be in progress."
109
+ # :returncode => true, :meetingID => "test",
110
+ # :attendeePW => "1234", :moderatorPW => "4321", :hasBeenForciblyEnded => true,
111
+ # :messageKey => "duplicateWarning",
112
+ # :message => "This conference was already in existence and may currently be in progress."
111
113
  # }
112
114
  #
113
- def create_meeting(meeting_name, meeting_id, moderator_password = nil, attendee_password = nil,
114
- welcome_message = nil, dial_number = nil, logout_url = nil,
115
- max_participants = nil, voice_bridge = nil)
116
-
117
- params = { :name => meeting_name, :meetingID => meeting_id,
118
- :moderatorPW => moderator_password, :attendeePW => attendee_password,
119
- :welcome => welcome_message, :dialNumber => dial_number,
120
- :logoutURL => logout_url, :maxParticpants => max_participants,
121
- :voiceBridge => voice_bridge }
115
+ # === Example response for 0.8
116
+ #
117
+ # {
118
+ # :returncode => true, :meetingID => "Test", :createTime => 1308591802,
119
+ # :attendeePW => "1234", :moderatorPW => "4321", :hasBeenForciblyEnded => false,
120
+ # :messageKey => "", :message => ""
121
+ # }
122
+ #
123
+ def create_meeting(meeting_name, meeting_id, options={}, modules=nil)
124
+ valid_options = [:moderatorPW, :attendeePW, :welcome, :maxParticipants,
125
+ :dialNumber, :voiceBridge, :webVoice, :logoutURL]
126
+
127
+ selected_opt = options.clone
128
+ if @version >= "0.8"
129
+ # v0.8 added "record", "duration" and "meta_" parameters
130
+ valid_options += [:record, :duration]
131
+ selected_opt.reject!{ |k,v| !valid_options.include?(k) and !(k.to_s =~ /^meta_.*$/) }
132
+ selected_opt[:record] = selected_opt[:record].to_s if selected_opt.has_key?(:record)
133
+ else
134
+ selected_opt.reject!{ |k,v| !valid_options.include?(k) }
135
+ end
136
+ params = { :name => meeting_name, :meetingID => meeting_id }.merge(selected_opt)
122
137
 
123
- response = send_api_request(:create, params)
138
+ # with modules we send a post request (only for >= 0.8)
139
+ if modules and @version >= "0.8"
140
+ response = send_api_request(:create, params, modules.to_xml)
141
+ else
142
+ response = send_api_request(:create, params)
143
+ end
124
144
 
125
145
  formatter = BigBlueButtonFormatter.new(response)
126
146
  formatter.to_string(:meetingID)
127
147
  formatter.to_string(:moderatorPW)
128
148
  formatter.to_string(:attendeePW)
129
149
  formatter.to_boolean(:hasBeenForciblyEnded)
150
+ if @version >= "0.8"
151
+ formatter.to_int(:createTime)
152
+ end
130
153
 
131
154
  response
132
155
  end
133
156
 
134
157
  # Ends an existing meeting. Throws BigBlueButtonException on failure.
135
- # meeting_id:: Unique identifier for the meeting
136
- # moderator_password:: Moderator password
158
+ # meeting_id (string, int):: Unique identifier for the meeting
159
+ # moderator_password (string, int):: Moderator password
137
160
  #
138
161
  # === Return examples (for 0.7)
139
162
  #
@@ -151,12 +174,29 @@ module BigBlueButton
151
174
 
152
175
  # Returns true or false as to whether meeting is open. A meeting is
153
176
  # only open after at least one participant has joined.
154
- # meeting_id:: Unique identifier for the meeting
177
+ # meeting_id (string, int):: Unique identifier for the meeting
155
178
  def is_meeting_running?(meeting_id)
156
179
  hash = send_api_request(:isMeetingRunning, { :meetingID => meeting_id } )
157
180
  BigBlueButtonFormatter.new(hash).to_boolean(:running)
158
181
  end
159
182
 
183
+ # Returns the url used to join the meeting
184
+ # meeting_id (string, int):: Unique identifier for the meeting
185
+ # user_name (string):: Name of the user
186
+ # password (string):: Password for this meeting - used to set the user as moderator or attendee
187
+ # options (Hash):: Hash with optional parameters. The accepted parameters are:
188
+ # userID (string, int), webVoiceConf (string, int) and createTime (int).
189
+ # For details about each see BBB API docs.
190
+ def join_meeting_url(meeting_id, user_name, password, options={})
191
+ valid_options = [:userID, :webVoiceConf]
192
+ valid_options += [:createTime] if @version >= "0.8"
193
+ options.reject!{ |k,v| !valid_options.include?(k) }
194
+
195
+ params = { :meetingID => meeting_id, :password => password, :fullName => user_name }.merge(options)
196
+
197
+ get_url(:join, params)
198
+ end
199
+
160
200
  # Warning: As of this version of the gem, this call does not work
161
201
  # (instead of returning XML response, it should join the meeting).
162
202
  #
@@ -164,14 +204,19 @@ module BigBlueButton
164
204
  # directing the user's browser to moderator_url or attendee_url
165
205
  # (note: this will still be required however to actually use bbb).
166
206
  # Returns the URL a user can use to enter this meeting.
167
- # meeting_id:: Unique identifier for the meeting
168
- # user_name:: Name of the user
169
- # password:: Moderator or attendee password for this meeting
170
- # user_id:: Unique identifier for this user
171
- # web_voice_conf:: Custom voice-extension for users using VoIP
172
- def join_meeting(meeting_id, user_name, password, user_id = nil, web_voice_conf = nil)
173
- params = { :meetingID => meeting_id, :password => password, :fullName => user_name,
174
- :userID => user_id, :webVoiceConf => web_voice_conf }
207
+ # meeting_id (string, int):: Unique identifier for the meeting
208
+ # user_name (string):: Name of the user
209
+ # password (string, int):: Moderator or attendee password for this meeting
210
+ # options (Hash):: Hash with optional parameters. The accepted parameters are:
211
+ # userID (string, int), webVoiceConf (string, int) and createTime (int).
212
+ # For details about each see BBB API docs.
213
+ def join_meeting(meeting_id, user_name, password, options={})
214
+ valid_options = [:userID, :webVoiceConf]
215
+ valid_options += [:createTime] if @version >= "0.8"
216
+ options.reject!{ |k,v| !valid_options.include?(k) }
217
+
218
+ params = { :meetingID => meeting_id, :password => password, :fullName => user_name }.merge(options)
219
+
175
220
  send_api_request(:join, params)
176
221
  end
177
222
 
@@ -179,10 +224,10 @@ module BigBlueButton
179
224
  # See the API documentation for details on the return XML
180
225
  # (http://code.google.com/p/bigbluebutton/wiki/API).
181
226
  #
182
- # meeting_id:: Unique identifier for the meeting
183
- # password:: Moderator password for this meeting
227
+ # meeting_id (string, int):: Unique identifier for the meeting
228
+ # password (string, int):: Moderator password for this meeting
184
229
  #
185
- # === Return examples (for 0.7)
230
+ # === Example responses for 0.7
186
231
  #
187
232
  # With attendees:
188
233
  #
@@ -205,12 +250,23 @@ module BigBlueButton
205
250
  # :attendees=>[], :messageKey=>"", :message=>""
206
251
  # }
207
252
  #
253
+ # === Example responses for 0.8
254
+ #
255
+ # {
256
+ # :returncode => true, :meetingName => "test", :meetingID => "test", :createTime => 1321906390524,
257
+ # :voiceBridge => 72194, :attendeePW => "1234", :moderatorPW => "4321", :running => false, :recording => false,
258
+ # :hasBeenForciblyEnded => false, :startTime => nil, :endTime => nil, :participantCount => 0, :maxUsers => 9,
259
+ # :moderatorCount => 0, :attendees => [],
260
+ # :metadata => { :two => "TWO", :one => "one" },
261
+ # :messageKey => "", :message => ""
262
+ # }
263
+ #
208
264
  def get_meeting_info(meeting_id, password)
209
265
  response = send_api_request(:getMeetingInfo, { :meetingID => meeting_id, :password => password } )
210
266
 
211
267
  formatter = BigBlueButtonFormatter.new(response)
212
268
  formatter.flatten_objects(:attendees, :attendee)
213
- response[:attendees].each { |a| formatter.format_attendee(a) }
269
+ response[:attendees].each { |a| BigBlueButtonFormatter.format_attendee(a) }
214
270
 
215
271
  formatter.to_string(:meetingID)
216
272
  formatter.to_string(:moderatorPW)
@@ -219,6 +275,15 @@ module BigBlueButton
219
275
  formatter.to_boolean(:running)
220
276
  formatter.to_datetime(:startTime)
221
277
  formatter.to_datetime(:endTime)
278
+ formatter.to_int(:participantCount)
279
+ formatter.to_int(:moderatorCount)
280
+ if @version >= "0.8"
281
+ formatter.to_string(:meetingName)
282
+ formatter.to_int(:maxUsers)
283
+ formatter.to_int(:voiceBridge)
284
+ formatter.to_int(:createTime)
285
+ formatter.to_boolean(:recording)
286
+ end
222
287
 
223
288
  response
224
289
  end
@@ -226,7 +291,7 @@ module BigBlueButton
226
291
  # Returns a hash object containing information about the meetings currently existent in the BBB
227
292
  # server, either they are running or not.
228
293
  #
229
- # === Return examples (for 0.7)
294
+ # === Example responses for 0.7
230
295
  #
231
296
  # Server with one or more meetings:
232
297
  #
@@ -247,7 +312,7 @@ module BigBlueButton
247
312
 
248
313
  formatter = BigBlueButtonFormatter.new(response)
249
314
  formatter.flatten_objects(:meetings, :meeting)
250
- response[:meetings].each { |m| formatter.format_meeting(m) }
315
+ response[:meetings].each { |m| BigBlueButtonFormatter.format_meeting(m) }
251
316
  response
252
317
  end
253
318
 
@@ -259,13 +324,119 @@ module BigBlueButton
259
324
  response[:returncode] ? response[:version].to_s : ""
260
325
  end
261
326
 
262
- # Make a simple request to the server to test the connection
327
+
328
+
329
+ #
330
+ # API calls since 0.8
331
+ #
332
+
333
+ # Retrieves the recordings that are available for playback for a given meetingID (or set of meeting IDs).
334
+ # options (Hash):: Hash with optional parameters. The accepted parameters are:
335
+ # :meetingID (string, Array). For details about each see BBB API docs.
336
+ # Any of the following values are accepted for :meetingID :
337
+ # :meetingID => "id1"
338
+ # :meetingID => "id1,id2,id3"
339
+ # :meetingID => ["id1"]
340
+ # :meetingID => ["id1", "id2", "id3"]
341
+ #
342
+ # === Example responses
343
+ # TODO: this example is not accurate yet
344
+ #
345
+ # { :returncode => true,
346
+ # :recordings => [
347
+ # {
348
+ # :recordID => "7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268", :meetingID=>"bd1811beecd20f24314819a52ec202bf446ab94b",
349
+ # :name => "Evening Class1", :published => true,
350
+ # :startTime => #<DateTime: 2011-11-18T12:10:23+00:00 (212188378223/86400,0/1,2299161)>,
351
+ # :endTime => #<DateTime: 2011-11-18T12:12:25+00:00 (42437675669/17280,0/1,2299161)>,
352
+ # :metadata => { :course => "Fundamentals Of JAVA",
353
+ # :description => "List of recordings",
354
+ # :activity => "Evening Class1" },
355
+ # :playback => {
356
+ # :format => {
357
+ # :type => "slides",
358
+ # :url => "http://test-install.blindsidenetworks.com/playback/slides/playback.html?meetingId=7f5745a08b24fa27551e7a065849dda3ce65dd32-1321618219268",
359
+ # :length=>3
360
+ # }
361
+ # }
362
+ # },
363
+ # { :recordID => "183f0bf3a0982a127bdb8161-13085974450", :meetingID => "CS102",
364
+ # ...
365
+ # ...
366
+ # }
367
+ # ]
368
+ # }
369
+ #
370
+ def get_recordings(options={})
371
+ raise BigBlueButtonException.new("Method only supported for versions >= 0.8") if @version < "0.8"
372
+
373
+ valid_options = [:meetingID]
374
+ options.reject!{ |k,v| !valid_options.include?(k) }
375
+
376
+ # ["id1", "id2", "id3"] becomes "id1,id2,id3"
377
+ if options.has_key?(:meetingID)
378
+ options[:meetingID] = options[:meetingID].join(",") if options[:meetingID].instance_of?(Array)
379
+ end
380
+
381
+ response = send_api_request(:getRecordings, options)
382
+
383
+ formatter = BigBlueButtonFormatter.new(response)
384
+ formatter.flatten_objects(:recordings, :recording)
385
+ response[:recordings].each { |r| BigBlueButtonFormatter.format_recording(r) }
386
+ response
387
+ end
388
+
389
+ # Publish and unpublish recordings for a given recordID (or set of record IDs).
390
+ # recordIDs (string, Array):: ID or IDs of the target recordings.
391
+ # Any of the following values are accepted:
392
+ # "id1"
393
+ # "id1,id2,id3"
394
+ # ["id1"]
395
+ # ["id1", "id2", "id3"]
396
+ # publish (boolean):: Publish or unpublish the recordings?
397
+ #
398
+ # === Example responses
399
+ #
400
+ # { :returncode => true, :published => true }
401
+ #
402
+ def publish_recordings(recordIDs, publish)
403
+ raise BigBlueButtonException.new("Method only supported for versions >= 0.8") if @version < "0.8"
404
+
405
+ recordIDs = recordIDs.join(",") if recordIDs.instance_of?(Array) # ["id1", "id2"] becomes "id1,id2"
406
+ response = send_api_request(:publishRecordings, { :recordID => recordIDs, :publish => publish.to_s })
407
+ end
408
+
409
+ # Delete one or more recordings for a given recordID (or set of record IDs).
410
+ # recordIDs (string, Array):: ID or IDs of the target recordings.
411
+ # Any of the following values are accepted:
412
+ # "id1"
413
+ # "id1,id2,id3"
414
+ # ["id1"]
415
+ # ["id1", "id2", "id3"]
416
+ #
417
+ # === Example responses
418
+ #
419
+ # { :returncode => true, :deleted => true }
420
+ #
421
+ def delete_recordings(recordIDs)
422
+ raise BigBlueButtonException.new("Method only supported for versions >= 0.8") if @version < "0.8"
423
+
424
+ recordIDs = recordIDs.join(",") if recordIDs.instance_of?(Array) # ["id1", "id2"] becomes "id1,id2"
425
+ response = send_api_request(:deleteRecordings, { :recordID => recordIDs })
426
+ end
427
+
428
+
429
+ #
430
+ # Helper functions
431
+ #
432
+
433
+ # Make a simple request to the server to test the connection.
263
434
  def test_connection
264
435
  response = send_api_request(:index)
265
436
  response[:returncode]
266
437
  end
267
438
 
268
- # API's are equal if all the following attributes are equal
439
+ # API's are equal if all the following attributes are equal.
269
440
  def ==(other)
270
441
  r = true
271
442
  [:url, :supported_versions, :salt, :version, :debug].each do |param|
@@ -274,11 +445,20 @@ module BigBlueButton
274
445
  r
275
446
  end
276
447
 
448
+ # Returns the HTTP response object returned in the last API call.
277
449
  def last_http_response
278
450
  @http_response
279
451
  end
280
452
 
281
- def get_url(method, data={})
453
+ # Returns the XML returned in the last API call.
454
+ def last_xml_response
455
+ @xml_response
456
+ end
457
+
458
+ # Formats an API call URL for the method 'method' using the parameters in 'params'.
459
+ # method (symbol):: The API method to be called (:create, :index, :join, and others)
460
+ # params (Hash):: The parameters to be passed in the URL
461
+ def get_url(method, params={})
282
462
  if method == :index
283
463
  return @url
284
464
  end
@@ -286,38 +466,50 @@ module BigBlueButton
286
466
  url = "#{@url}/#{method}?"
287
467
 
288
468
  # stringify and escape all params
289
- data.delete_if { |k, v| v.nil? } unless data.nil?
290
- params = ""
291
- params = data.map{ |k,v| "#{k}=" + CGI::escape(v.to_s) unless k.nil? || v.nil? }.join("&")
469
+ params.delete_if { |k, v| v.nil? } unless params.nil?
470
+ params_string = ""
471
+ params_string = params.map{ |k,v| "#{k}=" + CGI::escape(v.to_s) unless k.nil? || v.nil? }.join("&")
292
472
 
293
473
  # checksum calc
294
- checksum_param = params + @salt
474
+ checksum_param = params_string + @salt
295
475
  checksum_param = method.to_s + checksum_param
296
476
  checksum = Digest::SHA1.hexdigest(checksum_param)
297
477
 
298
478
  # final url
299
- url += "#{params}&" unless params.empty?
479
+ url += "#{params_string}&" unless params_string.empty?
300
480
  url += "checksum=#{checksum}"
301
481
  end
302
482
 
303
- def send_api_request(method, data = {})
304
- url = get_url(method, data)
305
-
306
- @http_response = send_request(url)
483
+ # Performs an API call.
484
+ #
485
+ # Throws a BigBlueButtonException if something goes wrong (e.g. server offline).
486
+ # Also throws an exception of the request was not successful (i.e. returncode == FAILED).
487
+ #
488
+ # Only formats the standard values in the response (the ones that exist in all responses).
489
+ #
490
+ # method (symbol):: The API method to be called (:create, :index, :join, and others)
491
+ # params (Hash):: The parameters to be passed in the URL
492
+ # data (string):: Data to be sent with the request. If set, the request will use an HTTP
493
+ # POST instead of a GET and the data will be sent in the request body.
494
+ def send_api_request(method, params={}, data=nil)
495
+ url = get_url(method, params)
496
+
497
+ @http_response = send_request(url, data)
307
498
  return { } if @http_response.body.empty?
308
499
 
309
500
  # 'Hashify' the XML
310
- hash = Hash.from_xml(@http_response.body)
501
+ @xml_response = @http_response.body
502
+ hash = Hash.from_xml(@xml_response)
311
503
 
312
504
  # simple validation of the xml body
313
- unless hash.has_key?(:response) and hash[:response].has_key?(:returncode)
505
+ unless hash.has_key?(:returncode)
314
506
  raise BigBlueButtonException.new("Invalid response body. Is the API URL correct? \"#{@url}\", version #{@version}")
315
507
  end
316
508
 
317
509
  # default cleanup in the response
318
510
  hash = BigBlueButtonFormatter.new(hash).default_formatting
319
511
 
320
- # all responses should have a returncode
512
+ # if the return code is an error generates an exception
321
513
  unless hash[:returncode]
322
514
  exception = BigBlueButtonException.new(hash[:message])
323
515
  exception.key = hash.has_key?(:messageKey) ? hash[:messageKey] : ""
@@ -329,20 +521,32 @@ module BigBlueButton
329
521
 
330
522
  protected
331
523
 
332
- def send_request(url)
524
+ # :nodoc:
525
+ # If data is set, uses a POST with data in the request body
526
+ # Otherwise uses GET
527
+ def send_request(url, data=nil)
333
528
  begin
334
529
  puts "BigBlueButtonAPI: URL request = #{url}" if @debug
335
530
  url_parsed = URI.parse(url)
336
531
  http = Net::HTTP.new(url_parsed.host, url_parsed.port)
337
532
  http.open_timeout = @timeout
338
533
  http.read_timeout = @timeout
339
- response = http.get(url_parsed.request_uri)
534
+ if data.nil?
535
+ response = http.get(url_parsed.request_uri)
536
+ else
537
+ puts "BigBlueButtonAPI: Sending as a POST request with data.size = #{data.size}" if @debug
538
+ opts = { 'Content-Type' => 'text/xml' }
539
+ response = http.post(url_parsed.request_uri, data, opts)
540
+ end
340
541
  puts "BigBlueButtonAPI: URL response = #{response.body}" if @debug
542
+
341
543
  rescue TimeoutError => error
342
544
  raise BigBlueButtonException.new("Timeout error. Your server is probably down: \"#{@url}\"")
545
+
343
546
  rescue Exception => error
344
547
  raise BigBlueButtonException.new("Connection error. Your URL is probably incorrect: \"#{@url}\"")
345
548
  end
549
+
346
550
  response
347
551
  end
348
552