sant0sk1-adhearsion 0.7.999

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/LICENSE +456 -0
  2. data/README.txt +5 -0
  3. data/Rakefile +75 -0
  4. data/adhearsion.gemspec +136 -0
  5. data/app_generators/ahn/USAGE +5 -0
  6. data/app_generators/ahn/ahn_generator.rb +77 -0
  7. data/app_generators/ahn/templates/.ahnrc +36 -0
  8. data/app_generators/ahn/templates/README +8 -0
  9. data/app_generators/ahn/templates/Rakefile +18 -0
  10. data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
  11. data/app_generators/ahn/templates/components/simon_game/lib/simon_game.rb +61 -0
  12. data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +14 -0
  13. data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +31 -0
  14. data/app_generators/ahn/templates/config/startup.rb +53 -0
  15. data/app_generators/ahn/templates/dialplan.rb +4 -0
  16. data/bin/ahn +28 -0
  17. data/bin/ahnctl +68 -0
  18. data/bin/jahn +32 -0
  19. data/lib/adhearsion.rb +32 -0
  20. data/lib/adhearsion/blank_slate.rb +5 -0
  21. data/lib/adhearsion/cli.rb +106 -0
  22. data/lib/adhearsion/component_manager.rb +277 -0
  23. data/lib/adhearsion/core_extensions/all.rb +9 -0
  24. data/lib/adhearsion/core_extensions/array.rb +0 -0
  25. data/lib/adhearsion/core_extensions/custom_daemonizer.rb +45 -0
  26. data/lib/adhearsion/core_extensions/global.rb +1 -0
  27. data/lib/adhearsion/core_extensions/hash.rb +0 -0
  28. data/lib/adhearsion/core_extensions/metaprogramming.rb +17 -0
  29. data/lib/adhearsion/core_extensions/numeric.rb +4 -0
  30. data/lib/adhearsion/core_extensions/proc.rb +0 -0
  31. data/lib/adhearsion/core_extensions/publishable.rb +73 -0
  32. data/lib/adhearsion/core_extensions/relationship_properties.rb +40 -0
  33. data/lib/adhearsion/core_extensions/string.rb +26 -0
  34. data/lib/adhearsion/core_extensions/thread.rb +13 -0
  35. data/lib/adhearsion/core_extensions/thread_safety.rb +7 -0
  36. data/lib/adhearsion/core_extensions/time.rb +0 -0
  37. data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
  38. data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
  39. data/lib/adhearsion/distributed/gateways/rest_gateway.rb +9 -0
  40. data/lib/adhearsion/distributed/gateways/soap_gateway.rb +9 -0
  41. data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +9 -0
  42. data/lib/adhearsion/distributed/peer_finder.rb +0 -0
  43. data/lib/adhearsion/distributed/remote_cli.rb +0 -0
  44. data/lib/adhearsion/events_support.rb +26 -0
  45. data/lib/adhearsion/hooks.rb +57 -0
  46. data/lib/adhearsion/host_definitions.rb +63 -0
  47. data/lib/adhearsion/initializer.rb +246 -0
  48. data/lib/adhearsion/initializer/asterisk.rb +59 -0
  49. data/lib/adhearsion/initializer/configuration.rb +236 -0
  50. data/lib/adhearsion/initializer/database.rb +49 -0
  51. data/lib/adhearsion/initializer/drb.rb +25 -0
  52. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  53. data/lib/adhearsion/initializer/rails.rb +40 -0
  54. data/lib/adhearsion/logging.rb +92 -0
  55. data/lib/adhearsion/tasks.rb +15 -0
  56. data/lib/adhearsion/tasks/database.rb +5 -0
  57. data/lib/adhearsion/tasks/generating.rb +20 -0
  58. data/lib/adhearsion/tasks/lint.rb +4 -0
  59. data/lib/adhearsion/tasks/testing.rb +37 -0
  60. data/lib/adhearsion/version.rb +9 -0
  61. data/lib/adhearsion/voip/asterisk.rb +10 -0
  62. data/lib/adhearsion/voip/asterisk/agi_server.rb +81 -0
  63. data/lib/adhearsion/voip/asterisk/ami.rb +147 -0
  64. data/lib/adhearsion/voip/asterisk/ami/actions.rb +238 -0
  65. data/lib/adhearsion/voip/asterisk/ami/machine.rb +871 -0
  66. data/lib/adhearsion/voip/asterisk/ami/machine.rl +109 -0
  67. data/lib/adhearsion/voip/asterisk/ami/parser.rb +262 -0
  68. data/lib/adhearsion/voip/asterisk/commands.rb +1284 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  71. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  72. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  73. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  74. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  75. data/lib/adhearsion/voip/call.rb +436 -0
  76. data/lib/adhearsion/voip/call_routing.rb +64 -0
  77. data/lib/adhearsion/voip/commands.rb +9 -0
  78. data/lib/adhearsion/voip/constants.rb +39 -0
  79. data/lib/adhearsion/voip/conveniences.rb +18 -0
  80. data/lib/adhearsion/voip/dial_plan.rb +207 -0
  81. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  82. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  83. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  84. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  85. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  86. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  87. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  88. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  89. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  90. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  91. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  92. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  93. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  94. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  95. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  96. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  97. metadata +167 -0
