ruby_ami 0.1.1

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.
@@ -0,0 +1,207 @@
1
+ Given "a new lexer" do
2
+ @lexer = IntrospectiveManagerStreamLexer.new
3
+ @custom_stanzas = {}
4
+ @custom_events = {}
5
+
6
+ @GivenPong = lambda do |with_or_without, action_id, number|
7
+ number = number == "a" ? 1 : number.to_i
8
+ data = case with_or_without
9
+ when "with" then "Response: Pong\r\nActionID: #{action_id}\r\n\r\n"
10
+ when "without" then "Response: Pong\r\n\r\n"
11
+ else raise "Do not recognize preposition #{with_or_without.inspect}. Should be either 'with' or 'without'"
12
+ end
13
+ number.times do
14
+ @lexer << data
15
+ end
16
+ end
17
+ end
18
+
19
+ Given "a version header for AMI $version" do |version|
20
+ @lexer << "Asterisk Call Manager/1.0\r\n"
21
+ end
22
+
23
+ Given "a normal login success with events" do
24
+ @lexer << fixture('login/standard/success')
25
+ end
26
+
27
+ Given "a normal login success with events split into two pieces" do
28
+ stanza = fixture('login/standard/success')
29
+ @lexer << stanza[0...3]
30
+ @lexer << stanza[3..-1]
31
+ end
32
+
33
+ Given "a stanza break" do
34
+ @lexer << "\r\n\r\n"
35
+ end
36
+
37
+ Given "a multi-line Response:Follows body of $method_name" do |method_name|
38
+ multi_line_response_body = send(:follows_body_text, method_name)
39
+
40
+ multi_line_response = format_newlines(<<-RESPONSE + "\r\n") % multi_line_response_body
41
+ Response: Follows\r
42
+ Privilege: Command\r
43
+ ActionID: 123123\r
44
+ %s\r
45
+ --END COMMAND--\r\n\r
46
+ RESPONSE
47
+
48
+ @lexer << multi_line_response
49
+ end
50
+
51
+ Given "a multi-line Response:Follows response simulating uptime" do
52
+ uptime_response = "Response: Follows\r
53
+ Privilege: Command\r
54
+ System uptime: 46 minutes, 30 seconds\r
55
+ --END COMMAND--\r\n\r\n"
56
+ @lexer << uptime_response
57
+ end
58
+
59
+ Given "syntactically invalid $name" do |name|
60
+ @lexer << send(:syntax_error_data, name)
61
+ end
62
+
63
+ Given /^(\d+) Pong responses with an ActionID of ([\d\w.]+)$/ do |number, action_id|
64
+ @GivenPong.call "with", action_id, number
65
+ end
66
+
67
+ Given /^a Pong response with an ActionID of ([\d\w.]+)$/ do |action_id|
68
+ @GivenPong.call "with", action_id, 1
69
+ end
70
+
71
+ Given /^(\d+) Pong responses without an ActionID$/ do |number|
72
+ @GivenPong.call "without", Time.now.to_f, number
73
+ end
74
+
75
+ Given /^a custom stanza named "(\w+)"$/ do |name|
76
+ @custom_stanzas[name] = "Response: Success\r\n"
77
+ end
78
+
79
+ Given 'the custom stanza named "$name" has key "$key" with value "$value"' do |name,key,value|
80
+ @custom_stanzas[name] << "#{key}: #{value}\r\n"
81
+ end
82
+
83
+ Given 'an AMI error whose message is "$message"' do |message|
84
+ @lexer << "Response: Error\r\nMessage: #{message}\r\n\r\n"
85
+ end
86
+
87
+ Given 'an immediate response with text "$text"' do |text|
88
+ @lexer << "#{text}\r\n\r\n"
89
+ end
90
+
91
+ Given 'a custom event with name "$event_name" identified by "$identifier"' do |event_name, identifer|
92
+ @custom_events[identifer] = {:Event => event_name }
93
+ end
94
+
95
+ Given 'a custom header for event identified by "$identifier" whose key is "$key" and value is "$value"' do |identifier, key, value|
96
+ @custom_events[identifier][key] = value
97
+ end
98
+
99
+ Given "an Authentication Required error" do
100
+ @lexer << "Response: Error\r\nActionID: BPJeKqW2-SnVg-PyFs-vkXT-7AWVVPD0N3G7\r\nMessage: Authentication Required\r\n\r\n"
101
+ end
102
+
103
+ Given "a follows packet with a colon in it" do
104
+ @lexer << follows_body_text("with_colon")
105
+ end
106
+
107
+ ########################################
108
+ #### WHEN
109
+ ########################################
110
+
111
+ When 'the custom stanza named "$name" is added to the buffer' do |name|
112
+ @lexer << (@custom_stanzas[name] + "\r\n")
113
+ end
114
+
115
+ When 'the custom event identified by "$identifier" is added to the buffer' do |identifier|
116
+ custom_event = @custom_events[identifier].clone
117
+ event_name = custom_event.delete :Event
118
+ stringified_event = "Event: #{event_name}\r\n"
119
+ custom_event.each_pair do |key,value|
120
+ stringified_event << "#{key}: #{value}\r\n"
121
+ end
122
+ stringified_event << "\r\n"
123
+ @lexer << stringified_event
124
+ end
125
+
126
+ When "the buffer is lexed" do
127
+ @lexer.resume!
128
+ end
129
+
130
+ ########################################
131
+ #### THEN
132
+ ########################################
133
+
134
+ Then "the protocol should have lexed without syntax errors" do
135
+ current_pointer = @lexer.send(:instance_variable_get, :@current_pointer)
136
+ data_ending_pointer = @lexer.send(:instance_variable_get, :@data_ending_pointer)
137
+ current_pointer.should equal(data_ending_pointer)
138
+ @lexer.syntax_errors.size.should equal(0)
139
+ end
140
+
141
+ Then /^the protocol should have lexed with (\d+) syntax errors?$/ do |number|
142
+ @lexer.syntax_errors.size.should equal(number.to_i)
143
+ end
144
+
145
+ Then "the syntax error fixture named $name should have been encountered" do |name|
146
+ irregularity = send(:syntax_error_data, name)
147
+ @lexer.syntax_errors.find { |error| error == irregularity }.should_not be_nil
148
+ end
149
+
150
+ Then /^(\d+) messages? should have been received$/ do |number_received|
151
+ @lexer.received_messages.size.should equal(number_received.to_i)
152
+ end
153
+
154
+ Then /^the 'follows' body of (\d+) messages? received should equal (\w+)$/ do |number, method_name|
155
+ multi_line_response = follows_body_text method_name
156
+ @lexer.received_messages.should_not be_empty
157
+ @lexer.received_messages.select do |message|
158
+ message.text_body == multi_line_response
159
+ end.size.should eql(number.to_i)
160
+ end
161
+
162
+ Then "the version should be set to $version" do |version|
163
+ @lexer.ami_version.should eql(version.to_f)
164
+ end
165
+
166
+ Then /^the ([\w\d]*) message received should have a key "([^\"]*)" with value "([^\"]*)"$/ do |ordered,key,value|
167
+ ordered = ordered[/^(\d+)\w+$/, 1].to_i - 1
168
+ @lexer.received_messages[ordered][key].should eql(value)
169
+ end
170
+
171
+ Then "$number AMI error should have been received" do |number|
172
+ @lexer.ami_errors.size.should equal(number.to_i)
173
+ end
174
+
175
+ Then 'the $order AMI error should have the message "$message"' do |order, message|
176
+ order = order[/^(\d+)\w+$/, 1].to_i - 1
177
+ @lexer.ami_errors[order].should be_kind_of(RubyAMI::Error)
178
+ @lexer.ami_errors[order].message.should eql(message)
179
+ end
180
+
181
+ Then '$number message should be an immediate response with text "$text"' do |number, text|
182
+ matching_immediate_responses = @lexer.received_messages.select do |response|
183
+ response.kind_of?(RubyAMI::Response) && response.text_body == text
184
+ end
185
+ matching_immediate_responses.size.should equal(number.to_i)
186
+ matching_immediate_responses.first["ActionID"].should eql(nil)
187
+ end
188
+
189
+ Then 'the $order event should have the name "$name"' do |order, name|
190
+ order = order[/^(\d+)\w+$/, 1].to_i - 1
191
+ @lexer.received_messages.select do |response|
192
+ response.kind_of?(RubyAMI::Event)
193
+ end[order].name.should eql(name)
194
+ end
195
+
196
+ Then '$number event should have been received' do |number|
197
+ @lexer.received_messages.select do |response|
198
+ response.kind_of?(RubyAMI::Event)
199
+ end.size.should equal(number.to_i)
200
+ end
201
+
202
+ Then 'the $order event should have key "$key" with value "$value"' do |order, key, value|
203
+ order = order[/^(\d+)\w+$/, 1].to_i - 1
204
+ @lexer.received_messages.select do |response|
205
+ response.kind_of?(RubyAMI::Event)
206
+ end[order][key].should eql(value)
207
+ end
@@ -0,0 +1,30 @@
1
+ :login:
2
+ :standard:
3
+ :client:
4
+ Action: Login
5
+ Username: :string
6
+ Secret: :string
7
+ Events: {one_of: ["on", "off"]}
8
+ :success:
9
+ Response: Success
10
+ Message: Authentication accepted
11
+ :fail:
12
+ Response: Error
13
+ Message: Authentication failed
14
+
15
+ :errors:
16
+ :missing_action:
17
+ Response: Error
18
+ Message: Missing action in request
19
+
20
+ :pong:
21
+ :with_action_id:
22
+ ActionID: 1287381.1238
23
+ Response: Pong
24
+ :without_action_id:
25
+ Response: Pong
26
+ :with_extra_keys:
27
+ ActionID: 1287381.1238
28
+ Response: Pong
29
+ Blah: This is something arbitrary
30
+ Blahhh: something else arbitrary
@@ -0,0 +1,16 @@
1
+ require 'simplecov'
2
+ require 'simplecov-rcov'
3
+ class SimpleCov::Formatter::MergedFormatter
4
+ def format(result)
5
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
6
+ SimpleCov::Formatter::RcovFormatter.new.format(result)
7
+ end
8
+ end
9
+ SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
10
+ SimpleCov.start do
11
+ add_filter "/vendor/"
12
+ end
13
+
14
+ require 'cucumber'
15
+ require 'rspec'
16
+ require 'ruby_ami'
@@ -0,0 +1,22 @@
1
+ class IntrospectiveManagerStreamLexer < RubyAMI::Lexer
2
+ attr_reader :received_messages, :syntax_errors, :ami_errors
3
+
4
+ def initialize(*args)
5
+ super
6
+ @received_messages = []
7
+ @syntax_errors = []
8
+ @ami_errors = []
9
+ end
10
+
11
+ def message_received(message = @current_message)
12
+ @received_messages << message
13
+ end
14
+
15
+ def error_received(error_message)
16
+ @ami_errors << error_message
17
+ end
18
+
19
+ def syntax_error_encountered(ignored_chunk)
20
+ @syntax_errors << ignored_chunk
21
+ end
22
+ end
@@ -0,0 +1,103 @@
1
+ RAGEL_FILES = %w[lib/ruby_ami/lexer.rl.rb]
2
+
3
+ def regenerate_ragel
4
+ `rake ragel`
5
+ end
6
+
7
+ FIXTURES = YAML.load_file File.dirname(__FILE__) + "/ami_fixtures.yml"
8
+
9
+ def fixture(path, overrides = {})
10
+ path_segments = path.split '/'
11
+ selected_event = path_segments.inject(FIXTURES.clone) do |hash, segment|
12
+ raise ArgumentError, path + " not found!" unless hash
13
+ hash[segment.to_sym]
14
+ end
15
+
16
+ # Downcase all keys in the event and the overrides
17
+ selected_event = selected_event.inject({}) do |downcased_hash,(key,value)|
18
+ downcased_hash[key.to_s.downcase] = value
19
+ downcased_hash
20
+ end
21
+
22
+ overrides = overrides.inject({}) do |downcased_hash,(key,value)|
23
+ downcased_hash[key.to_s.downcase] = value
24
+ downcased_hash
25
+ end
26
+
27
+ # Replace variables in the selected_event with any overrides, ignoring case of the key
28
+ keys_with_variables = selected_event.select { |(key, value)| value.kind_of?(Symbol) || value.kind_of?(Hash) }
29
+
30
+ keys_with_variables.each do |original_key, variable_type|
31
+ # Does an override an exist in the supplied list?
32
+ if overriden_pair = overrides.find { |(key, value)| key == original_key }
33
+ # We have an override! Let's replace the template value in the event with the overriden value
34
+ selected_event[original_key] = overriden_pair.last
35
+ else
36
+ # Based on the type, let's generate a placeholder.
37
+ selected_event[original_key] = case variable_type
38
+ when :string
39
+ rand(100000).to_s
40
+ when Hash
41
+ if variable_type.has_key? "one_of"
42
+ # Choose a random possibility
43
+ possibilities = variable_type['one_of']
44
+ possibilities[rand(possibilities.size)]
45
+ else
46
+ raise "Unrecognized Hash fixture property! ##{variable_type.keys.to_sentence}"
47
+ end
48
+ else
49
+ raise "Unrecognized fixture variable type #{variable_type}!"
50
+ end
51
+ end
52
+ end
53
+
54
+ hash_to_stanza(selected_event).tap do |event|
55
+ selected_event.each_pair do |key, value|
56
+ event.meta_def(key) { value }
57
+ end
58
+ end
59
+ end
60
+
61
+ def hash_to_stanza(hash)
62
+ ordered_hash = hash.to_a
63
+ starter = hash.find { |(key, value)| key.strip =~ /^(Response|Action)$/i }
64
+ ordered_hash.unshift ordered_hash.delete(starter) if starter
65
+ ordered_hash.inject(String.new) do |stanza,(key, value)|
66
+ stanza + "#{key}: #{value}\r\n"
67
+ end + "\r\n"
68
+ end
69
+
70
+ def format_newlines(string)
71
+ # HOLY FUCK THIS IS UGLY
72
+ tmp_replacement = random_string
73
+ string.gsub("\r\n", tmp_replacement).
74
+ gsub("\n", "\r\n").
75
+ gsub(tmp_replacement, "\r\n")
76
+ end
77
+
78
+ def random_string
79
+ (rand(1_000_000_000_000) + 1_000_000_000).to_s
80
+ end
81
+
82
+ def follows_body_text(name)
83
+ case name
84
+ when "ragel_description"
85
+ "Ragel is a software development tool that allows user actions to
86
+ be embedded into the transitions of a regular expression's corresponding state machine,
87
+ eliminating the need to switch from the regular expression engine and user code execution
88
+ environment and back again."
89
+ when "with_colon_after_first_line"
90
+ "Host Username Refresh State Reg.Time \r\nlax.teliax.net:5060 jicksta 105 Registered Tue, 11 Nov 2008 02:29:55"
91
+ when "show_channels_from_wayne"
92
+ "Channel Location State Application(Data)\r\n0 active channels\r\n0 active calls"
93
+ when "empty_string"
94
+ ""
95
+ end
96
+ end
97
+
98
+ def syntax_error_data(name)
99
+ case name
100
+ when "immediate_packet_with_colon"
101
+ "!IJ@MHY:!&@B*!B @ ! @^! @ !@ !\r!@ ! @ !@ ! !!m, \n\\n\n"
102
+ end
103
+ end
data/lib/ruby_ami.rb ADDED
@@ -0,0 +1,29 @@
1
+ %w{
2
+ active_support/dependencies/autoload
3
+ active_support/core_ext/object/blank
4
+ active_support/core_ext/numeric/time
5
+ active_support/core_ext/numeric/bytes
6
+ active_support/hash_with_indifferent_access
7
+
8
+ uuidtools
9
+ eventmachine
10
+ future-resource
11
+ logger
12
+ girl_friday
13
+ countdownlatch
14
+
15
+ ruby_ami/metaprogramming
16
+ }.each { |f| require f }
17
+
18
+ module RubyAMI
19
+ extend ActiveSupport::Autoload
20
+
21
+ autoload :Action
22
+ autoload :Client
23
+ autoload :Error
24
+ autoload :Event
25
+ autoload :Lexer
26
+ autoload :Response
27
+ autoload :Stream
28
+ autoload :Version
29
+ end
@@ -0,0 +1,6 @@
1
+ ENV['SKIP_RCOV'] = 'true'
2
+ guard 'rspec', :version => 2, :cli => '--format documentation' do
3
+ watch(%r{^spec/.+_spec\.rb$})
4
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
5
+ watch('spec/spec_helper.rb') { "spec/" }
6
+ end
@@ -0,0 +1,143 @@
1
+ module RubyAMI
2
+ class Action
3
+ attr_reader :name, :headers, :action_id
4
+
5
+ attr_accessor :state
6
+
7
+ CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls dahdishowchannels coreshowchannels
8
+ dbget status agents konferencelist] unless defined? CAUSAL_EVENT_NAMES
9
+
10
+ def initialize(name, headers = {}, &block)
11
+ @name = name.to_s.downcase.freeze
12
+ @headers = headers.stringify_keys.freeze
13
+ @action_id = UUIDTools::UUID.random_create.to_s
14
+ @response = FutureResource.new
15
+ @response_callback = block
16
+ @state = :new
17
+ @events = []
18
+ @event_lock = Mutex.new
19
+ end
20
+
21
+ [:new, :sent, :complete].each do |state|
22
+ define_method("#{state}?") { @state == state }
23
+ end
24
+
25
+ def replies_with_action_id?
26
+ !UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
27
+ end
28
+
29
+ ##
30
+ # When sending an action with "causal events" (i.e. events which must be collected to form a proper
31
+ # response), AMI should send a particular event which instructs us that no more events will be sent.
32
+ # This event is called the "causal event terminator".
33
+ #
34
+ # Note: you must supply both the name of the event and any headers because it's possible that some uses of an
35
+ # action (i.e. same name, different headers) have causal events while other uses don't.
36
+ #
37
+ # @param [String] name the name of the event
38
+ # @param [Hash] the headers associated with this event
39
+ # @return [String] the downcase()'d name of the event name for which to wait
40
+ #
41
+ def has_causal_events?
42
+ CAUSAL_EVENT_NAMES.include? name
43
+ end
44
+
45
+ ##
46
+ # Used to determine the event name for an action which has causal events.
47
+ #
48
+ # @param [String] action_name
49
+ # @return [String] The corresponding event name which signals the completion of the causal event sequence.
50
+ #
51
+ def causal_event_terminator_name
52
+ return unless has_causal_events?
53
+ case name
54
+ when "sippeers", "iaxpeers"
55
+ "peerlistcomplete"
56
+ when "dbget"
57
+ "dbgetresponse"
58
+ when "konferencelist"
59
+ "conferencelistcomplete"
60
+ else
61
+ name + "complete"
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Converts this action into a protocol-valid String, ready to be sent over a socket.
67
+ #
68
+ def to_s
69
+ @textual_representation ||= (
70
+ "Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
71
+ @headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
72
+ (@headers.any? ? "\r\n\r\n" : "\r\n")
73
+ )
74
+ end
75
+
76
+ #
77
+ # If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
78
+ # in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
79
+ # object.
80
+ #
81
+ def response(timeout = nil)
82
+ @response.resource(timeout).tap do |resp|
83
+ raise resp if resp.is_a? Exception
84
+ end
85
+ end
86
+
87
+ def response=(other)
88
+ @state = :complete
89
+ @response.resource = other
90
+ @response_callback.call other if @response_callback
91
+ end
92
+
93
+ def <<(message)
94
+ case message
95
+ when Error
96
+ self.response = message
97
+ when Event
98
+ raise StandardError, 'This action should not trigger events. Maybe it is now a causal action? This is most likely a bug in RubyAMI' unless has_causal_events?
99
+ @event_lock.synchronize do
100
+ @events << message
101
+ end
102
+ self.response = @pending_response if message.name.downcase == causal_event_terminator_name
103
+ when Response
104
+ if has_causal_events?
105
+ @pending_response = message
106
+ else
107
+ self.response = message
108
+ end
109
+ end
110
+ end
111
+
112
+ def events
113
+ @event_lock.synchronize do
114
+ @events.dup
115
+ end
116
+ end
117
+
118
+ def eql?(other)
119
+ to_s == other.to_s
120
+ end
121
+ alias :== :eql?
122
+
123
+ ##
124
+ # This class will be removed once this AMI library fully supports all known protocol anomalies.
125
+ #
126
+ class UnsupportedActionName < ArgumentError
127
+ UNSUPPORTED_ACTION_NAMES = %w[queues] unless defined? UNSUPPORTED_ACTION_NAMES
128
+
129
+ # Blacklist some actions depends on the Asterisk version
130
+ def self.preinitialize(version)
131
+ if version < 1.8
132
+ %w[iaxpeers muteaudio mixmonitormute aocmessage].each do |action|
133
+ UNSUPPORTED_ACTION_NAMES << action
134
+ end
135
+ end
136
+ end
137
+
138
+ def initialize(name)
139
+ super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly."
140
+ end
141
+ end
142
+ end
143
+ end # RubyAMI