# Adhearsion, open source technology integrator # Copyright 2006 Jay Phillips # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'core_extensions' module Asterisk # The exec method allows any traditional Asterisk applications to be called # within Adhearsion. The first argument should be the case-insensitive name # of the application and any arguments needed by the application can simply # by trailed onto the end of the method. For a full list of Asterisk's # applications, see "this":http://www.voip-info.org/wiki/index.php?page=Asterisk+-+documentation+of+application+commands def exec(app, *options) result = rawr "EXEC #{app} " + (options * '|') result = result[/-?\d+$/] result == "-2" ? false : result end # This method of receiving user input uses the fantastic Asterisk # Read() application, but its results are pretty inconsitent over # AGI. This code has been kept here in case these bugs are fixed. def old_input digits=nil, user_options=Hash.new('') used_options = {:variable => String.random, :digits => digits} used_options.default = '' used_options.merge! user_options args = [] %w(variable soundfile digits option attempts timeout).each do |x| args << used_options[x.to_sym] end log "THESE ARE THE INPUT ARGS: " + args.inspect exec :read, args $OSCAR[:VARS] << args.first x = get_variable(args.first).gsub(/[\(\)]/, "") puts "RETURN: #{x.inspect}" x.simplify end # Input is used to receive keypad input from the user, pausing until they # have entered the desired number of digits (specified with the first # parameter) or the timeout has been reached (specified as a hash argument # with the key :timeout). By default, there is no timeout, waiting infinitely. # # If you desire a sound to be played other than a simple beep to instruct # the callee to input data, pass the filename as an hash argument with either # the :play or :file key. # # When called without any arguments (or a first argument of -1), the user is # able to enter digits ad infinitum until they press the pound (#) key. def input digits=nil, hash={} timeout, file = (hash[:timeout] || -1), (hash[:play] || hash[:file] || 'beep') result = rawr "GET DATA #{file} #{timeout} #{digits}" result = result[/\=-?\d+/] result ? result[1..-1] : false end # Does as you'd imagine: returns the amount of time the given block takes to # execute. This is particularly useful in VoIP since billing and such often # requires time keeping beyond the Call Detail Records. This method is also # aliased to bill() as well. def time return 0 unless block_given? start = Time.now yield start Time.now - start end alias bill time # Requires an AMI connection! Given a block, everything contained # within it will be recorded. If no arguments are given, a String # for the filename will be generated from the current time and a # sequence of random alphanumeric characters. def record hash, &block defaults = {:file => "#{String.random(5)}", :folder => nil, :channel => Thread.current[:VARS]['channel'], :format => 'wav', :mix => '1'} defaults = defaults.merge hash #PBX.record end # Waits for single digit numpad key response from the user. If no timeout argument # is given, the system will wait indefinitely. The argument is the desired time # to wait fo0r the response in seconds. Feel free to use the ActiveSupport extensions # for using this method like wait_for_digit(2.minutes) or something similar. When the # timeout is encountered (or an error occurs receiving the input), the method # returns nil. If the asterisk or pound key was pressed, a String is returned of that # response. In all other cases, the Fixnum of the number pressed is returned. def wait_for_digit timeout=-1 digit = rawr("WAIT FOR DIGIT #{timeout < 0 ? -1 : timeout / 1000.0}").match(/=(.*)$/)[1].to_i return nil if digit <= 0 # If there was an error or timeout return digit.chr if [32,45].include? digit digit - ?0 end # An abstracted remote mutator for setting Asterisk variables. def set_variable(key,value) rawr "SET VARIABLE #{key} #{value}" end # An abstracted remote accessor for retrieving Asterisk variables. def get_variable(key, default=nil) result = rawr "GET VARIABLE #{key}" result[0..12] == "200 result=0" ? default : result[13..-1] end def dial who, *options #group = who.group if who.respond_to :group exec :dial, properize(who) end # The stream_file() method comes right over from the AGI protocol. # Its use is very similar to the Background() application from # Asterisk's extensions.conf language. A file is played in the # background while optionally waiting for input. In the event # the second argument is given, Asterisk will listen for that # sequence of digits and return control to Adhearsion, informing # us of the digits the user pressed. These digits are returned # as a String. If you would also like to know the ending position # at which the streaming stopped, see stream_file_with_offset(). def stream_file file, digits='', offset=0 stream_file_with_offset(file, digits, offset).first end # Similar to stream_file(), but returning an array of length 2 # instead. If supplied a first argument, the return value will # be digits pressed by the user to end def stream_file_with_offset file, digits='', offset=0 response = rawr "STREAM FILE #{file} #{digits} #{offset}" return nil unless response.starts_with? '200' result_index = response.index(?=) + 1 spacer_index = response.rindex(' ') endpos_index = spacer_index + 8 # " endpos=".length == 8 result = response[result_index...spacer_index].to_i endpos = response[endpos_index..-1].to_i return [nil,nil] if (result.zero? && endpos.zero?) || result == -1 [result, endpos] end # Festival is pretty buggy (and pretty inefficient). In theory, # this should speak out any text supplied to it. def speak text text.gsub!('\n').gsub!('\r').strip! exec :festival, "'#{text}'" end # Since voicemail is used so frequently, extra effort has been made to # make it syntactically sweet. voicemail() takes the mailbox number of # a user as its first or second argument and the mailbox type as its # first or second argument (the order doesn't matter). If a specific # voicemail context is necessary, trail it to the end of the method # with the ":at => 'contextname'" syntax. The mailbox type can be # :busy, :unavailable, or :normal, though if no type is given, the # type :normal is assumed. # # Usage: voicemail :busy, 4544 # or: voicemail 4544 # or: voicemail 4544, :unavailable, :at => 'codemecca' def voicemail msg_or_who=nil, who_or_msg=nil, hash={} opts = [msg_or_who, who_or_msg] type = opts & [:normal, :busy, :unavailable] box = opts - type box.last <<= "@#{hash[:at]}}" if hash[:at] exec :Voicemail, *(type + box) end # Execute VoiceMailMain behind the scenes to check a specified # user's voicemail inbox or, if no arguments are passed, prompt # the user for their mailbox number. # # Usage: check_voicemail 'jay', :at => 'default', :args => 's' # or : check_voicemail # or : check_voicemail 'hubbard' def check_voicemail name=nil, hash={} args = [ [name, hash[:at]].compact * '@', hash[:args]] exec :VoiceMailMain, args.compact end # Play is a long-overdue easy way to play audio files in a dialplan with Asterisk. It can take a # single String of the filename (defaults to /var/lib/asterisk/sounds) just as you # would give it to Playback(), or it can take any number of trailed on Strings. # If you pass it an Array, it will traverse the array, playing each of the items # in order. If you're doing this, make your life *incredibly* easier by using the # fantastic Ruby Array literal for Arrays of Strings: %w(). Within this, one # specifies separate, simple Strings by placing whitespace bewtween them. Since # no Asterisk files contain whitespace, this works _very_ well! Example: # # play %w(a-connect-charge-of 22 cents-per-minute will-apply) # # Note here that numbers can be play()ed as well. By convention, play() will call # SayNumber() on these indices instead of Playback(). def play *files files.flatten! files.each do |f| if f.simplify.is_a? Fixnum then exec :saynumber, f else exec :playback, f end end end # The rawr() method is the main way of receiving a raw response from the Asterisk # server. When no argument is given, it will immediately ask for a response, # returning that String. When an argument +is+ given, it will first send that # command and then return the response that command generated. Everything is # chomp()ed before returned. Pun here totally intended. def rawr(what=nil) begin putc what if what PBX.io.gets.chomp! rescue => e #log "Socket no longer available for communication. Exceptions will likely occur." end end # Simply print()s the command over the AGI IO socket. def putc(what) PBX.io.print what end # The magical method that handles how objects passed to dial() are converted to their # corresponding Asterisk-recognizable technology/extension identifier. Likely wouldn't # be used much outside of dial(). def properize who possible_methods = [:users, :members, :user, :member].select { |pm| who.respond_to? pm } # These are the convention methods for Group-like objects who = who.send possible_methods.first if possible_methods.any? # If 'who' has any of the possible_methods, replace 'who' with the return value of that method return unless who who = [who] unless who.kind_of?(Enumerable) && !who.kind_of?(String) # In case we can't perform collection algorithms on 'who', let's encapsulate it in an Array # If the first thing in the Enumerable responds to a User-like convention, then set 'who' equal to the extension accessor of those objects who.map! { |p| p.send possible_methods.first }.compact! if (possible_methods = [:extension, :extensions].select { |pm| who.first.respond_to? pm }).any? # Now replace each item in the Enumerable with its form converted to extension (assuming it's not already in that format) who.map! do |ext| if ext.kind_of?(String) && ext.index(?/) then ext else ext = "1#{ext}" if ext.is_national_number? && ext.to_s[0] != ?1 "SIP/#{ext}" end end who *= '&' # Finally, join() anything left in the Array with an '&' end # Returns the status of the last dial(). Possible dial # statuses include :answer, :busy, :noanswer, :cancel, # :congestion, and :chanunavail. If :cancel is # returned, the caller hung up before the callee picked # up. If :congestion is returned, the dialed extension # probably doesn't exist. If :chanunavail, the callee # phone may not be registered. def last_dial_status get_variable(:DIALSTATUS).downcase.to_sym end # Answer the channel. Adhearsion is configured by default to automatically do this # when a call comes in. def answer() rawr 'ANSWER' end # Hangs up the channel. Adhearsion is configured by default to matically do this # when a context completes execution def hangup() rawr 'HANGUP' end # Direct translation of the Asterisk NoOp() application. Used primarily for # viewing debug information in the Asterisk CLI. def noop(*options) rawr "NOOP #{options}" end # The SIP/ZAP/IAX/IAX2/Zap methods allow for cleaner addressing of particular # extensions, abstracting Asterisk's representation. If not given any # argument, this methods simply return a symbol identifying their # appropriate protocol. # See the SIP class def SIP(ext=nil) ext ? "SIP/#{ext}" : :SIP end # See the ZAP class def ZAP(ext=nil) ext ? "Zap/#{ext}" : :ZAP end # See the IAX class def IAX(ext=nil) ext ? "IAX2/#{ext}" : :IAX end alias IAX2 IAX alias Zap ZAP # For the completely selfish purpose of sugaring the syntax, two tiny holder classes # are created to allow the synonym of SIP(1404) to be SIP[1404]. Both functions, # when given an argument, return a String that Asterisk understands representing that # particular channel. # Syntax sugar for declaring SIP devices. Use: SIP/123 or SIP[123] class SIP def self.[](arg=nil) SIP(arg) end def self./(arg=nil) SIP(arg) end end # Syntax sugar for declaring IAX devices. Use: IAX/123 or IAX[123] class IAX def self.[](arg=nil) IAX(arg) end def self./(arg=nil) IAX(arg) end end # Syntax sugar for declaring ZAP devices. Use: ZAP/123 or ZAP[123] class ZAP def self.[](arg=nil) ZAP(arg) end def self./(arg=nil) ZAP(arg) end end # For users who may try to use the (proper) IAX2 form. See IAX for more info. class IAX2 < IAX; end # For users who may try to use the (common) "Zap" form. See ZAP for more info. class Zap < ZAP; end end # The PBX object is an object manifestation of the PBX with which Adhearsion will be # associated. Helpers often open up this class and add methods to it. Note: when doing # this in your own helpers, all methods will need to be class (a.k.a. static) methods # since no actual instance of this object is passed around. class PBX def PBX.io() Thread.current[:io] end end # The Contexts class is a blank slate in which the extensions.rb file is evaluated. # Its method_missing() simply takes a block and, given the name of attempted method, # meta_def()s a new accessor method in Contexts with this name that returns the block # given. This is how contexts can be included anywhere in extensions.rb using the # easy "+context_name" syntax. class Contexts (instance_methods - %w(__send__ __id__ define_method instance_eval)).each do |m| undef_method m end def method_missing name, *args, &block super(name, *args, &block) unless block Thread.current[:container].run_inside do meta_def name do block end end end # This Container object is a container in which each context is executed (not instantiated). # It extends the Asterisk module from adhearsion.rb and thus inherits all of the functionality # declared there. This class can be monkey patched if necessary. class Container include Asterisk def initialize class << self def metaclass; class << self; self; end; end def meta_eval &blk; metaclass.instance_eval(&blk); end def meta_def name, &blk meta_eval { define_method name, &blk } end end end def method_missing name, *args, &block Kernel.method_missing name, *args, &block end def run_inside &code instance_eval(&code) end def eval_inside code run_inside do eval code end end end end # An Exception thrown when the directory supplied in the constructor of a # new RailsApp object is invalid. class InvalidRailsDirectory < Exception;end # When instantiated with an absolute location to a Rails app, the new RailsApp will perform # a number of useful observations about the files and directories available, such as loading # the database configuration and listing all of the models. All observations are made accessible # through attribute accessors. class RailsApp def initialize path=Dir.pwd update! path end # Performs the observations on the Rails app once more. def update! path @path = path @database_config_file = File.join @path, 'config', 'database.yml' if File.readable? @database_config_file @database_config = YAML.load_file @database_config_file else raise InvalidRailsDirectory.new("Database config file #{@database_config_file} not found!") end @models_folder = File.join path, 'app', 'models' @models_files = Dir[ File.join(@models_folder, '*.rb') ] @time_updated = Time.now end attr_reader :database_config_file, :database_config, :path, :models_folder, :models_files, :models_names, :time_updated end