@@ -0,0 +1,240 @@
1
+ require File.join(File.dirname(__FILE__), 'config_generator')
2
+
3
+ module Adhearsion
4
+ module VoIP
5
+ module Asterisk
6
+ module ConfigFileGenerators
7
+ class Voicemail < AsteriskConfigGenerator
8
+
9
+ DEFAULT_GENERAL_SECTION = {
10
+ :format => :wav
11
+ }
12
+
13
+ # Don't worry. These will be overridable soon.
14
+ STATIC_ZONEMESSAGES_CONTEXT = %{
15
+ [zonemessages]
16
+ eastern=America/New_York|'vm-received' Q 'digits/at' IMp
17
+ central=America/Chicago|'vm-received' Q 'digits/at' IMp
18
+ central24=America/Chicago|'vm-received' q 'digits/at' H N 'hours'
19
+ military=Zulu|'vm-received' q 'digits/at' H N 'hours' 'phonetic/z_p'
20
+ european=Europe/Copenhagen|'vm-received' a d b 'digits/at' HM
21
+ }.unindent
22
+
23
+ attr_reader :properties, :context_definitions
24
+ def initialize
25
+ @properties = DEFAULT_GENERAL_SECTION.clone
26
+ @mailboxes = {}
27
+ @context_definitions = []
28
+ super
29
+ end
30
+
31
+ def context(name)
32
+ raise ArgumentError, "Name cannot be 'general'!" if name.to_s.downcase == 'general'
33
+ raise ArgumentError, "A name can only be characters, numbers, and underscores!" if name.to_s !~ /^[\w_]+$/
34
+
35
+ returning ContextDefinition.new(name) do |context_definition|
36
+ yield context_definition
37
+ context_definitions << context_definition
38
+ end
39
+ end
40
+
41
+ def greeting_maximum(seconds)
42
+ int "maxgreet" => seconds
43
+ end
44
+
45
+ def execute_on_pin_change(command)
46
+ string "externpass" => command
47
+ end
48
+
49
+ def recordings
50
+ @recordings ||= RecordingDefinition.new
51
+ yield @recordings if block_given?
52
+ @recordings
53
+ end
54
+
55
+ def emails
56
+ @emails ||= EmailDefinition.new
57
+ if block_given?
58
+ yield @emails
59
+ else
60
+ @emails
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ email_properties = @emails ? @emails.properties : {}
66
+ AsteriskConfigGenerator.warning_message +
67
+ "[general]\n" +
68
+ properties.merge(email_properties).map { |(key,value)| "#{key}=#{value}" }.sort.join("\n") + "\n\n" +
69
+ STATIC_ZONEMESSAGES_CONTEXT +
70
+ context_definitions.map(&:to_s).join("\n\n")
71
+ end
72
+
73
+ private
74
+
75
+ class ContextDefinition < AsteriskConfigGenerator
76
+
77
+ attr_reader :mailboxes
78
+ def initialize(name)
79
+ @name = name
80
+ @mailboxes = []
81
+ super()
82
+ end
83
+
84
+ # TODO: This will hold a lot of the methods from the [general] section!
85
+
86
+ def to_s
87
+ (%W[[#@name]] + mailboxes.map(&:to_s)).join "\n"
88
+ end
89
+
90
+ def mailbox(mailbox_number)
91
+ box = MailboxDefinition.new(mailbox_number)
92
+ yield box
93
+ mailboxes << box
94
+ end
95
+
96
+ private
97
+
98
+ def mailbox_entry(options)
99
+ returning MailboxDefinition.new do |mailbox|
100
+ yield mailbox if block_given?
101
+ mailboxes << definition
102
+ end
103
+ end
104
+
105
+ class MailboxDefinition
106
+
107
+ attr_reader :mailbox_number
108
+ def initialize(mailbox_number)
109
+ check_numeric mailbox_number
110
+ @mailbox_number = mailbox_number
111
+ @definition = {}
112
+ super()
113
+ end
114
+
115
+ def pin_number(number)
116
+ check_numeric number
117
+ @definition[:pin_number] = number
118
+ end
119
+
120
+ def name(str)
121
+ @definition[:name] = str
122
+ end
123
+
124
+ def email(str)
125
+ @definition[:email] = str
126
+ end
127
+
128
+ def to_hash
129
+ @definition
130
+ end
131
+
132
+ def to_s
133
+ %(#{mailbox_number} => #{@definition[:pin_number]},#{@definition[:name]},#{@definition[:email]})[/^(.+?),*$/,1]
134
+ end
135
+
136
+ private
137
+
138
+ def check_numeric(number)
139
+ raise ArgumentError, number.inspect + " is not numeric!" unless number.to_s =~ /^\d+$/
140
+ end
141
+
142
+ end
143
+ end
144
+
145
+ class EmailDefinition < AsteriskConfigGenerator
146
+ EMAIL_VARIABLE_CONVENIENCES = {
147
+ :name => '${VM_NAME}',
148
+ :duration => '${VM_DUR}',
149
+ :message_number => '${VM_MSGNUM}',
150
+ :mailbox => '${VM_MAILBOX}',
151
+ :caller_id => '${VM_CALLERID}',
152
+ :date => '${VM_DATE}',
153
+ :caller_id_number => '${VM_CIDNUM}',
154
+ :caller_id_name => '${VM_CIDNAME}'
155
+ }
156
+
157
+ attr_reader :properties
158
+ def initialize
159
+ @properties = {}
160
+ super
161
+ end
162
+
163
+ def [](email_variable)
164
+ if EMAIL_VARIABLE_CONVENIENCES.has_key? email_variable
165
+ EMAIL_VARIABLE_CONVENIENCES[email_variable]
166
+ else
167
+ raise ArgumentError, "Unrecognized variable #{email_variable.inspect}"
168
+ end
169
+ end
170
+
171
+ def disable!
172
+ raise NotImpementedError
173
+ end
174
+
175
+ def from(options)
176
+ name, email = options.values_at :name, :email
177
+ string :serveremail => email
178
+ string :fromstring => name
179
+ end
180
+
181
+ def attach_recordings(true_or_false)
182
+ boolean :attach => true_or_false
183
+ end
184
+
185
+ def attach_recordings?
186
+ properties[:attach] == 'yes'
187
+ end
188
+
189
+ def body(str)
190
+ str = str.gsub("\r", '').gsub("\n", '\n')
191
+ if str.length > 512
192
+ raise ArgumentError, "Asterisk has an email body limit of 512 characters! Your body is too long!\n" +
193
+ ("-" * 10) + "\n" + str
194
+ end
195
+ string :emailbody => str
196
+ end
197
+
198
+ def subject(str)
199
+ string :emailsubject => str
200
+ end
201
+
202
+ def command(cmd)
203
+ string :mailcmd => cmd
204
+ end
205
+
206
+ end
207
+
208
+ class RecordingDefinition < AsteriskConfigGenerator
209
+
210
+ attr_reader :properties
211
+ def initialize
212
+ @properties = {}
213
+ super
214
+ end
215
+
216
+ def format(symbol)
217
+ one_of [:gsm, :wav49, :wav], :format => symbol
218
+ end
219
+
220
+ def allowed_length(seconds)
221
+ case seconds
222
+ when Fixnum
223
+ int :maxmessage => "value"
224
+ when Range
225
+ int :minmessage => seconds.first
226
+ int :maxmessage => seconds.last
227
+ else
228
+ raise ArgumentError, "Argument must be a Fixnum or Range!"
229
+ end
230
+ end
231
+
232
+ def maximum_silence(seconds)
233
+ int :maxsilence => seconds
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,71 @@
1
+ require 'enumerator'
2
+ module Adhearsion
3
+ module VoIP
4
+ module Asterisk
5
+ class ConfigurationManager
6
+
7
+ class << self
8
+ def normalize_configuration(file_contents)
9
+ # cat sip.conf | sed -e 's/\s*;.*$//g' | sed -e '/^;.*$/d' | sed -e '/^\s*$/d'
10
+ file_contents.split(/\n+/).map do |line|
11
+ line.sub(/;.+$/, '').strip
12
+ end.join("\n").squeeze("\n")
13
+ end
14
+ end
15
+
16
+ attr_reader :filename
17
+
18
+ def initialize(filename)
19
+ @filename = filename
20
+ end
21
+
22
+ def sections
23
+ @sections ||= read_configuration
24
+ end
25
+
26
+ def [](section_name)
27
+ result = sections.find { |(name, *rest)| section_name == name }
28
+ result.last if result
29
+ end
30
+
31
+ def delete_section(section_name)
32
+ sections.reject! { |(name, *rest)| section_name == name }
33
+ end
34
+
35
+ def new_section(name, properties={})
36
+ sections << [name, properties]
37
+ end
38
+
39
+ private
40
+
41
+ def read_configuration
42
+ normalized_file = self.class.normalize_configuration execute(read_command)
43
+ normalized_file.split(/^\[([-_\w]+)\]$/)[1..-1].enum_slice(2).map do |(name,properties)|
44
+ [name, hash_from_properties(properties)]
45
+ end
46
+ end
47
+
48
+ def hash_from_properties(properties)
49
+ properties.split(/\n+/).inject({}) do |property_hash,property|
50
+ all, name, value = *property.match(/^\s*([^=]+?)\s*=\s*(.+)\s*$/)
51
+ next property_hash unless name && value
52
+ property_hash[name] = value
53
+ property_hash
54
+ end
55
+ end
56
+
57
+ def execute(command)
58
+ %x[command]
59
+ end
60
+
61
+ def read_command
62
+ "cat #{filename}"
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Read a file: cat a file
71
+ # Parse a file: separate into a two dimensional hash
@@ -0,0 +1,80 @@
1
+ module Adhearsion
2
+ class DialPlan
3
+ class ConfirmationManager
4
+
5
+ class << self
6
+
7
+ def encode_hash_for_dial_macro_argument(options)
8
+ options = options.clone
9
+ macro_name = options.delete :macro
10
+ options[:play] &&= options[:play].kind_of?(Array) ? options[:play].join('++') : options[:play]
11
+ encoded_options = URI.escape options.map { |key,value| "#{key}:#{value}" }.join('!')
12
+ returning "M(#{macro_name}^#{encoded_options})" do |str|
13
+ if str.rindex('^') != str.index('^')
14
+ raise ArgumentError, "You seem to have supplied a :confirm option with a caret (^) in it!" +
15
+ " Please remove it. This will blow Asterisk up."
16
+ end
17
+ end
18
+ end
19
+
20
+ def handle(call)
21
+ new(call).handle
22
+ end
23
+
24
+ def confirmation_call?(call)
25
+ call.variables.has_key?(:network_script) && call.variables[:network_script].starts_with?('confirm!')
26
+ end
27
+
28
+ def decode_hash(encoded_hash)
29
+ encoded_hash = encoded_hash =~ /^M\((.+)\)$/ ? $1 : encoded_hash
30
+ encoded_hash = encoded_hash =~ /^([^:]+\^)?(.+)$/ ? $2 : encoded_hash # Remove the macro name if it's there
31
+ unencoded = URI.unescape(encoded_hash).split('!')
32
+ unencoded.shift unless unencoded.first.include?(':')
33
+ unencoded = unencoded.map { |pair| key, value = pair.split(':'); [key.to_sym ,value] }.flatten
34
+ returning Hash[*unencoded] do |hash|
35
+ hash[:timeout] &&= hash[:timeout].to_i
36
+ hash[:play] &&= hash[:play].split('++')
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ attr_reader :call
43
+ def initialize(call)
44
+ @call = call
45
+ extend Adhearsion::VoIP::Commands.for(call.originating_voip_platform)
46
+ end
47
+
48
+ def handle
49
+ variables = self.class.decode_hash call.variables[:network_script]
50
+
51
+ answer
52
+ loop do
53
+ response = interruptable_play(*variables[:play])
54
+ if response && response.to_s == variables[:key].to_s
55
+ # Don't set a variable to pass through to dial()
56
+ break
57
+ elsif response && response.to_s != variables[:key].to_s
58
+ next
59
+ else
60
+ response = wait_for_digit variables[:timeout]
61
+ if response
62
+ if response.to_s == variables[:key].to_s
63
+ # Don't set a variable to pass through to dial()
64
+ break
65
+ else
66
+ next
67
+ end
68
+ else
69
+ # By setting MACRO_RESULT to CONTINUE, we cancel the dial.
70
+ variable 'MACRO_RESULT' => "CONTINUE"
71
+ break
72
+ end
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,436 @@
1
+ require 'uri'
2
+ #TODO Some of this is asterisk-specific
3
+ module Adhearsion
4
+ class << self
5
+ def active_calls
6
+ @calls ||= Calls.new
7
+ end
8
+
9
+ def receive_call_from(io, &block)
10
+ active_calls << (call = Call.receive_from(io, &block))
11
+ call
12
+ end
13
+
14
+ def remove_inactive_call(call)
15
+ active_calls.remove_inactive_call(call)
16
+ end
17
+ end
18
+
19
+ ##
20
+ # This manages the list of calls the Adhearsion service receives
21
+ class Calls
22
+ def initialize
23
+ @semaphore = Monitor.new
24
+ @calls = {}
25
+ end
26
+
27
+ def <<(call)
28
+ atomically do
29
+ calls[call.unique_identifier] = call
30
+ end
31
+ end
32
+
33
+ def any?
34
+ atomically do
35
+ !calls.empty?
36
+ end
37
+ end
38
+
39
+ def size
40
+ atomically do
41
+ calls.size
42
+ end
43
+ end
44
+
45
+ def remove_inactive_call(call)
46
+ atomically do
47
+ calls.delete call.unique_identifier
48
+ end
49
+ end
50
+
51
+ # Searches all active calls by their unique_identifier. See Call#unique_identifier.
52
+ def find(id)
53
+ atomically do
54
+ return calls[id]
55
+ end
56
+ end
57
+
58
+ def clear!
59
+ atomically do
60
+ calls.clear
61
+ end
62
+ end
63
+
64
+ def with_tag(tag)
65
+ atomically do
66
+ calls.inject(Array.new) do |calls_with_tag,(key,call)|
67
+ call.tagged_with?(tag) ? calls_with_tag << call : calls_with_tag
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+ attr_reader :semaphore, :calls
74
+
75
+ def atomically(&block)
76
+ semaphore.synchronize(&block)
77
+ end
78
+
79
+ end
80
+
81
+ class UselessCallException < Exception; end
82
+
83
+ class MetaAgiCallException < Exception
84
+ attr_reader :call
85
+ def initialize(call)
86
+ super()
87
+ @call = call
88
+ end
89
+ end
90
+
91
+ class FailedExtensionCallException < MetaAgiCallException; end
92
+
93
+ class HungupExtensionCallException < MetaAgiCallException; end
94
+
95
+ ##
96
+ # Encapsulates call-related data and behavior.
97
+ # For example, variables passed in on call initiation are
98
+ # accessible here as attributes
99
+ class Call
100
+
101
+ # This is basically a translation of ast_channel_reason2str() from main/channel.c and
102
+ # ast_control_frame_type in include/asterisk/frame.h in the Asterisk source code. When
103
+ # Asterisk jumps to the 'failed' extension, it sets a REASON channel variable to a number.
104
+ # The indexes of these symbols represent the possible numbers REASON could be.
105
+ ASTERISK_FRAME_STATES = [
106
+ :failure, # "Call Failure (not BUSY, and not NO_ANSWER, maybe Circuit busy or down?)"
107
+ :hangup, # Other end has hungup
108
+ :ring, # Local ring
109
+ :ringing, # Remote end is ringing
110
+ :answer, # Remote end has answered
111
+ :busy, # Remote end is busy
112
+ :takeoffhook, # Make it go off hook
113
+ :offhook, # Line is off hook
114
+ :congestion, # Congestion (circuits busy)
115
+ :flash, # Flash hook
116
+ :wink, # Wink
117
+ :option, # Set a low-level option
118
+ :radio_key, # Key Radio
119
+ :radio_unkey, # Un-Key Radio
120
+ :progress, # Indicate PROGRESS
121
+ :proceeding, # Indicate CALL PROCEEDING
122
+ :hold, # Indicate call is placed on hold
123
+ :unhold, # Indicate call is left from hold
124
+ :vidupdate # Indicate video frame update
125
+ ]
126
+
127
+
128
+ class << self
129
+ ##
130
+ # The primary public interface for creating a Call instance.
131
+ # Given an IO (probably a socket accepted from an Asterisk service),
132
+ # creates a Call instance which encapsulates everything we know about that call.
133
+ def receive_from(io, &block)
134
+ returning new(io, variable_parser_for(io).variables) do |call|
135
+ block.call(call) if block
136
+ end
137
+ end
138
+
139
+ private
140
+ def variable_parser_for(io)
141
+ Variables::Parser.parse(io)
142
+ end
143
+
144
+ end
145
+
146
+ attr_accessor :io, :type, :variables, :originating_voip_platform, :inbox
147
+ def initialize(io, variables)
148
+ @io, @variables = io, variables.symbolize_keys
149
+ check_if_valid_call
150
+ define_variable_accessors
151
+ set_originating_voip_platform!
152
+ @tag_mutex = Mutex.new
153
+ @tags = []
154
+ end
155
+
156
+ def tags
157
+ @tag_mutex.synchronize do
158
+ return @tags.clone
159
+ end
160
+ end
161
+
162
+ def tag(symbol)
163
+ raise ArgumentError, "tag must be a Symbol" unless symbol.is_a? Symbol
164
+ @tag_mutex.synchronize do
165
+ @tags << symbol
166
+ end
167
+ end
168
+
169
+ def remove_tag(symbol)
170
+ @tag_mutex.synchronize do
171
+ @tags.reject! { |tag| tag == symbol }
172
+ end
173
+ end
174
+
175
+ def tagged_with?(symbol)
176
+ @tag_mutex.synchronize do
177
+ @tags.include? symbol
178
+ end
179
+ end
180
+
181
+ def deliver_message(message)
182
+ inbox << message
183
+ end
184
+ alias << deliver_message
185
+
186
+ def inbox
187
+ @inbox ||= Queue.new
188
+ end
189
+
190
+ def hangup!
191
+ io.close
192
+ Adhearsion.remove_inactive_call self
193
+ end
194
+
195
+ def closed?
196
+ io.closed?
197
+ end
198
+
199
+ # Asterisk sometimes uses the "failed" extension to indicate a failed dial attempt.
200
+ # Since it may be important to handle these, this flag helps the dialplan Manager
201
+ # figure that out.
202
+ def failed_call?
203
+ @failed_call
204
+ end
205
+
206
+ def hungup_call?
207
+ @hungup_call
208
+ end
209
+
210
+ # Adhearsion indexes calls by this identifier so they may later be found and manipulated. For calls from Asterisk, this
211
+ # method uses the following properties for uniqueness, falling back to the next if one is for some reason unavailable:
212
+ #
213
+ # Asterisk channel ID -> unique ID -> Call#object_id
214
+ # (e.g. SIP/mytrunk-jb12c88a) -> (e.g. 1215039989.47033) -> (e.g. 2792080)
215
+ #
216
+ # Note: channel is used over unique ID because channel may be used to bridge two channels together.
217
+ def unique_identifier
218
+ case originating_voip_platform
219
+ when :asterisk
220
+ variables[:channel] || variables[:uniqueid] || object_id
221
+ else
222
+ raise NotImplementedError
223
+ end
224
+ end
225
+
226
+ def define_variable_accessors(recipient=self)
227
+ variables.each do |key, value|
228
+ define_singleton_accessor_with_pair(key, value, recipient)
229
+ end
230
+ end
231
+
232
+ def extract_failed_reason_from(environment)
233
+ if originating_voip_platform == :asterisk
234
+ failed_reason = environment.variable 'REASON'
235
+ failed_reason &&= ASTERISK_FRAME_STATES[failed_reason.to_i]
236
+ define_singleton_accessor_with_pair(:failed_reason, failed_reason, environment)
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ def define_singleton_accessor_with_pair(key, value, recipient=self)
243
+ recipient.metaclass.send :attr_accessor, key unless recipient.class.respond_to?("#{key}=")
244
+ recipient.send "#{key}=", value
245
+ end
246
+
247
+ def check_if_valid_call
248
+ extension = variables[:extension]
249
+ @failed_call = true if extension == 'failed'
250
+ @hungup_call = true if extension == 'h'
251
+ raise UselessCallException if extension == 't' # TODO: Move this whole method to Manager
252
+ end
253
+
254
+ def set_originating_voip_platform!
255
+ # TODO: we can make this determination programatically at some point,
256
+ # but it will probably involve a bit more engineering than just a case statement (like
257
+ # subclasses of Call for the various platforms), so we'll be totally cheap for now.
258
+ self.originating_voip_platform = :asterisk
259
+ end
260
+
261
+ module Variables
262
+
263
+ module Coercions
264
+
265
+ COERCION_ORDER = %w{
266
+ remove_agi_prefixes_from_keys_and_strip_whitespace
267
+ coerce_keys_into_symbols
268
+ coerce_extension_into_phone_number_object
269
+ coerce_numerical_values_to_numerics
270
+ replace_unknown_values_with_nil
271
+ replace_yes_no_answers_with_booleans
272
+ coerce_request_into_uri_object
273
+ decompose_uri_query_into_hash
274
+ override_variables_with_query_params
275
+ remove_dashes_from_context_name
276
+ coerce_type_of_number_into_symbol
277
+ }
278
+
279
+ class << self
280
+
281
+ def remove_agi_prefixes_from_keys_and_strip_whitespace(variables)
282
+ variables.inject({}) do |new_variables,(key,value)|
283
+ returning new_variables do
284
+ stripped_name = key.kind_of?(String) ? key[/^(agi_)?(.+)$/,2] : key
285
+ new_variables[stripped_name] = value.kind_of?(String) ? value.strip : value
286
+ end
287
+ end
288
+ end
289
+
290
+ def coerce_keys_into_symbols(variables)
291
+ variables.inject({}) do |new_variables,(key,value)|
292
+ returning new_variables do
293
+ new_variables[key.to_sym] = value
294
+ end
295
+ end
296
+ end
297
+
298
+ def coerce_extension_into_phone_number_object(variables)
299
+ returning variables do
300
+ variables[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new(variables[:extension])
301
+ end
302
+ end
303
+
304
+ def coerce_numerical_values_to_numerics(variables)
305
+ variables.inject({}) do |vars,(key,value)|
306
+ returning vars do
307
+ is_numeric = value =~ /^-?\d+(?:(\.)\d+)?$/
308
+ is_float = $1
309
+ vars[key] = if is_numeric
310
+ if Adhearsion::VoIP::DSL::NumericalString.starts_with_leading_zero?(value)
311
+ Adhearsion::VoIP::DSL::NumericalString.new(value)
312
+ else
313
+ is_float ? value.to_f : value.to_i
314
+ end
315
+ else
316
+ value
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ def replace_unknown_values_with_nil(variables)
323
+ variables.each do |key,value|
324
+ variables[key] = nil if value == 'unknown'
325
+ end
326
+ end
327
+
328
+ def replace_yes_no_answers_with_booleans(variables)
329
+ variables.each do |key,value|
330
+ case value
331
+ when 'yes' : variables[key] = true
332
+ when 'no' : variables[key] = false
333
+ end
334
+ end
335
+ end
336
+
337
+ def coerce_request_into_uri_object(variables)
338
+ returning variables do
339
+ variables[:request] = URI.parse(variables[:request]) unless variables[:request].kind_of? URI
340
+ end
341
+ end
342
+
343
+ def coerce_type_of_number_into_symbol(variables)
344
+ returning variables do
345
+ variables[:type_of_calling_number] = Adhearsion::VoIP::Constants::Q931_TYPE_OF_NUMBER[variables.delete(:callington).to_i]
346
+ end
347
+ end
348
+
349
+ def decompose_uri_query_into_hash(variables)
350
+ returning variables do
351
+ if variables[:request].query
352
+ variables[:query] = variables[:request].query.split('&').inject({}) do |query_string_parameters, key_value_pair|
353
+ parameter_name, parameter_value = *key_value_pair.match(/(.+)=(.+)/).captures
354
+ query_string_parameters[parameter_name] = parameter_value
355
+ query_string_parameters
356
+ end
357
+ else
358
+ variables[:query] = {}
359
+ end
360
+ end
361
+ end
362
+
363
+ def override_variables_with_query_params(variables)
364
+ returning variables do
365
+ if variables[:query]
366
+ variables[:query].each do |key, value|
367
+ variables[key.to_sym] = value
368
+ end
369
+ end
370
+ end
371
+ end
372
+
373
+ def remove_dashes_from_context_name(variables)
374
+ returning variables do
375
+ variables[:context].gsub!('-', '_')
376
+ end
377
+ end
378
+
379
+ end
380
+ end
381
+
382
+ class Parser
383
+
384
+ class << self
385
+ def parse(*args, &block)
386
+ returning new(*args, &block) do |parser|
387
+ parser.parse
388
+ end
389
+ end
390
+
391
+ def coerce_variables(variables)
392
+ Coercions::COERCION_ORDER.inject(variables) do |tmp_variables, coercing_method_name|
393
+ Coercions.send(coercing_method_name, tmp_variables)
394
+ end
395
+ end
396
+
397
+ def separate_line_into_key_value_pair(line)
398
+ line.match(/^([^:]+):\s?(.+)/).captures
399
+ end
400
+ end
401
+
402
+ attr_reader :io, :variables, :lines
403
+ def initialize(io)
404
+ @io = io
405
+ @lines = []
406
+ end
407
+
408
+ def parse
409
+ extract_variable_lines_from_io
410
+ initialize_variables_as_hash_from_lines
411
+ @variables = self.class.coerce_variables(variables)
412
+ end
413
+
414
+ private
415
+
416
+ def initialize_variables_as_hash_from_lines
417
+ @variables = lines.inject({}) do |new_variables,line|
418
+ returning new_variables do
419
+ key, value = self.class.separate_line_into_key_value_pair line
420
+ new_variables[key] = value
421
+ end
422
+ end
423
+ end
424
+
425
+ def extract_variable_lines_from_io
426
+ while line = io.readline.chomp
427
+ break if line.empty?
428
+ @lines << line
429
+ end
430
+ end
431
+
432
+ end
433
+
434
+ end
435
+ end
436
+ end