ruby-ami-2.4.0/0000755000175000017500000000000012720060167013201 5ustar joostvbjoostvbruby-ami-2.4.0/.gitignore0000644000175000017500000000016412720060167015172 0ustar joostvbjoostvb*.gem .bundle Gemfile.lock pkg/* .rvmrc .yardoc doc coverage spec/reports features/reports vendor *.swp .rbx/* tmp ruby-ami-2.4.0/.rspec0000644000175000017500000000004612720060167014316 0ustar joostvbjoostvb--format documentation --colour --tty ruby-ami-2.4.0/.travis.yml0000644000175000017500000000033412720060167015312 0ustar joostvbjoostvblanguage: ruby sudo: false rvm: - 1.9.3 - 2.0.0 - 2.1.0 - jruby - rbx-2.1.1 - ruby-head matrix: allow_failures: - rvm: ruby-head - rvm: rbx-2.1.1 notifications: irc: "irc.freenode.org#adhearsion" ruby-ami-2.4.0/CHANGELOG.md0000644000175000017500000001604012720060167015013 0ustar joostvbjoostvb# [develop](https://github.com/adhearsion/ruby_ami) # [2.4.0](https://github.com/adhearsion/ruby_ami/compare/v2.3.0...v2.4.0) - [2015-12-07](https://rubygems.org/gems/ruby_ami/versions/2.4.0) # Feature: Reveal the AMI version for a `Stream` via `Stream#version` # [2.3.0](https://github.com/adhearsion/ruby_ami/compare/v2.2.1...v2.3.0) - [2015-06-01](https://rubygems.org/gems/ruby_ami/versions/2.3.0) * Feature: Allow optional error handler when calling `send_action` * Bugfix: Catch for Errno::HOSTUNREACH error when connecting to AMI * Bugfix: Add support for Goodbye responses instead of processing them as syntax errors * Bugfix: Allow simple example of event handling (switching on `event.name` only) to work by giving connection status events a name to match their class names # [2.2.1](https://github.com/adhearsion/ruby_ami/compare/v2.2.0...v2.2.1) - [2014-05-22](https://rubygems.org/gems/ruby_ami/versions/2.2.1) * Bugfix: Ensure DateTime is present when consumer code doesn't `require 'time'` ([#24](https://github.com/adhearsion/ruby_ami/issues/24)) # [2.2.0](https://github.com/adhearsion/ruby_ami/compare/v2.1.0...v2.2.0) - [2014-02-28](https://rubygems.org/gems/ruby_ami/versions/2.2.0) * Feature: Provide timestamps of events as `#timestamp`, `#receipt_time` and `#best_time`. * Feature: More performant parsing of AGI environment strings # [2.1.0](https://github.com/adhearsion/ruby_ami/compare/v2.0.0...v2.1.0) - [2013-05-29](https://rubygems.org/gems/ruby_ami/versions/2.1.0) * Enhancement: Replace Ragel parser with pure Ruby version, which is much more performant and simpler * Bugfix: Handle AGI 5xx responses # [2.0.0](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v2.0.0) - [2013-04-15](https://rubygems.org/gems/ruby_ami/versions/2.0.0) * Major refactoring for simplification and performance * Actions are no longer synchronised on the wire since ActionID is now a reliable method of response/event association * Callbacks are no longer required. #send_action now simply blocks waiting for a response * Client still starts up two Streams, one for actions and one for events, but only for possible performance gains. It is possible to use Stream directly since it now does its own login and response association. Client is a very thin routing layer. It's encouraged that if you expect low traffic, you should use Stream directly. Client may be removed in v3.0. # [1.3.4](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v1.3.4) - [2013-04-25](https://rubygems.org/gems/ruby_ami/versions/1.3.4) * Bugfix: Handle AGI 5xx responses # [1.3.3](https://github.com/adhearsion/ruby_ami/compare/v1.3.2...v1.3.3) - [2013-04-09](https://rubygems.org/gems/ruby_ami/versions/1.3.3) * Bugfix: DBGet actions are now not terminated specially # [1.3.2](https://github.com/adhearsion/ruby_ami/compare/v1.3.1...v1.3.2) - [2013-03-22](https://rubygems.org/gems/ruby_ami/versions/1.3.2) * CS: Avoid celluloid deprecation warnings # [1.3.1](https://github.com/adhearsion/ruby_ami/compare/v1.3.0...v1.3.1) - [2013-03-20](https://rubygems.org/gems/ruby_ami/versions/1.3.1) * Bugfix: Add support for causal event types `confbridgelist` and `confbridgelistrooms` * Bugfix: Loosen celluloid dependency # [1.3.0](https://github.com/adhearsion/ruby_ami/compare/v1.2.6...v1.3.0) - [2013-01-23](https://rubygems.org/gems/ruby_ami/versions/1.3.0) * Feature: Added timeout feature to client connection process. Currently does not work on Rubinius due to https://github.com/rubinius/rubinius/issues/2127 # [1.2.6](https://github.com/adhearsion/ruby_ami/compare/v1.2.5...v1.2.6) - [2012-12-26](https://rubygems.org/gems/ruby_ami/versions/1.2.6) * Bugfix: JRuby and rbx compatability # [1.2.5](https://github.com/adhearsion/ruby_ami/compare/v1.2.4...v1.2.5) - [2012-10-24](https://rubygems.org/gems/ruby_ami/versions/1.2.5) * Bugfix: Log wire stuff at trace level # [1.2.4](https://github.com/adhearsion/ruby_ami/compare/v1.2.3...v1.2.4) - [2012-10-13](https://rubygems.org/gems/ruby_ami/versions/1.2.4) * Bugfix: No longer suffer "invalid byte sequence" exceptions due to encoding mismatch. Thanks tboyko # [1.2.3](https://github.com/adhearsion/ruby_ami/compare/v1.2.2...v1.2.3) - [2012-09-20](https://rubygems.org/gems/ruby_ami/versions/1.2.3) * Streams now inherit the client's logger # [1.2.2](https://github.com/adhearsion/ruby_ami/compare/v1.2.1...v1.2.2) - [2012-09-05](https://rubygems.org/gems/ruby_ami/versions/1.2.2) * Streams now log syntax errors * Celluloid dependency updated # [1.2.1](https://github.com/adhearsion/ruby_ami/compare/v1.2.0...v1.2.1) - [2012-07-19](https://rubygems.org/gems/ruby_ami/versions/1.2.1) * Use SecureRandom for UUIDs # [1.2.0](https://github.com/adhearsion/ruby_ami/compare/v1.1.2...v1.2.0) - [2012-07-18](https://rubygems.org/gems/ruby_ami/versions/1.2.0) * Feature: Added parsers for (Async)AGI environment and result strings * Bugfix: Avoid a race condition in stream establishment and event receipt * Bugfix: If socket creation fails, log an appropriate error # [1.1.2](https://github.com/adhearsion/ruby_ami/compare/v1.1.1...v1.1.2) - [2012-07-04](https://rubygems.org/gems/ruby_ami/versions/1.1.2) * Bugfix: Avoid recursive stream stopping # [1.1.1](https://github.com/adhearsion/ruby_ami/compare/v1.1.0...v1.1.1) - [2012-06-25](https://rubygems.org/gems/ruby_ami/versions/1.1.1) * v1.1.0 re-released with fixed celluloid-io dependency # [1.1.0](https://github.com/adhearsion/ruby_ami/compare/v1.0.1...v1.1.0) - [2012-06-16](https://rubygems.org/gems/ruby_ami/versions/1.1.0) * Change: Switch from EventMachine to Celluloid & CelluloidIO for better JRuby compatability and performance (action and events connections are now in separate threads) # 1.0.1 - 2012-04-25 * Bugfix: Actions which do not receive a response within 10s will allow further actions to be executed. Synchronous originate has a 60s timeout. # 1.0.0 - 2012-03-09 * Bugfix: Remove rcov * Bump to 1.0.0 since we're in active use # 0.1.5 - 2011-12-22 * Bugfix: Work consistently all all versions of Asterisk * Both 1.8 and 10 * Login actions connection with events turned on (in order to get FullyBooted event) * Turn events off immediately after fully-booted * Pass FullyBooted events from the actions connection up to the event handler # 0.1.4 - 2011-12-1 * Bugfix: Actions connection should login with Events: System. This ensures that the FullyBooted event will come through on both connections. # 0.1.3 - 2011-11-22 * Bugfix: A client can now safely be shut down before it is started, and only performs actions on live streams. * Bugfix: RubyAMI::Error#inspect now shows an error's message and headers * Bugfix: Spec and JRuby fixes # 0.1.2 * Bugfix: Prevent stream connection status events being passed up to the consumer event handler * Bugfix: Corrected the README usage docs * Bugfix: Alias Logger#trace to Logger#debug if the consumer is using a simple logger without a trace level # 0.1.1 * Bugfix: Make countdownlatch and i18n runtime dependencies * Bugfig: Include the generated lexer file in the gem # 0.1.0 * Initial release ruby-ami-2.4.0/Gemfile0000644000175000017500000000004612720060167014474 0ustar joostvbjoostvbsource "http://rubygems.org" gemspec ruby-ami-2.4.0/Guardfile0000644000175000017500000000125012720060167015024 0ustar joostvbjoostvbguard 'rspec', cmd: 'bundle exec rspec --format documentation' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec/" } end guard 'cucumber', cli: '--profile default --color --format progress' do watch("lib/ruby_ami/lexer.rb") { 'features' } watch(%r{^features/.+\.feature$}) watch(%r{^features/support/.+$}) { 'features' } watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' } end guard 'rake', task: 'benchmark' do watch("lib/ruby_ami/lexer.rb") watch(/benchmarks\/*/) end ruby-ami-2.4.0/LICENSE.txt0000644000175000017500000000205612720060167015027 0ustar joostvbjoostvbCopyright (c) 2011 Ben Langfeld, Jay Phillips Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-ami-2.4.0/README.md0000644000175000017500000001103012720060167014453 0ustar joostvbjoostvb# RubyAMI [![Gem Version](https://badge.fury.io/rb/ruby_ami.png)](https://rubygems.org/gems/ruby_ami) [![Build Status](https://secure.travis-ci.org/adhearsion/ruby_ami.png?branch=develop)](http://travis-ci.org/adhearsion/ruby_ami) [![Dependency Status](https://gemnasium.com/adhearsion/ruby_ami.png?travis)](https://gemnasium.com/adhearsion/ruby_ami) [![Code Climate](https://codeclimate.com/github/adhearsion/ruby_ami.png)](https://codeclimate.com/github/adhearsion/ruby_ami) [![Coverage Status](https://coveralls.io/repos/adhearsion/ruby_ami/badge.png?branch=develop)](https://coveralls.io/r/adhearsion/ruby_ami) [![Inline docs](http://inch-ci.org/github/adhearsion/ruby_ami.png?branch=develop)](http://inch-ci.org/github/adhearsion/ruby_ami) RubyAMI is an AMI client library in Ruby and based on EventMachine with the sole purpose of providing a connection to the Asterisk Manager Interface. RubyAMI does not provide any features beyond connection management and protocol parsing. Actions are sent over the wire, and responses are returned. Events are passed to a callback you define. It's up to you to match these up into something useful. In this regard, RubyAMI is very similar to [Blather](https://github.com/sprsquish/blather) for XMPP or [Punchblock](https://github.com/adhearsion/punchblock), the Ruby 3PCC library. In fact, Punchblock uses RubyAMI under the covers for its Asterisk implementation, including an implementation of AsyncAGI. NB: If you're looking to develop an application on Asterisk, you should take a look at the [Adhearsion](http://adhearsion.com) framework first. This library is much lower level. ## Installation gem install ruby_ami ## Usage In order to setup a connection to listen for AMI events, one can do: ```ruby require 'ruby_ami' stream = RubyAMI::Stream.new '127.0.0.1', 5038, 'manager', 'password', ->(e) { handle_event e }, Logger.new(STDOUT), 10 def handle_event(event) case event.name when 'FullyBooted' puts "The server booted and is available for commands." else puts "Received an event from Asterisk: #{event.inspect}" end end $stream.run # This will block until the actor is terminated elsewhere. $stream.async.run is also available if you need to do other things in the main thread. ``` It is also possible to execute actions in response to events: ```ruby require 'ruby_ami' $stream = RubyAMI::Stream.new '127.0.0.1', 5038, 'manager', 'password', ->(e) { handle_event e }, Logger.new(STDOUT), 10 def handle_event(event) case event.name when 'FullyBooted' puts "The connection was successful. Originating a call." response = $stream.send_action 'Originate', 'Channel' => 'SIP/foo' puts "The call origination resulted in #{response.inspect}" end end $stream.run ``` Executing actions does not strictly have to be done within the event handler, but it is not valid to send AMI events before receiving a `FullyBooted` event. If you attempt to execute an action prior to this, it may fail, and `RubyAMI::Stream` will not help you recover or queue the action until the connection is `FullyBooted`; you must manage this timing yourself. That said, assuming you take care of this, you may invoke `RubyAMI::Stream#send_action` from anywhere in your code and it will return the response of the action. RubyAMI also has a class called `RubyAMI::Client` which used to be the main usage method. The purpose of this class was to tie together two AMI connections and separate events and action execution between the two in order to avoid some issues present in Asterisk < 1.8 with regards to separating overlapping events and executing multiple actions simultaneously. These issues are no longer present, and so **`RubyAMI::Client` is now deprecated and will be removed in RubyAMI 3.0**. ## Links: * [Source](https://github.com/adhearsion/ruby_ami) * [Documentation](http://rdoc.info/github/adhearsion/ruby_ami/master/frames) * [Bug Tracker](https://github.com/adhearsion/ruby_ami/issues) ## Note on Patches/Pull Requests * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with rakefile, version, or history. * If you want to have your own version, that is fine but bump version in a commit by itself so I can ignore when I pull * Send me a pull request. Bonus points for topic branches. ## Copyright Copyright (c) 2013 Ben Langfeld, Jay Phillips. MIT licence (see LICENSE for details). ruby-ami-2.4.0/Rakefile0000644000175000017500000000167712720060167014661 0ustar joostvbjoostvbrequire 'bundler/gem_tasks' require 'rspec/core' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.rspec_opts = '--color' end require 'cucumber' require 'cucumber/rake/task' Cucumber::Rake::Task.new(:features) do |t| t.cucumber_opts = %w{--tags ~@jruby} unless defined?(JRUBY_VERSION) end Cucumber::Rake::Task.new(:wip) do |t| t.cucumber_opts = %w{-p wip} end task :default => [:spec, :features] require 'timeout' desc "Run benchmarks" task :benchmark do begin Timeout.timeout(120) do glob = File.expand_path("../benchmarks/*.rb", __FILE__) Dir[glob].each { |benchmark| load benchmark } end rescue Exception, Timeout::Error => ex puts "ERROR: Couldn't complete benchmark: #{ex.class}: #{ex}" puts " #{ex.backtrace.join("\n ")}" exit 1 unless ENV['CI'] # Hax for running benchmarks on Travis end end require 'yard' YARD::Rake::YardocTask.new ruby-ami-2.4.0/benchmarks/0000755000175000017500000000000012720060167015316 5ustar joostvbjoostvbruby-ami-2.4.0/benchmarks/agi_env_parse.rb0000644000175000017500000000151112720060167020443 0ustar joostvbjoostvb#!/usr/bin/env ruby require 'rubygems' require 'bundler/setup' require 'ruby_ami' require 'benchmark/ips' env_string = "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A" Benchmark.ips do |ips| ips.report("environment parsing") { RubyAMI::AsyncAGIEnvironmentParser.new(env_string).to_hash } ips.report("CGI unescaping") { CGI.unescape(env_string) } end ruby-ami-2.4.0/benchmarks/lexer.rb0000755000175000017500000000137012720060167016766 0ustar joostvbjoostvb#!/usr/bin/env ruby require 'rubygems' require 'bundler/setup' require 'ruby_ami' require 'benchmark/ips' class LexerHost def initialize @lexer = RubyAMI::Lexer.new self end def receive_data(data) @lexer << data end def message_received(message) end def error_received(error) end def syntax_error_encountered(ignored_chunk) end end lexer_host = LexerHost.new event = <<-EVENT Event: Dial SubEvent: Channel: Destination: CallerIDNum: CallerIDName: ConnectedLineNum: ConnectedLineName: UniqueID: DestUniqueID: Dialstring: EVENT event.gsub!("\n", "\r\n") Benchmark.ips do |ips| ips.report("event lexing") { lexer_host.receive_data event } end ruby-ami-2.4.0/cucumber.yml0000644000175000017500000000005512720060167015531 0ustar joostvbjoostvbdefault: --tags ~@wip wip: --wip --tags @wip ruby-ami-2.4.0/features/0000755000175000017500000000000012720060167015017 5ustar joostvbjoostvbruby-ami-2.4.0/features/lexer.feature0000644000175000017500000002357012720060167017522 0ustar joostvbjoostvbFeature: Lexing AMI As a RubyAMI user I want to lex the AMI protocol So that I can control Asterisk asynchronously Scenario: Lexing only the initial AMI version header Given a new lexer And a version header for AMI 2.8.0 Then the protocol should have lexed without syntax errors And the version should be set to 2.8.0 Scenario: Lexing the initial AMI header and a login attempt Given a new lexer And a version header for AMI 1.0 And a normal login success with events Then the protocol should have lexed without syntax errors And 1 message should have been received Scenario: Lexing the initial AMI header and then a Response:Follows section Given a new lexer And a version header for AMI 1.0 And a multi-line Response:Follows body of show_channels_from_wayne Then the protocol should have lexed without syntax errors And the 'follows' body of 1 message received should equal show_channels_from_wayne Scenario: Lexing a Response:Follows section with no body Given a new lexer And a version header for AMI 1.0 And a multi-line Response:Follows body of empty_String Then the protocol should have lexed without syntax errors And the 'follows' body of 1 message received should equal empty_string Scenario: Lexing a multi-line Response:Follows simulating the "core show channels" command Given a new lexer And a version header for AMI 1.0 Given a multi-line Response:Follows body of show_channels_from_wayne Then the protocol should have lexed without syntax errors And the 'follows' body of 1 message received should equal show_channels_from_wayne Scenario: Lexing a multi-line Response:Follows simulating the "core show uptime" command Given a new lexer And a version header for AMI 1.0 Given a multi-line Response:Follows response simulating uptime Then the protocol should have lexed without syntax errors And the first message received should have a key "System uptime" with value "46 minutes, 30 seconds" Scenario: Lexing a Response:Follows section which has a colon not on the first line Given a new lexer And a multi-line Response:Follows body of with_colon_after_first_line Then the protocol should have lexed without syntax errors And 1 message should have been received And the 'follows' body of 1 message received should equal with_colon_after_first_line @wip Scenario: Lexing an immediate response with a colon in it. Given a new lexer And an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers\r\n\r\n\r\n\r\n" Then the protocol should have lexed without syntax errors And 1 message should have been received And 1 message should be an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers" Scenario: Lexing the initial AMI header and then an "Authentication Required" error. Given a new lexer And a version header for AMI 1.0 And an Authentication Required error Then the protocol should have lexed without syntax errors Scenario: Lexing the initial AMI header and then a Response:Follows section Given a new lexer And a version header for AMI 1.0 And a multi-line Response:Follows body of show_channels_from_wayne And a multi-line Response:Follows body of show_channels_from_wayne Then the protocol should have lexed without syntax errors And the 'follows' body of 2 messages received should equal show_channels_from_wayne Scenario: Lexing a stanza without receiving an AMI header Given a new lexer And a normal login success with events Then the protocol should have lexed without syntax errors And 1 message should have been received Scenario: Receiving an immediate response as soon as the socket is opened Given a new lexer And an immediate response with text "Immediate responses are so ridiculous" Then the protocol should have lexed without syntax errors And 1 message should have been received And 1 message should be an immediate response with text "Immediate responses are so ridiculous" Scenario: Receiving an immediate message surrounded by real messages Given a new lexer And a normal login success with events And an immediate response with text "No queues have been created." And a normal login success with events Then the protocol should have lexed without syntax errors And 3 messages should have been received And 1 message should be an immediate response with text "No queues have been created." Scenario: Receiving a Pong after a simulated login Given a new lexer And a version header for AMI 1.0 And a normal login success with events And a Pong response with an ActionID of randomness Then the protocol should have lexed without syntax errors And 2 messages should have been received Scenario: Ten Pong responses in a row Given a new lexer And 5 Pong responses without an ActionID And 5 Pong responses with an ActionID of randomness Then the protocol should have lexed without syntax errors And 10 messages should have been received Scenario: A Pong with an ActionID Given a new lexer And a Pong response with an ActionID of 1224469850.61673 Then the first message received should have a key "ActionID" with value "1224469850.61673" Scenario: A response containing a floating point value Given a new lexer And a custom stanza named "call" And the custom stanza named "call" has key "ActionID" with value "1224469850.61673" And the custom stanza named "call" has key "Uniqueid" with value "1173223225.10309" When the custom stanza named "call" is added to the buffer Then the 1st message received should have a key "Uniqueid" with value "1173223225.10309" Scenario: Receiving a message with custom key/value pairs Given a new lexer And a custom stanza named "person" And the custom stanza named "person" has key "ActionID" with value "1224469850.61673" And the custom stanza named "person" has key "Name" with value "Jay Phillips" And the custom stanza named "person" has key "Age" with value "21" And the custom stanza named "person" has key "Location" with value "San Francisco, CA" And the custom stanza named "person" has key "x-header" with value "" And the custom stanza named "person" has key "Channel" with value "IAX2/127.0.0.1/4569-9904" And the custom stanza named "person" has key "I have spaces" with value "i have trailing padding " When the custom stanza named "person" is added to the buffer Then the protocol should have lexed without syntax errors And the first message received should have a key "Name" with value "Jay Phillips" And the first message received should have a key "ActionID" with value "1224469850.61673" And the first message received should have a key "Name" with value "Jay Phillips" And the first message received should have a key "Age" with value "21" And the first message received should have a key "Location" with value "San Francisco, CA" And the first message received should have a key "x-header" with value "" And the first message received should have a key "Channel" with value "IAX2/127.0.0.1/4569-9904" And the first message received should have a key "I have spaces" with value "i have trailing padding " Scenario: Executing a stanza that was partially received Given a new lexer And a normal login success with events split into two pieces Then the protocol should have lexed without syntax errors And 1 message should have been received Scenario: Receiving an AMI error followed by a normal event Given a new lexer And an AMI error whose message is "Missing action in request" And a normal login success with events Then the protocol should have lexed without syntax errors And 1 AMI error should have been received And the 1st AMI error should have the message "Missing action in request" And 1 message should have been received Scenario: Lexing an immediate response Given a new lexer And a normal login success with events And an immediate response with text "Yes, plain English is sent sometimes over AMI." And a normal login success with events Then the protocol should have lexed without syntax errors And 3 messages should have been received And 1 message should be an immediate response with text "Yes, plain English is sent sometimes over AMI." Scenario: Lexing an AMI event Given a new lexer And a custom event with name "NewChannelEvent" identified by "this_event" And a custom header for event identified by "this_event" whose key is "Foo" and value is "Bar" And a custom header for event identified by "this_event" whose key is "Channel" and value is "IAX2/127.0.0.1:4569-9904" And a custom header for event identified by "this_event" whose key is "AppData" and value is "agi://localhost" When the custom event identified by "this_event" is added to the buffer Then the protocol should have lexed without syntax errors And 1 event should have been received And the 1st event should have the name "NewChannelEvent" And the 1st event should have key "Foo" with value "Bar" And the 1st event should have key "Channel" with value "IAX2/127.0.0.1:4569-9904" And the 1st event should have key "AppData" with value "agi://localhost" Scenario: Lexing an immediate packet with a colon in it (syntax error) Given a new lexer And syntactically invalid immediate_packet_with_colon And a stanza break Then 0 messages should have been received And the protocol should have lexed with 1 syntax error And the syntax error fixture named immediate_packet_with_colon should have been encountered ruby-ami-2.4.0/features/step_definitions/0000755000175000017500000000000012720060167020365 5ustar joostvbjoostvbruby-ami-2.4.0/features/step_definitions/lexer_steps.rb0000644000175000017500000001512212720060167023250 0ustar joostvbjoostvbGiven "a new lexer" do @lexer = IntrospectiveManagerStreamLexer.new @custom_stanzas = {} @custom_events = {} @GivenPong = lambda do |with_or_without, action_id, number| number = number == "a" ? 1 : number.to_i data = case with_or_without when "with" then "Response: Pong\r\nActionID: #{action_id}\r\n\r\n" when "without" then "Response: Pong\r\n\r\n" else raise "Do not recognize preposition #{with_or_without.inspect}. Should be either 'with' or 'without'" end number.times do @lexer << data end end end Given "a version header for AMI $version" do |version| @lexer << "Asterisk Call Manager/#{version}\r\n" end Given "a normal login success with events" do @lexer << fixture('login/standard/success') end Given "a normal login success with events split into two pieces" do stanza = fixture('login/standard/success') @lexer << stanza[0...3] @lexer << stanza[3..-1] end Given "a stanza break" do @lexer << "\r\n\r\n" end Given "a multi-line Response:Follows body of $method_name" do |method_name| multi_line_response_body = send(:follows_body_text, method_name) multi_line_response = format_newlines(<<-RESPONSE + "\r\n") % multi_line_response_body Response: Follows\r Privilege: Command\r ActionID: 123123\r %s\r --END COMMAND--\r\n\r RESPONSE @lexer << multi_line_response end Given "a multi-line Response:Follows response simulating uptime" do uptime_response = "Response: Follows\r Privilege: Command\r System uptime: 46 minutes, 30 seconds\r --END COMMAND--\r\n\r\n" @lexer << uptime_response end Given "syntactically invalid $name" do |name| @lexer << send(:syntax_error_data, name) end Given /^(\d+) Pong responses with an ActionID of ([\d\w.]+)$/ do |number, action_id| @GivenPong.call "with", action_id, number end Given /^a Pong response with an ActionID of ([\d\w.]+)$/ do |action_id| @GivenPong.call "with", action_id, 1 end Given /^(\d+) Pong responses without an ActionID$/ do |number| @GivenPong.call "without", Time.now.to_f, number end Given /^a custom stanza named "(\w+)"$/ do |name| @custom_stanzas[name] = "Response: Success\r\n" end Given 'the custom stanza named "$name" has key "$key" with value "$value"' do |name,key,value| @custom_stanzas[name] << "#{key}: #{value}\r\n" end Given 'an AMI error whose message is "$message"' do |message| @lexer << "Response: Error\r\nMessage: #{message}\r\n\r\n" end Given 'an immediate response with text "$text"' do |text| @lexer << "#{text}\r\n\r\n" end Given 'a custom event with name "$event_name" identified by "$identifier"' do |event_name, identifer| @custom_events[identifer] = {:Event => event_name } end Given 'a custom header for event identified by "$identifier" whose key is "$key" and value is "$value"' do |identifier, key, value| @custom_events[identifier][key] = value end Given "an Authentication Required error" do @lexer << "Response: Error\r\nActionID: BPJeKqW2-SnVg-PyFs-vkXT-7AWVVPD0N3G7\r\nMessage: Authentication Required\r\n\r\n" end Given "a follows packet with a colon in it" do @lexer << follows_body_text("with_colon") end ######################################## #### WHEN ######################################## When 'the custom stanza named "$name" is added to the buffer' do |name| @lexer << (@custom_stanzas[name] + "\r\n") end When 'the custom event identified by "$identifier" is added to the buffer' do |identifier| custom_event = @custom_events[identifier].clone event_name = custom_event.delete :Event stringified_event = "Event: #{event_name}\r\n" custom_event.each_pair do |key,value| stringified_event << "#{key}: #{value}\r\n" end stringified_event << "\r\n" @lexer << stringified_event end ######################################## #### THEN ######################################## Then "the protocol should have lexed without syntax errors" do current_pointer = @lexer.send(:instance_variable_get, :@current_pointer) data_ending_pointer = @lexer.send(:instance_variable_get, :@data_ending_pointer) current_pointer.should == data_ending_pointer @lexer.syntax_errors.size.should equal(0) end Then /^the protocol should have lexed with (\d+) syntax errors?$/ do |number| @lexer.syntax_errors.size.should == number.to_i end Then "the syntax error fixture named $name should have been encountered" do |name| irregularity = send(:syntax_error_data, name) @lexer.syntax_errors.find { |error| error == irregularity }.should_not be_nil end Then /^(\d+) messages? should have been received$/ do |number_received| @lexer.received_messages.size.should == number_received.to_i end Then /^the 'follows' body of (\d+) messages? received should equal (\w+)$/ do |number, method_name| multi_line_response = follows_body_text method_name @lexer.received_messages.should_not be_empty @lexer.received_messages.select do |message| message.text_body == multi_line_response end.size.should == number.to_i end Then "the version should be set to $version" do |version| @lexer.ami_version.should eql(version) end Then /^the ([\w\d]*) message received should have a key "([^\"]*)" with value "([^\"]*)"$/ do |ordered,key,value| ordered = ordered[/^(\d+)\w+$/, 1].to_i - 1 @lexer.received_messages[ordered][key].should eql(value) end Then "$number AMI error should have been received" do |number| @lexer.ami_errors.size.should equal(number.to_i) end Then 'the $order AMI error should have the message "$message"' do |order, message| order = order[/^(\d+)\w+$/, 1].to_i - 1 @lexer.ami_errors[order].should be_kind_of(RubyAMI::Error) @lexer.ami_errors[order].message.should eql(message) end Then '$number message should be an immediate response with text "$text"' do |number, text| matching_immediate_responses = @lexer.received_messages.select do |response| response.kind_of?(RubyAMI::Response) && response.text_body == text end matching_immediate_responses.size.should equal(number.to_i) matching_immediate_responses.first["ActionID"].should eql(nil) end Then 'the $order event should have the name "$name"' do |order, name| order = order[/^(\d+)\w+$/, 1].to_i - 1 @lexer.received_messages.select do |response| response.kind_of?(RubyAMI::Event) end[order].name.should eql(name) end Then '$number event should have been received' do |number| @lexer.received_messages.select do |response| response.kind_of?(RubyAMI::Event) end.size.should equal(number.to_i) end Then 'the $order event should have key "$key" with value "$value"' do |order, key, value| order = order[/^(\d+)\w+$/, 1].to_i - 1 @lexer.received_messages.select do |response| response.kind_of?(RubyAMI::Event) end[order][key].should eql(value) end ruby-ami-2.4.0/features/support/0000755000175000017500000000000012720060167016533 5ustar joostvbjoostvbruby-ami-2.4.0/features/support/ami_fixtures.yml0000644000175000017500000000116312720060167021756 0ustar joostvbjoostvb:login: :standard: :client: Action: Login Username: :string Secret: :string Events: {one_of: ["on", "off"]} :success: Response: Success Message: Authentication accepted :fail: Response: Error Message: Authentication failed :errors: :missing_action: Response: Error Message: Missing action in request :pong: :with_action_id: ActionID: 1287381.1238 Response: Pong :without_action_id: Response: Pong :with_extra_keys: ActionID: 1287381.1238 Response: Pong Blah: This is something arbitrary Blahhh: something else arbitraryruby-ami-2.4.0/features/support/env.rb0000644000175000017500000000006612720060167017652 0ustar joostvbjoostvbrequire 'cucumber' require 'rspec' require 'ruby_ami' ruby-ami-2.4.0/features/support/introspective_lexer.rb0000644000175000017500000000076712720060167023167 0ustar joostvbjoostvbclass IntrospectiveManagerStreamLexer < RubyAMI::Lexer attr_reader :received_messages, :syntax_errors, :ami_errors def initialize(*args) super @received_messages = [] @syntax_errors = [] @ami_errors = [] end def message_received(message = @current_message) @received_messages << message end def error_received(error_message) @ami_errors << error_message end def syntax_error_encountered(ignored_chunk) @syntax_errors << ignored_chunk end end ruby-ami-2.4.0/features/support/lexer_helper.rb0000644000175000017500000000652612720060167021547 0ustar joostvbjoostvbFIXTURES = YAML.load_file File.dirname(__FILE__) + "/ami_fixtures.yml" class Object def metaclass class << self self end end def meta_eval(&block) metaclass.instance_eval &block end def meta_def(name, &block) meta_eval do define_method name, &block end end end def fixture(path, overrides = {}) path_segments = path.split '/' selected_event = path_segments.inject(FIXTURES.clone) do |hash, segment| raise ArgumentError, path + " not found!" unless hash hash[segment.to_sym] end # Downcase all keys in the event and the overrides selected_event = selected_event.inject({}) do |downcased_hash,(key,value)| downcased_hash[key.to_s.downcase] = value downcased_hash end overrides = overrides.inject({}) do |downcased_hash,(key,value)| downcased_hash[key.to_s.downcase] = value downcased_hash end # Replace variables in the selected_event with any overrides, ignoring case of the key keys_with_variables = selected_event.select { |(key, value)| value.kind_of?(Symbol) || value.kind_of?(Hash) } keys_with_variables.each do |original_key, variable_type| # Does an override an exist in the supplied list? if overriden_pair = overrides.find { |(key, value)| key == original_key } # We have an override! Let's replace the template value in the event with the overriden value selected_event[original_key] = overriden_pair.last else # Based on the type, let's generate a placeholder. selected_event[original_key] = case variable_type when :string rand(100000).to_s when Hash if variable_type.has_key? "one_of" # Choose a random possibility possibilities = variable_type['one_of'] possibilities[rand(possibilities.size)] else raise "Unrecognized Hash fixture property! ##{variable_type.keys.to_sentence}" end else raise "Unrecognized fixture variable type #{variable_type}!" end end end hash_to_stanza(selected_event).tap do |event| selected_event.each_pair do |key, value| event.meta_def(key) { value } end end end def hash_to_stanza(hash) ordered_hash = hash.to_a starter = hash.find { |(key, value)| key.strip =~ /^(Response|Action)$/i } ordered_hash.unshift ordered_hash.delete(starter) if starter ordered_hash.inject(String.new) do |stanza,(key, value)| stanza + "#{key}: #{value}\r\n" end + "\r\n" end def format_newlines(string) # HOLY FUCK THIS IS UGLY tmp_replacement = random_string string.gsub("\r\n", tmp_replacement). gsub("\n", "\r\n"). gsub(tmp_replacement, "\r\n") end def random_string (rand(1_000_000_000_000) + 1_000_000_000).to_s end def follows_body_text(name) case name when "with_colon_after_first_line" "Host Username Refresh State Reg.Time \r\nlax.teliax.net:5060 jicksta 105 Registered Tue, 11 Nov 2008 02:29:55" when "show_channels_from_wayne" "Channel Location State Application(Data)\r\n0 active channels\r\n0 active calls" when "empty_string" "" end end def syntax_error_data(name) case name when "immediate_packet_with_colon" "!IJ@MHY:!&@B*!B @ ! @^! @ !@ !\r!@ ! @ !@ ! !!m, \n\\n\n" end end ruby-ami-2.4.0/lib/0000755000175000017500000000000012720060167013747 5ustar joostvbjoostvbruby-ami-2.4.0/lib/ruby_ami.rb0000644000175000017500000000052412720060167016104 0ustar joostvbjoostvb%w{ celluloid/io }.each { |f| require f } class Logger alias :trace :debug end module RubyAMI def self.new_uuid SecureRandom.uuid end end %w{ core_ext/celluloid action agi_result_parser async_agi_environment_parser client error event lexer response stream version }.each { |f| require "ruby_ami/#{f}" } ruby-ami-2.4.0/lib/ruby_ami/0000755000175000017500000000000012720060167015556 5ustar joostvbjoostvbruby-ami-2.4.0/lib/ruby_ami/action.rb0000644000175000017500000000575312720060167017372 0ustar joostvbjoostvbmodule RubyAMI class Action attr_reader :name, :headers, :action_id, :response CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls dahdishowchannels coreshowchannels dbget status agents konferencelist confbridgelist confbridgelistrooms] unless defined? CAUSAL_EVENT_NAMES def initialize(name, headers = {}, &block) @name = name.to_s.downcase.freeze @headers = headers.freeze @action_id = RubyAMI.new_uuid @response = nil @complete = false @events = [] @callback = block end def complete? @complete end ## # When sending an action with "causal events" (i.e. events which must be collected to form a proper # response), AMI should send a particular event which instructs us that no more events will be sent. # This event is called the "causal event terminator". # # Note: you must supply both the name of the event and any headers because it's possible that some uses of an # action (i.e. same name, different headers) have causal events while other uses don't. # # @param [String] name the name of the event # @param [Hash] the headers associated with this event # @return [String] the downcase()'d name of the event name for which to wait # def has_causal_events? CAUSAL_EVENT_NAMES.include? name end ## # Used to determine the event name for an action which has causal events. # # @param [String] action_name # @return [String] The corresponding event name which signals the completion of the causal event sequence. # def causal_event_terminator_name return unless has_causal_events? case name when "sippeers", "iaxpeers" "peerlistcomplete" when "konferencelist" "conferencelistcomplete" else name + "complete" end end ## # Converts this action into a protocol-valid String, ready to be sent over a socket. # def to_s @textual_representation ||= ( "Action: #{@name}\r\nActionID: #{@action_id}\r\n" + @headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") + (@headers.any? ? "\r\n\r\n" : "\r\n") ) end def <<(message) case message when Error self.response = message complete! when Event 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? response.events << message complete! if message.name.downcase == causal_event_terminator_name when Response self.response = message complete! unless has_causal_events? end self end def eql?(other) to_s == other.to_s end alias :== :eql? private def response=(other) @response = other end def complete! @complete = true @callback.call response if @callback end end end # RubyAMI ruby-ami-2.4.0/lib/ruby_ami/agi_result_parser.rb0000644000175000017500000000204412720060167021615 0ustar joostvbjoostvbrequire 'cgi' module RubyAMI class AGIResultParser attr_reader :code, :result, :data FORMAT = /^(?\d{3})( result=(?-?\d*))? ?(?\(?.*\)?)?$/.freeze DATA_KV_FORMAT = /(?[\w\d]+)=(?[\w\d]*)/.freeze DATA_CLEANER = /(^\()|(\)$)/.freeze def initialize(result_string) @result_string = result_string.dup raise ArgumentError, "The result string (#{@result_string}) did not match the required format (#{FORMAT})." unless match parse end def data_hash return unless data_kv_match {data_kv_match[:key] => data_kv_match[:value]} end private def unescape CGI.unescape @result_string end def match @match ||= unescape.chomp.match(FORMAT) end def parse @code = match[:code].to_i @result = match[:result] ? match[:result].to_i : nil @data = match[:data] ? match[:data].gsub(DATA_CLEANER, '').freeze : nil end def data_kv_match @data_kv_match ||= data.match(DATA_KV_FORMAT) end end end ruby-ami-2.4.0/lib/ruby_ami/async_agi_environment_parser.rb0000644000175000017500000000112312720060167024035 0ustar joostvbjoostvb# encoding: utf-8 require 'cgi' module RubyAMI class AsyncAGIEnvironmentParser NEWLINE = "%0A".freeze COLON_SPACE = '%3A%20'.freeze def initialize(environment_string) @environment_string = environment_string.dup end def to_hash to_array.inject({}) do |accumulator, element| accumulator[element[0].to_sym] = CGI.unescape(element[1] || '') accumulator end end def to_s @environment_string.dup end private def to_array @environment_string.split(NEWLINE).map { |p| p.split COLON_SPACE } end end end ruby-ami-2.4.0/lib/ruby_ami/client.rb0000644000175000017500000000346112720060167017365 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI class Client include Celluloid trap_exit :stream_died attr_reader :events_stream, :actions_stream def initialize(options) @options = options @event_handler = @options[:event_handler] @state = :stopped client = current_actor @events_stream = new_stream ->(event) { client.async.handle_event event } @actions_stream = new_stream ->(message) { client.async.handle_message message } end [:started, :stopped, :ready].each do |state| define_method("#{state}?") { @state == state } end def start @events_stream.async.run @actions_stream.async.run @state = :started end def send_action(*args) actions_stream.send_action *args end def handle_message(message) logger.trace "[RECV-ACTIONS]: #{message.inspect}" case message when Stream::Connected send_action 'Events', 'EventMask' => 'Off' when Stream::Disconnected when Event pass_event message end end def handle_event(event) logger.trace "[RECV-EVENTS]: #{event.inspect}" case event when Stream::Connected, Stream::Disconnected else pass_event event end end private def pass_event(event) @event_handler.call event if @event_handler.respond_to? :call end def new_stream(callback) Stream.new_link @options[:host], @options[:port], @options[:username], @options[:password], callback, logger, @options[:timeout] end def stream_died(stream, reason = nil) terminate end def logger super rescue @logger ||= begin logger = Logger logger.define_singleton_method :trace, logger.method(:debug) logger end end end end ruby-ami-2.4.0/lib/ruby_ami/core_ext/0000755000175000017500000000000012720060167017366 5ustar joostvbjoostvbruby-ami-2.4.0/lib/ruby_ami/core_ext/celluloid.rb0000644000175000017500000000013312720060167021664 0ustar joostvbjoostvbmodule Celluloid::Logger def self.trace(*args, &block) debug *args, &block end end ruby-ami-2.4.0/lib/ruby_ami/error.rb0000644000175000017500000000103312720060167017231 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI class Error < StandardError attr_accessor :message, :action def initialize(headers = {}) @headers = headers end def [](key) @headers[key] end def []=(key,value) self.message = value if key == 'Message' @headers[key] = value end def action_id @headers['ActionID'] end def inspect "#<#{self.class} #{[:message, :headers].map { |c| "#{c}=#{self.__send__(c).inspect rescue nil}" }.compact * ', '}>" end end end # RubyAMI ruby-ami-2.4.0/lib/ruby_ami/event.rb0000644000175000017500000000133412720060167017225 0ustar joostvbjoostvb# encoding: utf-8 require 'time' require 'ruby_ami/response' module RubyAMI class Event < Response attr_reader :name, :receipt_time def initialize(name, headers = {}) @receipt_time = DateTime.now super headers @name = name end # @return [DateTime, nil] the timestamp of the event, or nil if none is available def timestamp return unless headers['Timestamp'] DateTime.strptime headers['Timestamp'], '%s' end # @return [DateTime] the best known timestamp for the event. Either its timestamp if specified, or its receipt time if not. def best_time timestamp || receipt_time end def inspect_attributes [:name] + super end end end # RubyAMI ruby-ami-2.4.0/lib/ruby_ami/lexer.rb0000644000175000017500000001011212720060167017215 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI class Lexer STANZA_BREAK = "\r\n\r\n" PROMPT = /Asterisk Call Manager\/(\d+(\.\d+)*)\r\n/ KEYVALUEPAIR = /^([[[:alnum:]]-_ ]+): *(.*)\r\n/ FOLLOWSDELIMITER = /\r?\n?--END COMMAND--\r\n\r\n/ SUCCESS = /response: *success/i PONG = /response: *pong/i EVENT = /event: *(?.*)?/i ERROR = /response: *error/i FOLLOWS = /response: *follows/i GOODBYE = /response: *goodbye/i SCANNER = /.*?#{STANZA_BREAK}/m HEADER_SLICE = /.*\r\n/ IMMEDIATE_RESP = /.*/ CLASSIFIER = /((?#{EVENT})|(?#{SUCCESS})|(?#{PONG})|(?#{FOLLOWS})|(?#{ERROR})|(?#{GOODBYE})|(?#{IMMEDIATE_RESP})\r\n)\r\n/i attr_accessor :ami_version def initialize(delegate = nil) @delegate = delegate @buffer = "" @ami_version = nil end def <<(new_data) @buffer << new_data parse_buffer end private def parse_buffer # Special case for the protocol header if @buffer =~ PROMPT @ami_version = $1 @buffer.slice! HEADER_SLICE end # We need at least one complete message before parsing return unless @buffer.include?(STANZA_BREAK) @processed = 0 response_follows_message = false current_message = nil @buffer.scan(SCANNER).each do |raw| if response_follows_message if handle_response_follows(response_follows_message, raw) @processed += raw.length message_received response_follows_message response_follows_message = nil end else response_follows_message = parse_message raw end end @buffer.slice! 0, @processed end def parse_message(raw) return if raw.length == 0 # Mark this message as processed, including the 4 stripped cr/lf bytes @processed += raw.length match = raw.match CLASSIFIER msg = if match[:event] Event.new match[:event_name] elsif match[:success] || match[:pong] || match[:goodbye] Response.new elsif match[:follows] response_follows = true Response.new elsif match[:error] Error.new elsif match[:immediate] if raw.include?(':') syntax_error_encountered raw.chomp(STANZA_BREAK) return end immediate_response = true Response.from_immediate_response match[:immediate] end # Strip off the header line raw.slice! HEADER_SLICE populate_message_body msg, raw return msg if response_follows && !handle_response_follows(msg, raw) case msg when Error error_received msg else message_received msg end nil end ## # Called after a response or event has been successfully parsed. # # @param [Response, Event] message The message just received # def message_received(message) @delegate.message_received message end ## # Called after an AMI error has been successfully parsed. # # @param [Response, Event] message The message just received # def error_received(message) @delegate.error_received message end ## # Called when there's a syntax error on the socket. This doesn't happen as often as it should because, in many cases, # it's impossible to distinguish between a syntax error and an immediate packet. # # @param [String] ignored_chunk The offending text which caused the syntax error. def syntax_error_encountered(ignored_chunk) @delegate.syntax_error_encountered ignored_chunk end def populate_message_body(obj, raw) while raw.slice! KEYVALUEPAIR obj[$1] = $2 end obj end def handle_response_follows(obj, raw) obj.text_body ||= '' obj.text_body << raw return false unless raw =~ FOLLOWSDELIMITER obj.text_body.sub! FOLLOWSDELIMITER, '' obj.text_body.chomp! true end end end ruby-ami-2.4.0/lib/ruby_ami/response.rb0000644000175000017500000000213412720060167017741 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI ## # This is the object containing a response from Asterisk. # class Response class << self def from_immediate_response(text) new.tap do |instance| instance.text_body = text end end end attr_accessor :text_body, # For "Response: Follows" sections :events def initialize(headers = {}) @headers = headers @events = [] end def has_text_body? !!@text_body end def headers @headers.clone end def [](arg) @headers[arg.to_s] end def []=(key,value) @headers[key.to_s] = value end def action_id @headers['ActionID'] end def inspect "#<#{self.class} #{inspect_attributes.map { |c| "#{c}=#{self.__send__(c).inspect rescue nil}" }.compact * ', '}>" end def inspect_attributes [:headers, :text_body, :events] end def eql?(o, *fields) o.is_a?(self.class) && (fields + inspect_attributes).all? { |f| self.__send__(f) == o.__send__(f) } end alias :== :eql? end end # RubyAMI ruby-ami-2.4.0/lib/ruby_ami/stream.rb0000644000175000017500000001014412720060167017376 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI class Stream class ConnectionStatus def name self.class.to_s end def eql?(other) other.is_a? self.class end alias :== :eql? end Connected = Class.new ConnectionStatus Disconnected = Class.new ConnectionStatus include Celluloid::IO attr_reader :logger finalizer :finalize def initialize(host, port, username, password, event_callback, logger = Logger, timeout = 0) super() @host, @port, @username, @password, @event_callback, @logger, @timeout = host, port, username, password, event_callback, logger, timeout logger.debug "Starting up..." @lexer = Lexer.new self @sent_actions = {} @causal_actions = {} end [:started, :stopped, :ready].each do |state| define_method("#{state}?") { @state == state } end def run Timeout::timeout(@timeout) do @socket = TCPSocket.from_ruby_socket ::TCPSocket.new(@host, @port) end post_init loop { receive_data @socket.readpartial(4096) } rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e logger.error "Connection failed due to #{e.class}. Check your config and the server." rescue EOFError logger.info "Client socket closed!" rescue Timeout::Error logger.error "Timeout exceeded while trying to connect." ensure async.terminate end def post_init @state = :started fire_event Connected.new login @username, @password if @username && @password end def version @lexer.ami_version end def send_data(data) @socket.write data end def send_action(name, headers = {}, error_handler = self.method(:abort)) condition = Celluloid::Condition.new action = dispatch_action name, headers do |response| condition.signal response end condition.wait action.response.tap do |resp| if resp.is_a? Exception error_handler.call(resp) end end end def receive_data(data) logger.trace "[RECV] #{data}" @lexer << data end def message_received(message) logger.trace "[RECV] #{message.inspect}" case message when Event action = causal_action_for_event message if action action << message complete_causal_action_for_event message if action.complete? else fire_event message end when Response, Error action = sent_action_for_response message raise StandardError, "Received an AMI response with an unrecognized ActionID! #{message.inspect}" unless action action << message end end def syntax_error_encountered(ignored_chunk) logger.error "Encountered a syntax error. Ignoring chunk: #{ignored_chunk.inspect}" end alias :error_received :message_received private def login(username, password, event_mask = 'On') dispatch_action 'Login', 'Username' => username, 'Secret' => password, 'Events' => event_mask end def dispatch_action(*args, &block) action = Action.new *args, &block logger.trace "[SEND] #{action.to_s}" register_sent_action action send_data action.to_s action end def fire_event(event) @event_callback.call event end def register_sent_action(action) @sent_actions[action.action_id] = action register_causal_action action if action.has_causal_events? end def sent_action_with_id(action_id) @sent_actions.delete action_id end def sent_action_for_response(response) sent_action_with_id response.action_id end def register_causal_action(action) @causal_actions[action.action_id] = action end def causal_action_for_event(event) @causal_actions[event.action_id] end def complete_causal_action_for_event(event) @causal_actions.delete event.action_id end def finalize logger.debug "Finalizing stream" @socket.close if @socket @state = :stopped fire_event Disconnected.new end end end ruby-ami-2.4.0/lib/ruby_ami/version.rb0000644000175000017500000000007112720060167017566 0ustar joostvbjoostvb# encoding: utf-8 module RubyAMI VERSION = "2.4.0" end ruby-ami-2.4.0/ruby_ami.gemspec0000644000175000017500000000251112720060167016354 0ustar joostvbjoostvb# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "ruby_ami/version" Gem::Specification.new do |s| s.name = "ruby_ami" s.version = RubyAMI::VERSION s.authors = ["Ben Langfeld", "Ben Klang"] s.email = ["ben@langfeld.me", "bklang@mojolingo.com"] s.homepage = "" s.summary = %q{Futzing with AMI so you don't have to} s.description = %q{A Ruby client library for the Asterisk Management Interface built on Celluloid IO.} s.rubyforge_project = "ruby_ami" s.files = `git ls-files`.split("\n") << 'lib/ruby_ami/lexer.rb' s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] s.add_runtime_dependency %q, ["~> 0.13"] s.add_development_dependency %q, ["~> 1.0"] s.add_development_dependency %q, ["~> 2.5"] s.add_development_dependency %q, [">= 0"] s.add_development_dependency %q, ["~> 0.6"] s.add_development_dependency %q, [">= 0"] s.add_development_dependency %q s.add_development_dependency %q s.add_development_dependency %q s.add_development_dependency %q s.add_development_dependency %q end ruby-ami-2.4.0/spec/0000755000175000017500000000000012720060167014133 5ustar joostvbjoostvbruby-ami-2.4.0/spec/ruby_ami/0000755000175000017500000000000012720060167015742 5ustar joostvbjoostvbruby-ami-2.4.0/spec/ruby_ami/action_spec.rb0000644000175000017500000000733212720060167020563 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Action do let(:name) { 'foobar' } let(:headers) { {'foo' => 'bar'} } subject do described_class.new name, headers do |response| @callback_result = response end end it { should_not be_complete } describe "SIPPeers actions" do subject { Action.new('SIPPeers') } its(:has_causal_events?) { should be true } end describe "the ParkedCalls terminator event" do subject { Action.new('ParkedCalls') } its(:causal_event_terminator_name) { should == "parkedcallscomplete" } end it "should properly convert itself into a String when additional headers are given" do string = Action.new("Hawtsawce", "Monkey" => "Zoo").to_s string.should =~ /^Action: Hawtsawce\r\n/i string.should =~ /[^\n]\r\n\r\n$/ string.should =~ /^(\w+:\s*[\w-]+\r\n){3}\r\n$/ end it "should properly convert itself into a String when no additional headers are given" do Action.new("Ping").to_s.should =~ /^Action: Ping\r\nActionID: [\w-]+\r\n\r\n$/i Action.new("ParkedCalls").to_s.should =~ /^Action: ParkedCalls\r\nActionID: [\w-]+\r\n\r\n$/i end describe '#<<' do describe 'for a non-causal action' do context 'with a response' do let(:response) { Response.new } before { subject << response } it 'should set the response' do subject.response.should be response end it 'should call the callback' do @callback_result.should be response end it { should be_complete } end context 'with an error' do let(:error) { Error.new.tap { |e| e.message = 'AMI error' } } before { subject << error } it 'should set the response' do subject.response.should == error end it { should be_complete } end context 'with an event' do it 'should raise an error' do lambda { subject << Event.new('foo') }.should raise_error StandardError, /causal action/ end end end describe 'for a causal action' do let(:name) { 'Status' } let(:response) { Response.new } context 'with a response' do before { subject << response } it { should_not be_complete } end context 'with an event' do let(:event) { Event.new 'foo' } before { subject << response << event } it "should add the events to the response" do subject.response.events.should == [event] end end context 'with a terminating event' do let(:event) { Event.new 'StatusComplete' } before do subject << response subject.should_not be_complete subject << event end it "should add the events to the response" do subject.response.events.should == [event] end it { should be_complete } its(:response) { should be response } end end end describe 'comparison' do describe 'with another Action' do context 'with identical name and headers' do let(:other) { Action.new name, headers } it { should == other } end context 'with identical name and different headers' do let(:other) { Action.new name, 'boo' => 'baz' } it { should_not == other } end context 'with different name and identical headers' do let(:other) { Action.new 'BARBAZ', headers } it { should_not == other } end end it { should_not == :foo } end end # Action end # RubyAMI ruby-ami-2.4.0/spec/ruby_ami/agi_result_parser_spec.rb0000644000175000017500000000345712720060167023024 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe AGIResultParser do subject { described_class.new result_string } context 'with something that does not match the valid format' do let(:result_string) { 'foobar' } it 'should raise ArgumentError on creation' do expect { subject }.to raise_error(ArgumentError, /format/) end end context 'with a simple result with no data' do let(:result_string) { "200%20result=123%0A" } its(:code) { should == 200 } its(:result) { should == 123 } its(:data) { should == '' } its(:data_hash) { should == nil } end context 'with a simple unescaped result with no data' do let(:result_string) { "200 result=123" } its(:code) { should == 200 } its(:result) { should == 123 } its(:data) { should == '' } its(:data_hash) { should == nil } end context 'with a result and data in parens' do let(:result_string) { "200%20result=-123%20(timeout)%0A" } its(:code) { should == 200 } its(:result) { should == -123 } its(:data) { should == 'timeout' } its(:data_hash) { should == nil } end context 'with a result and key-value data' do let(:result_string) { "200%20result=123%20foo=bar%0A" } its(:code) { should == 200 } its(:result) { should == 123 } its(:data) { should == 'foo=bar' } its(:data_hash) { should == {'foo' => 'bar'} } end context 'with a 5xx error' do let(:result_string) { "510%20Invalid%20or%20unknown%20command%0A" } its(:code) { should == 510 } its(:result) { should be_nil } its(:data) { should == 'Invalid or unknown command' } its(:data_hash) { should be_nil } end end end ruby-ami-2.4.0/spec/ruby_ami/async_agi_environment_parser_spec.rb0000644000175000017500000000354412720060167025244 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe AsyncAGIEnvironmentParser do let :environment_string do "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A" end subject { described_class.new environment_string } its(:to_s) { should == environment_string } its(:to_s) { should_not be environment_string } describe 'retrieving a hash representation' do its(:to_hash) do should == { :agi_request => 'async', :agi_channel => 'SIP/1234-00000000', :agi_language => 'en', :agi_type => 'SIP', :agi_uniqueid => '1320835995.0', :agi_version => '1.8.4.1', :agi_callerid => '5678', :agi_calleridname => 'Jane Smith', :agi_callingpres => '0', :agi_callingani2 => '0', :agi_callington => '0', :agi_callingtns => '0', :agi_dnid => '1000', :agi_rdnis => 'unknown', :agi_context => 'default', :agi_extension => '1000', :agi_priority => '1', :agi_enhanced => '0.0', :agi_accountcode => '', :agi_threadid => '4366221312' } end it "should not return the same hash object every time" do subject.to_hash.should_not be subject.to_hash end end end end ruby-ami-2.4.0/spec/ruby_ami/client_spec.rb0000644000175000017500000000445712720060167020571 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Client do let(:event_handler) { [] } let(:options) do { :host => '127.0.0.1', :port => 50000 - rand(1000), :username => 'username', :password => 'password', :event_handler => lambda { |event| event_handler << event } } end subject { Client.new options } it { should be_stopped } its(:events_stream) { should be_a Stream } its(:actions_stream) { should be_a Stream } it 'should return when the timeout option is specified and reached' do pending options[:timeout] = 2 options[:host] = '192.0.2.1' # unreachable IP that will generally cause a timeout (RFC 5737) start_time = Time.now subject.start duration = Time.now - start_time duration.should be_between(options[:timeout], options[:timeout] + 1) end describe 'starting up' do before do ms = MockServer.new ms.should_receive(:receive_data).at_least :once s = ServerMock.new options[:host], options[:port], ms subject.async.start sleep 0.2 end it { should be_started } end describe 'logging in streams' do context 'when the actions stream connects' do let(:mock_actions_stream) { mock 'Actions Stream' } before do subject.wrapped_object.stub(:actions_stream).and_return mock_actions_stream end it 'should disable events' do mock_actions_stream.should_receive(:send_action).with 'Events', 'EventMask' => 'Off' subject.handle_message Stream::Connected.new end end end describe 'when the events stream disconnects' do it 'should shut down the client' do subject.events_stream.terminate sleep 0.2 subject.alive?.should be_false end end describe 'when the actions stream disconnects' do it 'should shut down the client' do subject.actions_stream.terminate sleep 0.2 subject.alive?.should be_false end end describe 'when an event is received' do let(:event) { Event.new 'foobar' } it 'should call the event handler' do subject.handle_event event event_handler.should == [event] end end end end ruby-ami-2.4.0/spec/ruby_ami/error_spec.rb0000644000175000017500000000016412720060167020433 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Error do pending end # Error end # RubyAMI ruby-ami-2.4.0/spec/ruby_ami/event_spec.rb0000644000175000017500000000542112720060167020424 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Event do subject { described_class.new 'Hangup' } describe "#receipt_time" do before do @now = DateTime.now DateTime.stub now: @now end it "should be the time the object was created (event receipt time)" do subject.receipt_time.should == @now end end context "when the event has a timestamp" do subject { described_class.new 'Hangup', 'Timestamp' => '1393368380.572575' } describe "#timestamp" do it "should be a time object representing the event's timestamp (assuming UTC)" do subject.timestamp.should == DateTime.new(2014, 2, 25, 22, 46, 20) end end describe "#best_time" do it "should be the timestamp" do subject.best_time.should == subject.timestamp end end end context "when the event does not have a timestamp" do describe "#timestamp" do it "should be nil" do subject.timestamp.should be_nil end end describe "#best_time" do it "should be the receipt_time" do subject.best_time.should == subject.receipt_time end end end describe "equality" do context "with the same name and the same headers" do let :event1 do Event.new 'Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end let :event2 do Event.new 'Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end it "should be equal" do event1.should be == event2 end end context "with a different name and the same headers" do let :event1 do Event.new 'Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end let :event2 do Event.new 'Foo', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end it "should not be equal" do event1.should_not be == event2 end end context "with the same name and different headers" do let :event1 do Event.new 'Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end let :event2 do Event.new 'Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '1' end it "should not be equal" do event1.should_not be == event2 end end end end # Event end # RubyAMI ruby-ami-2.4.0/spec/ruby_ami/response_spec.rb0000644000175000017500000000216312720060167021141 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Response do describe "equality" do context "with the same headers" do let :event1 do Response.new 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end let :event2 do Response.new 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end it "should be equal" do event1.should be == event2 end end context "with different headers" do let :event1 do Response.new 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0' end let :event2 do Response.new 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '1' end it "should not be equal" do event1.should_not be == event2 end end end end # Response end # RubyAMI ruby-ami-2.4.0/spec/ruby_ami/stream_spec.rb0000644000175000017500000002171212720060167020577 0ustar joostvbjoostvb# encoding: utf-8 require 'spec_helper' module RubyAMI describe Stream do let(:server_port) { 50000 - rand(1000) } def client @client ||= mock('Client') end before do def client.message_received(message) @messages ||= Queue.new @messages << message end def client.messages @messages end end let :client_messages do messages = [] messages << client.messages.pop until client.messages.empty? messages end let(:username) { nil } let(:password) { nil } def mocked_server(times = nil, fake_client = nil, &block) mock_target = MockServer.new mock_target.should_receive(:receive_data).send(*(times ? [:exactly, times] : [:at_least, 1])).with &block s = ServerMock.new '127.0.0.1', server_port, mock_target @stream = Stream.new '127.0.0.1', server_port, username, password, lambda { |m| client.message_received m } @stream.async.run fake_client.call if fake_client.respond_to? :call Celluloid::Actor.join s Timeout.timeout 5 do Celluloid::Actor.join @stream end end def expect_connected_event client.should_receive(:message_received).with Stream::Connected.new end def expect_disconnected_event client.should_receive(:message_received).with Stream::Disconnected.new end before { @sequence = 1 } describe "after connection" do it "should be started" do expect_connected_event expect_disconnected_event mocked_server 0, -> { @stream.started?.should be_true } end it "stores the reported AMI version" do expect_connected_event expect_disconnected_event mocked_server(1, lambda { @stream.send_action('Command') # Just to get the server kicked in to replying using the below block expect(@stream.version).to eq('2.8.0') }) do |val, server| server.send_data "Asterisk Call Manager/2.8.0\n" # Just to unblock the above command before the actor shuts down server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Recording started EVENT end end it "can send an action" do expect_connected_event expect_disconnected_event mocked_server(1, lambda { @stream.send_action('Command') }) do |val, server| val.should == <<-ACTION Action: command\r ActionID: #{RubyAMI.new_uuid}\r \r ACTION server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Recording started EVENT end end it "can send an action with headers" do expect_connected_event expect_disconnected_event mocked_server(1, lambda { @stream.send_action('Command', 'Command' => 'RECORD FILE evil') }) do |val, server| val.should == <<-ACTION Action: command\r ActionID: #{RubyAMI.new_uuid}\r Command: RECORD FILE evil\r \r ACTION server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Recording started EVENT end end it "can process an action with a Response: Follows result" do action_id = RubyAMI.new_uuid response = nil mocked_server(1, lambda { response = @stream.send_action('Command', 'Command' => 'dialplan add extension 1,1,AGI,agi:async into adhearsion-redirect') }) do |val, server| val.should == <<-ACTION Action: command\r ActionID: #{action_id}\r Command: dialplan add extension 1,1,AGI,agi:async into adhearsion-redirect\r \r ACTION server.send_data <<-EVENT Response: Follows Privilege: Command ActionID: #{action_id} Extension '1,1,AGI(agi:async)' added into 'adhearsion-redirect' context --END COMMAND-- EVENT end expected_response = Response.new 'Privilege' => 'Command', 'ActionID' => action_id expected_response.text_body = %q{Extension '1,1,AGI(agi:async)' added into 'adhearsion-redirect' context} response.should == expected_response end context "with a username and password set" do let(:username) { 'fred' } let(:password) { 'jones' } it "should log itself in" do expect_connected_event expect_disconnected_event mocked_server(1, lambda { }) do |val, server| val.should == <<-ACTION Action: login\r ActionID: #{RubyAMI.new_uuid}\r Username: fred\r Secret: jones\r Events: On\r \r ACTION server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Authentication accepted EVENT end end end end it 'sends events to the client when the stream is ready' do mocked_server(1, lambda { @stream.send_data 'Foo' }) do |val, server| server.send_data <<-EVENT Event: Hangup Channel: SIP/101-3f3f Uniqueid: 1094154427.10 Cause: 0 EVENT end client_messages.should be == [ Stream::Connected.new, Event.new('Hangup', 'Channel' => 'SIP/101-3f3f', 'Uniqueid' => '1094154427.10', 'Cause' => '0'), Stream::Disconnected.new ] end describe 'when a response is received' do before do expect_connected_event expect_disconnected_event end it 'should be returned from #send_action' do response = nil mocked_server(1, lambda { response = @stream.send_action 'Command', 'Command' => 'RECORD FILE evil' }) do |val, server| server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Recording started EVENT end response.should == Response.new('ActionID' => RubyAMI.new_uuid, 'Message' => 'Recording started') end it 'should handle disconnect as a Response' do response = nil mocked_server(1, lambda { response = @stream.send_action 'Logoff' }) do |val, server| server.send_data <<-EVENT Response: Goodbye ActionID: #{RubyAMI.new_uuid} Message: Thanks for all the fish. EVENT end response.should == Response.new('ActionID' => RubyAMI.new_uuid, 'Message' => 'Thanks for all the fish.') end describe 'when it is an error' do describe 'when there is no error handler' do it 'should be raised by #send_action, but not kill the stream' do send_action = lambda do expect { @stream.send_action 'status' }.to raise_error(RubyAMI::Error, 'Action failed') @stream.should be_alive end mocked_server(1, send_action) do |val, server| server.send_data <<-EVENT Response: Error ActionID: #{RubyAMI.new_uuid} Message: Action failed EVENT end end end describe 'when there is an error handler' do it 'should call the error handler' do error_handler = lambda { |resp| resp.should be_a_kind_of RubyAMI::Error } send_action = lambda do expect { @stream.send_action 'status', {}, error_handler }.to_not raise_error @stream.should be_alive end mocked_server(1, send_action) do |val, server| server.send_data <<-EVENT Response: Error ActionID: #{RubyAMI.new_uuid} Message: Action failed EVENT end end end end describe 'for a causal action' do let :expected_events do [ Event.new('PeerEntry', 'ActionID' => RubyAMI.new_uuid, 'Channeltype' => 'SIP', 'ObjectName' => 'usera'), Event.new('PeerlistComplete', 'ActionID' => RubyAMI.new_uuid, 'EventList' => 'Complete', 'ListItems' => '2') ] end let :expected_response do Response.new('ActionID' => RubyAMI.new_uuid, 'Message' => 'Events to follow').tap do |response| response.events = expected_events end end it "should return the response with events" do response = nil mocked_server(1, lambda { response = @stream.send_action 'sippeers' }) do |val, server| server.send_data <<-EVENT Response: Success ActionID: #{RubyAMI.new_uuid} Message: Events to follow Event: PeerEntry ActionID: #{RubyAMI.new_uuid} Channeltype: SIP ObjectName: usera Event: PeerlistComplete EventList: Complete ListItems: 2 ActionID: #{RubyAMI.new_uuid} EVENT end response.should == expected_response end end end it 'puts itself in the stopped state and fires a disconnected event when unbound' do expect_connected_event expect_disconnected_event mocked_server(1, lambda { @stream.send_data 'Foo' }) do |val, server| @stream.stopped?.should be false end @stream.alive?.should be false end end describe Stream::Connected do its(:name) { should == 'RubyAMI::Stream::Connected' } end describe Stream::Disconnected do its(:name) { should == 'RubyAMI::Stream::Disconnected' } end end ruby-ami-2.4.0/spec/spec_helper.rb0000644000175000017500000000055412720060167016755 0ustar joostvbjoostvb# encoding: utf-8 require 'ruby_ami' Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} include RubyAMI RSpec.configure do |config| config.mock_with :rspec config.filter_run :focus => true config.run_all_when_everything_filtered = true config.before :each do uuid = RubyAMI.new_uuid RubyAMI.stub :new_uuid => uuid end end ruby-ami-2.4.0/spec/support/0000755000175000017500000000000012720060167015647 5ustar joostvbjoostvbruby-ami-2.4.0/spec/support/mock_server.rb0000644000175000017500000000172312720060167020516 0ustar joostvbjoostvb# encoding: utf-8 MockServer = Class.new class ServerMock include Celluloid::IO finalizer :finalize def initialize(host, port, mock_target = MockServer.new) puts "*** Starting echo server on #{host}:#{port}" @server = TCPServer.new host, port @mock_target = mock_target @clients = [] async.run end def finalize Logger.debug "ServerMock finalizing" @server.close if @server @clients.each(&:close) end def run after(0.5) { terminate } loop { async.handle_connection @server.accept } end def handle_connection(socket) @clients << socket _, port, host = socket.peeraddr puts "*** Received connection from #{host}:#{port}" loop { receive_data socket.readpartial(4096) } end def receive_data(data) Logger.debug "ServerMock receiving data: #{data}" @mock_target.receive_data data, self end def send_data(data) @clients.each { |client| client.write data.gsub("\n", "\r\n") } end end ruby-ami-2.4.0/metadata.yml0000644000175000017500000001471312720060167015512 0ustar joostvbjoostvb--- !ruby/object:Gem::Specification name: ruby_ami version: !ruby/object:Gem::Version version: 2.4.0 platform: ruby authors: - Ben Langfeld - Ben Klang autorequire: bindir: bin cert_chain: [] date: 2015-12-07 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: celluloid-io requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '0.13' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '0.13' - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.5' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.5' - !ruby/object:Gem::Dependency name: cucumber requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: yard requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '0.6' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '0.6' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: guard-rspec requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: guard-shell requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: guard-cucumber requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: guard-rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: benchmark_suite requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: A Ruby client library for the Asterisk Management Interface built on Celluloid IO. email: - ben@langfeld.me - bklang@mojolingo.com executables: [] extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".rspec" - ".travis.yml" - CHANGELOG.md - Gemfile - Guardfile - LICENSE.txt - README.md - Rakefile - benchmarks/agi_env_parse.rb - benchmarks/lexer.rb - cucumber.yml - features/lexer.feature - features/step_definitions/lexer_steps.rb - features/support/ami_fixtures.yml - features/support/env.rb - features/support/introspective_lexer.rb - features/support/lexer_helper.rb - lib/ruby_ami.rb - lib/ruby_ami/action.rb - lib/ruby_ami/agi_result_parser.rb - lib/ruby_ami/async_agi_environment_parser.rb - lib/ruby_ami/client.rb - lib/ruby_ami/core_ext/celluloid.rb - lib/ruby_ami/error.rb - lib/ruby_ami/event.rb - lib/ruby_ami/lexer.rb - lib/ruby_ami/response.rb - lib/ruby_ami/stream.rb - lib/ruby_ami/version.rb - ruby_ami.gemspec - spec/ruby_ami/action_spec.rb - spec/ruby_ami/agi_result_parser_spec.rb - spec/ruby_ami/async_agi_environment_parser_spec.rb - spec/ruby_ami/client_spec.rb - spec/ruby_ami/error_spec.rb - spec/ruby_ami/event_spec.rb - spec/ruby_ami/response_spec.rb - spec/ruby_ami/stream_spec.rb - spec/spec_helper.rb - spec/support/mock_server.rb homepage: '' licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: ruby_ami rubygems_version: 2.4.5 signing_key: specification_version: 4 summary: Futzing with AMI so you don't have to test_files: - features/lexer.feature - features/step_definitions/lexer_steps.rb - features/support/ami_fixtures.yml - features/support/env.rb - features/support/introspective_lexer.rb - features/support/lexer_helper.rb - spec/ruby_ami/action_spec.rb - spec/ruby_ami/agi_result_parser_spec.rb - spec/ruby_ami/async_agi_environment_parser_spec.rb - spec/ruby_ami/client_spec.rb - spec/ruby_ami/error_spec.rb - spec/ruby_ami/event_spec.rb - spec/ruby_ami/response_spec.rb - spec/ruby_ami/stream_spec.rb - spec/spec_helper.rb - spec/support/mock_server.rb has_rdoc: