mtrudel-adhearsion 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/EVENTS +11 -0
- data/LICENSE +456 -0
- data/Rakefile +127 -0
- data/adhearsion.gemspec +149 -0
- data/app_generators/ahn/USAGE +5 -0
- data/app_generators/ahn/ahn_generator.rb +91 -0
- data/app_generators/ahn/templates/.ahnrc +34 -0
- data/app_generators/ahn/templates/README +8 -0
- data/app_generators/ahn/templates/Rakefile +25 -0
- data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
- data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown +11 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb +48 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +87 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml +34 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb +263 -0
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +104 -0
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +2 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml +12 -0
- data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
- data/app_generators/ahn/templates/config/startup.rb +50 -0
- data/app_generators/ahn/templates/dialplan.rb +3 -0
- data/app_generators/ahn/templates/events.rb +32 -0
- data/bin/ahn +28 -0
- data/bin/ahnctl +68 -0
- data/bin/jahn +42 -0
- data/examples/asterisk_manager_interface/standalone.rb +51 -0
- data/lib/adhearsion.rb +37 -0
- data/lib/adhearsion/cli.rb +223 -0
- data/lib/adhearsion/component_manager.rb +207 -0
- data/lib/adhearsion/component_manager/component_tester.rb +55 -0
- data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
- data/lib/adhearsion/events_support.rb +84 -0
- data/lib/adhearsion/foundation/all.rb +9 -0
- data/lib/adhearsion/foundation/blank_slate.rb +5 -0
- data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
- data/lib/adhearsion/foundation/event_socket.rb +203 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -0
- data/lib/adhearsion/foundation/global.rb +1 -0
- data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
- data/lib/adhearsion/foundation/numeric.rb +13 -0
- data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
- data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
- data/lib/adhearsion/foundation/string.rb +26 -0
- data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
- data/lib/adhearsion/foundation/thread_safety.rb +7 -0
- data/lib/adhearsion/host_definitions.rb +67 -0
- data/lib/adhearsion/initializer.rb +373 -0
- data/lib/adhearsion/initializer/asterisk.rb +81 -0
- data/lib/adhearsion/initializer/configuration.rb +254 -0
- data/lib/adhearsion/initializer/database.rb +50 -0
- data/lib/adhearsion/initializer/drb.rb +31 -0
- data/lib/adhearsion/initializer/freeswitch.rb +22 -0
- data/lib/adhearsion/initializer/rails.rb +41 -0
- data/lib/adhearsion/logging.rb +92 -0
- data/lib/adhearsion/tasks.rb +16 -0
- data/lib/adhearsion/tasks/database.rb +5 -0
- data/lib/adhearsion/tasks/deprecations.rb +59 -0
- data/lib/adhearsion/tasks/generating.rb +20 -0
- data/lib/adhearsion/tasks/lint.rb +4 -0
- data/lib/adhearsion/tasks/testing.rb +37 -0
- data/lib/adhearsion/version.rb +9 -0
- data/lib/adhearsion/voip/asterisk.rb +4 -0
- data/lib/adhearsion/voip/asterisk/agi_server.rb +84 -0
- data/lib/adhearsion/voip/asterisk/commands.rb +1314 -0
- data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
- data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
- data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
- data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
- data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +597 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1589 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
- data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
- data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
- data/lib/adhearsion/voip/call.rb +453 -0
- data/lib/adhearsion/voip/call_routing.rb +64 -0
- data/lib/adhearsion/voip/commands.rb +9 -0
- data/lib/adhearsion/voip/constants.rb +39 -0
- data/lib/adhearsion/voip/conveniences.rb +18 -0
- data/lib/adhearsion/voip/dial_plan.rb +218 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
- data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
- data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
- data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
- data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
- data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
- data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
- data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
- data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
- data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
- data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
- data/lib/theatre.rb +151 -0
- data/lib/theatre/README.markdown +64 -0
- data/lib/theatre/callback_definition_loader.rb +84 -0
- data/lib/theatre/guid.rb +23 -0
- data/lib/theatre/invocation.rb +121 -0
- data/lib/theatre/namespace_manager.rb +153 -0
- data/lib/theatre/version.rb +2 -0
- metadata +182 -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,597 @@
|
|
1
|
+
require 'adhearsion/voip/asterisk/manager_interface/ami_lexer'
|
2
|
+
|
3
|
+
module Adhearsion
|
4
|
+
module VoIP
|
5
|
+
module Asterisk
|
6
|
+
|
7
|
+
##
|
8
|
+
# Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for
|
9
|
+
# documentation on the new way of handling AMI. This new version is much better and should not require an enormous
|
10
|
+
# migration on your part.
|
11
|
+
#
|
12
|
+
class AMI
|
13
|
+
def initialize
|
14
|
+
raise "Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for documentation on the new way of handling AMI. This new version is much better and should not require an enormous migration on your part."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
mattr_accessor :manager_interface
|
19
|
+
|
20
|
+
module Manager
|
21
|
+
|
22
|
+
##
|
23
|
+
# This class abstracts a connection to the Asterisk Manager Interface. Its purpose is, first and foremost, to make
|
24
|
+
# the protocol consistent. Though the classes employed to assist this class (ManagerInterfaceAction,
|
25
|
+
# ManagerInterfaceResponse, ManagerInterfaceError, etc.) are relatively user-friendly, they're designed to be a
|
26
|
+
# building block on which to build higher-level abstractions of the Asterisk Manager Interface.
|
27
|
+
#
|
28
|
+
# For a higher-level abstraction of the Asterisk Manager Interface, see the SuperManager class.
|
29
|
+
#
|
30
|
+
class ManagerInterface
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
def connect(*args)
|
35
|
+
returning new(*args) do |connection|
|
36
|
+
connection.connect!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def replies_with_action_id?(name, headers={})
|
41
|
+
name = name.to_s.downcase
|
42
|
+
# TODO: Expand this case statement
|
43
|
+
case name
|
44
|
+
when "queues", "iaxpeers"
|
45
|
+
false
|
46
|
+
else
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# When sending an action with "causal events" (i.e. events which must be collected to form a proper
|
53
|
+
# response), AMI should send a particular event which instructs us that no more events will be sent.
|
54
|
+
# This event is called the "causal event terminator".
|
55
|
+
#
|
56
|
+
# Note: you must supply both the name of the event and any headers because it's possible that some uses of an
|
57
|
+
# action (i.e. same name, different headers) have causal events while other uses don't.
|
58
|
+
#
|
59
|
+
# @param [String] name the name of the event
|
60
|
+
# @param [Hash] the headers associated with this event
|
61
|
+
# @return [String] the downcase()'d name of the event name for which to wait
|
62
|
+
#
|
63
|
+
def has_causal_events?(name, headers={})
|
64
|
+
name = name.to_s.downcase
|
65
|
+
case name
|
66
|
+
when "queuestatus", "sippeers", "parkedcalls", "status"
|
67
|
+
true
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Used to determine the event name for an action which has causal events.
|
75
|
+
#
|
76
|
+
# @param [String] action_name
|
77
|
+
# @return [String] The corresponding event name which signals the completion of the causal event sequence.
|
78
|
+
#
|
79
|
+
def causal_event_terminator_name_for(action_name)
|
80
|
+
return nil unless has_causal_events?(action_name)
|
81
|
+
action_name = action_name.to_s.downcase
|
82
|
+
case action_name
|
83
|
+
when "queuestatus", 'parkedcalls', "status"
|
84
|
+
action_name + "complete"
|
85
|
+
when "sippeers"
|
86
|
+
"peerlistcomplete"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
DEFAULT_SETTINGS = {
|
93
|
+
:host => "localhost",
|
94
|
+
:port => 5038,
|
95
|
+
:username => "admin",
|
96
|
+
:password => "secret",
|
97
|
+
:events => true
|
98
|
+
}.freeze unless defined? DEFAULT_SETTINGS
|
99
|
+
|
100
|
+
attr_reader *DEFAULT_SETTINGS.keys
|
101
|
+
|
102
|
+
##
|
103
|
+
# Creates a new Asterisk Manager Interface connection and exposes certain methods to control it. The constructor
|
104
|
+
# takes named parameters as Symbols. Note: if the :events option is given, this library will establish a separate
|
105
|
+
# socket for just events. Two sockets are used because some actions actually respond with events, making it very
|
106
|
+
# complicated to differentiate between response-type events and normal events.
|
107
|
+
#
|
108
|
+
# @param [Hash] options Available options are :host, :port, :username, :password, and :events
|
109
|
+
#
|
110
|
+
def initialize(options={})
|
111
|
+
options = parse_options options
|
112
|
+
|
113
|
+
@host = options[:host]
|
114
|
+
@username = options[:username]
|
115
|
+
@password = options[:password]
|
116
|
+
@port = options[:port]
|
117
|
+
@events = options[:events]
|
118
|
+
|
119
|
+
@sent_messages = {}
|
120
|
+
@sent_messages_lock = Mutex.new
|
121
|
+
|
122
|
+
@actions_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
123
|
+
:message_received => :action_message_received,
|
124
|
+
:error_received => :action_error_received
|
125
|
+
|
126
|
+
@write_queue = Queue.new
|
127
|
+
|
128
|
+
if @events
|
129
|
+
@events_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
130
|
+
:message_received => :event_message_received,
|
131
|
+
:error_received => :event_error_received
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def action_message_received(message)
|
136
|
+
if message.kind_of? Manager::ManagerInterfaceEvent
|
137
|
+
# Trigger the return value of the waiting action id...
|
138
|
+
corresponding_action = @current_action_with_causal_events
|
139
|
+
event_collection = @event_collection_for_current_action
|
140
|
+
|
141
|
+
if corresponding_action
|
142
|
+
|
143
|
+
# If this is the meta-event which signals no more events will follow and the response is complete.
|
144
|
+
if message.name.downcase == corresponding_action.causal_event_terminator_name
|
145
|
+
|
146
|
+
# Result found! Wake up any Threads waiting
|
147
|
+
corresponding_action.future_resource.resource = event_collection.freeze
|
148
|
+
|
149
|
+
@current_action_with_causal_events = nil
|
150
|
+
@event_collection_for_current_action = nil
|
151
|
+
|
152
|
+
else
|
153
|
+
event_collection << message
|
154
|
+
# We have more causal events coming.
|
155
|
+
end
|
156
|
+
else
|
157
|
+
ahn_log.ami.error "Got an unexpected event on actions socket! This may be a bug! #{message.inspect}"
|
158
|
+
end
|
159
|
+
|
160
|
+
elsif message["ActionID"].nil?
|
161
|
+
# No ActionID! Release the write lock and wake up the waiter
|
162
|
+
else
|
163
|
+
action_id = message["ActionID"]
|
164
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
165
|
+
if corresponding_action
|
166
|
+
message.action = corresponding_action
|
167
|
+
|
168
|
+
if corresponding_action.has_causal_events?
|
169
|
+
# By this point the write loop will already have started blocking by calling the response() method on the
|
170
|
+
# action. Because we must collect more events before we wake the write loop up again, let's create these
|
171
|
+
# instance variable which will needed when the subsequent causal events come in.
|
172
|
+
@current_action_with_causal_events = corresponding_action
|
173
|
+
@event_collection_for_current_action = []
|
174
|
+
else
|
175
|
+
# Wake any Threads waiting on the response.
|
176
|
+
corresponding_action.future_resource.resource = message
|
177
|
+
end
|
178
|
+
else
|
179
|
+
ahn_log.ami.error "Received an AMI message with an unrecognized ActionID!! This may be an bug! #{message.inspect}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def action_error_received(ami_error)
|
185
|
+
action_id = ami_error["ActionID"]
|
186
|
+
|
187
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
188
|
+
|
189
|
+
if corresponding_action
|
190
|
+
corresponding_action.future_resource.resource = ami_error
|
191
|
+
else
|
192
|
+
ahn_log.ami.error "Received an AMI error with an unrecognized ActionID!! This may be an bug! #{ami_error.inspect}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Called only when this ManagerInterface is instantiated with events enabled.
|
198
|
+
#
|
199
|
+
def event_message_received(event)
|
200
|
+
return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
|
201
|
+
# TODO: convert the event name to a certain namespace.
|
202
|
+
Events.trigger %w[asterisk manager_interface], event
|
203
|
+
end
|
204
|
+
|
205
|
+
def event_error_received(message)
|
206
|
+
# Does this ever even occur?
|
207
|
+
ahn_log.ami.error "Hmmm, got an error on the AMI events-only socket! This must be a bug! #{message.inspect}"
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Called when our Ragel parser encounters some unexpected syntax from Asterisk. Anytime this is called, it should
|
212
|
+
# be considered a bug in Adhearsion. Note: this same method is called regardless of whether the syntax error
|
213
|
+
# happened on the actions socket or on the events socket.
|
214
|
+
#
|
215
|
+
def syntax_error_encountered(ignored_chunk)
|
216
|
+
ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
|
217
|
+
"PLEASE REPORT THIS ON http://bugs.adhearsion.com! OFFENDING TEXT:\n#{ignored_chunk.inspect}"
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Must be called after instantiation. Also see ManagerInterface::connect().
|
222
|
+
#
|
223
|
+
# @raise [AuthenticationFailedException] if username or password are rejected
|
224
|
+
#
|
225
|
+
def connect!
|
226
|
+
establish_actions_connection
|
227
|
+
establish_events_connection if @events
|
228
|
+
self
|
229
|
+
end
|
230
|
+
|
231
|
+
def actions_connection_established
|
232
|
+
@actions_state = :connected
|
233
|
+
@actions_writer_thread = Thread.new(&method(:write_loop))
|
234
|
+
end
|
235
|
+
|
236
|
+
def actions_connection_disconnected
|
237
|
+
@actions_state = :disconnected
|
238
|
+
end
|
239
|
+
|
240
|
+
def events_connection_established
|
241
|
+
@events_state = :connected
|
242
|
+
end
|
243
|
+
|
244
|
+
def actions_connection_disconnected
|
245
|
+
@events_state = :disconnected
|
246
|
+
end
|
247
|
+
|
248
|
+
def disconnect!
|
249
|
+
# PSEUDO CODE
|
250
|
+
# TODO: Go through all the waiting condition variables and raise an exception
|
251
|
+
#@write_queue << :STOP!
|
252
|
+
raise NotImplementedError
|
253
|
+
end
|
254
|
+
|
255
|
+
def dynamic
|
256
|
+
# TODO: Return an object which responds to method_missing
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Used to directly send a new action to Asterisk. Note: NEVER supply an ActionID; these are handled internally.
|
261
|
+
#
|
262
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
263
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
264
|
+
# @return [FutureResource] Call resource() on this object if you wish to access the response (optional). Note: if the response has not come in yet, your Thread will wait until it does.
|
265
|
+
#
|
266
|
+
def send_action_asynchronously(action_name, headers={})
|
267
|
+
check_action_name action_name
|
268
|
+
action = ManagerInterfaceAction.new(action_name, headers)
|
269
|
+
if action.replies_with_action_id?
|
270
|
+
@write_queue << action
|
271
|
+
action
|
272
|
+
else
|
273
|
+
raise NotImplementedError
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Sends an action over the AMI connection and blocks your Thread until the response comes in. If there was an error
|
279
|
+
# for some reason, the error will be raised as an ManagerInterfaceError.
|
280
|
+
#
|
281
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
282
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
283
|
+
# @raise [ManagerInterfaceError] When Asterisk can't execute this action, it sends back an Error which is converted into an ManagerInterfaceError object and raised. Access ManagerInterfaceError#message for the reported message from Asterisk.
|
284
|
+
# @return [ManagerInterfaceResponse, ImmediateResponse] Contains the response from Asterisk and all headers
|
285
|
+
#
|
286
|
+
def send_action_synchronously(*args)
|
287
|
+
returning send_action_asynchronously(*args).response do |response|
|
288
|
+
raise response if response.kind_of?(ManagerInterfaceError)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
alias send_action send_action_synchronously
|
293
|
+
|
294
|
+
|
295
|
+
####### #######
|
296
|
+
########### ###########
|
297
|
+
################# SOON-DEPRECATED COMMANDS #################
|
298
|
+
########### ###########
|
299
|
+
####### #######
|
300
|
+
|
301
|
+
# ping sends an action to the Asterisk Manager Interface that returns a pong
|
302
|
+
# more details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Ping
|
303
|
+
def ping
|
304
|
+
deprecation_warning
|
305
|
+
send_action "Ping"
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
def deprecation_warning
|
310
|
+
ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
|
311
|
+
"and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
|
312
|
+
"SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
|
313
|
+
" See http://docs.adhearsion.com/AMI for more information."
|
314
|
+
end
|
315
|
+
|
316
|
+
# The originate method launches a call to Asterisk, full details here:
|
317
|
+
# http://www.voip-info.org/tiki-index.php?page=Asterisk+Manager+API+Action+Originate
|
318
|
+
# Takes these arguments as a hash:
|
319
|
+
#
|
320
|
+
# Channel: Channel on which to originate the call (The same as you specify in the Dial application command)
|
321
|
+
# Context: Context to use on connect (must use Exten & Priority with it)
|
322
|
+
# Exten: Extension to use on connect (must use Context & Priority with it)
|
323
|
+
# Priority: Priority to use on connect (must use Context & Exten with it)
|
324
|
+
# Timeout: Timeout (in milliseconds) for the originating connection to happen(defaults to 30000 milliseconds)
|
325
|
+
# CallerID: CallerID to use for the call
|
326
|
+
# Variable: Channels variables to set (max 32). Variables will be set for both channels (local and connected).
|
327
|
+
# Account: Account code for the call
|
328
|
+
# Application: Application to use on connect (use Data for parameters)
|
329
|
+
# Data : Data if Application parameter is used
|
330
|
+
# Async: For the origination to be asynchronous (allows multiple calls to be generated without waiting for a response)
|
331
|
+
# ActionID: The request identifier. It allows you to identify the response to this request.
|
332
|
+
# You may use a number or a string. Useful when you make several simultaneous requests.
|
333
|
+
#
|
334
|
+
# For example:
|
335
|
+
# originate { :channel => 'SIP/1000@sipnetworks.com',
|
336
|
+
# :context => 'my_context',
|
337
|
+
# :exten => 's',
|
338
|
+
# :priority => '1' }
|
339
|
+
def originate(options={})
|
340
|
+
deprecation_warning
|
341
|
+
options = options.clone
|
342
|
+
options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
|
343
|
+
options[:exten] = options.delete :extension if options.has_key? :extension
|
344
|
+
send_action "Originate", options
|
345
|
+
end
|
346
|
+
|
347
|
+
# An introduction connects two endpoints together. The first argument is
|
348
|
+
# the first person the PBX will call. When she's picked up, Asterisk will
|
349
|
+
# play ringing while the second person is being dialed.
|
350
|
+
#
|
351
|
+
# The first argument is the person called first. Pass this as a canonical
|
352
|
+
# IAX2/server/user type argument. Destination takes the same format, but
|
353
|
+
# comma-separated Dial() arguments can be optionally passed after the
|
354
|
+
# technology.
|
355
|
+
#
|
356
|
+
# TODO: Provide an example when this works.
|
357
|
+
#
|
358
|
+
def introduce(caller, callee, opts={})
|
359
|
+
deprecation_warning
|
360
|
+
dial_args = callee
|
361
|
+
dial_args += "|#{opts[:options]}" if opts[:options]
|
362
|
+
call_and_exec caller, "Dial", :args => dial_args, :caller_id => opts[:caller_id]
|
363
|
+
end
|
364
|
+
|
365
|
+
# hangup terminates a call accepts a channel as the argument
|
366
|
+
# full details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Hangup
|
367
|
+
def hangup(channel)
|
368
|
+
deprecation_warning
|
369
|
+
send_action "Hangup", :channel => channel
|
370
|
+
end
|
371
|
+
|
372
|
+
# call_and_exec allows you to make a call to a channel and then execute an Astersik application
|
373
|
+
# on that call
|
374
|
+
def call_and_exec(channel, app, opts={})
|
375
|
+
deprecation_warning
|
376
|
+
args = { :channel => channel, :application => app }
|
377
|
+
args[:caller_id] = opts[:caller_id] if opts[:caller_id]
|
378
|
+
args[:data] = opts[:args] if opts[:args]
|
379
|
+
originate args
|
380
|
+
end
|
381
|
+
|
382
|
+
# call_into_context is syntactic sugar for the Asterisk originate command that allows you to
|
383
|
+
# lanuch a call into a particular context. For example:
|
384
|
+
#
|
385
|
+
# call_into_context('SIP/1000@sipnetworks.com', 'my_context', { :variables => { :session_guid => new_guid }})
|
386
|
+
def call_into_context(channel, context, options={})
|
387
|
+
deprecation_warning
|
388
|
+
args = {:channel => channel, :context => context}
|
389
|
+
args[:priority] = options[:priority] || 1
|
390
|
+
args[:exten] = options[:extension] if options[:extension]
|
391
|
+
args[:caller_id] = options[:caller_id] if options[:caller_id]
|
392
|
+
if options[:variables] && options[:variables].kind_of?(Hash)
|
393
|
+
args[:variable] = options[:variables].map {|pair| pair.join('=')}.join('|')
|
394
|
+
end
|
395
|
+
originate args
|
396
|
+
end
|
397
|
+
|
398
|
+
####### #######
|
399
|
+
########### ###########
|
400
|
+
################# END SOON-DEPRECATED COMMANDS #################
|
401
|
+
########### ###########
|
402
|
+
####### #######
|
403
|
+
|
404
|
+
|
405
|
+
protected
|
406
|
+
|
407
|
+
##
|
408
|
+
# This class will be removed once this AMI library fully supports all known protocol anomalies.
|
409
|
+
#
|
410
|
+
class UnsupportedActionName < ArgumentError
|
411
|
+
UNSUPPORTED_ACTION_NAMES = %w[
|
412
|
+
queues
|
413
|
+
iaxpeers
|
414
|
+
] unless defined? UNSUPPORTED_ACTION_NAMES
|
415
|
+
def initialize(name)
|
416
|
+
super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly."
|
417
|
+
end
|
418
|
+
|
419
|
+
end
|
420
|
+
|
421
|
+
def check_action_name(name)
|
422
|
+
name = name.to_s.downcase
|
423
|
+
raise UnsupportedActionName.new(name) if UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
|
424
|
+
true
|
425
|
+
end
|
426
|
+
|
427
|
+
def write_loop
|
428
|
+
loop do
|
429
|
+
next_action = @write_queue.shift
|
430
|
+
return :stopped if next_action.equal? :STOP!
|
431
|
+
register_action_with_metadata next_action
|
432
|
+
|
433
|
+
ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
|
434
|
+
@actions_connection.send_data next_action.to_s
|
435
|
+
# If it's "causal event" action, we must wait here until it's fully responded
|
436
|
+
next_action.response if next_action.has_causal_events?
|
437
|
+
end
|
438
|
+
rescue => e
|
439
|
+
p e
|
440
|
+
end
|
441
|
+
|
442
|
+
##
|
443
|
+
# When we send out an AMI action, we need to track the ActionID and have the other Thread handling the socket IO
|
444
|
+
# notify the sending Thread that a response has been received. This method instantiates a new FutureResource and
|
445
|
+
# keeps it around in a synchronized Hash for the IO-handling Thread to notify when a response with a matching
|
446
|
+
# ActionID is seen again. See also data_for_message_received_with_action_id() which is how the IO-handling Thread
|
447
|
+
# gets the metadata registered in the method back later.
|
448
|
+
#
|
449
|
+
# @param [ManagerInterfaceAction] action The ManagerInterfaceAction to send
|
450
|
+
# @param [Hash] headers The other key/value pairs being sent with this message
|
451
|
+
#
|
452
|
+
def register_action_with_metadata(action)
|
453
|
+
raise ArgumentError, "Must supply an action!" if action.nil?
|
454
|
+
@sent_messages_lock.synchronize do
|
455
|
+
@sent_messages[action.action_id] = action
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def data_for_message_received_with_action_id(action_id)
|
460
|
+
@sent_messages_lock.synchronize do
|
461
|
+
@sent_messages.delete action_id
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
##
|
466
|
+
# Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
|
467
|
+
#
|
468
|
+
# @return [EventSocket]
|
469
|
+
#
|
470
|
+
def establish_actions_connection
|
471
|
+
@actions_connection = EventSocket.connect(@host, @port) do |handler|
|
472
|
+
handler.receive_data { |data| @actions_lexer << data }
|
473
|
+
handler.connected { actions_connection_established }
|
474
|
+
handler.disconnected { actions_connection_disconnected }
|
475
|
+
end
|
476
|
+
login_actions
|
477
|
+
end
|
478
|
+
|
479
|
+
##
|
480
|
+
# Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
|
481
|
+
#
|
482
|
+
# @return [EventSocket]
|
483
|
+
#
|
484
|
+
def establish_events_connection
|
485
|
+
|
486
|
+
# Note: the @events_connection instance variable is set in login()
|
487
|
+
@events_connection = EventSocket.connect(@host, @port) do |handler|
|
488
|
+
handler.receive_data { |data| @events_lexer << data }
|
489
|
+
handler.connected { events_connection_established }
|
490
|
+
handler.disconnected { events_connection_disconnected }
|
491
|
+
end
|
492
|
+
login_events
|
493
|
+
ahn_log.ami "Successful AMI events-only connection into #{@username}@#{@host}"
|
494
|
+
end
|
495
|
+
|
496
|
+
def login_actions
|
497
|
+
action = send_action_asynchronously "Login", "Username" => @username, "Secret" => @password, "Events" => "Off"
|
498
|
+
response = action.response
|
499
|
+
if response.kind_of? ManagerInterfaceError
|
500
|
+
raise AuthenticationFailedException, "Incorrect username and password! #{response.message}"
|
501
|
+
else
|
502
|
+
ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
|
503
|
+
response
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
|
509
|
+
# been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
|
510
|
+
# it goes straight to the EventSocket connection (bypassing the @write_queue).
|
511
|
+
#
|
512
|
+
def login_events
|
513
|
+
login_action = ManagerInterfaceAction.new "Login", "Username" => @username, "Secret" => @password, "Events" => "On"
|
514
|
+
@events_connection.send_data login_action.to_s
|
515
|
+
end
|
516
|
+
|
517
|
+
def parse_options(options)
|
518
|
+
unrecognized_keys = options.keys.map { |key| key.to_sym } - DEFAULT_SETTINGS.keys
|
519
|
+
if unrecognized_keys.any?
|
520
|
+
raise ArgumentError, "Unrecognized named argument(s): #{unrecognized_keys.to_sentence}"
|
521
|
+
end
|
522
|
+
DEFAULT_SETTINGS.merge options
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
# Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
|
527
|
+
#
|
528
|
+
class AuthenticationFailedException < Exception; end
|
529
|
+
|
530
|
+
class NotConnectedError < Exception; end
|
531
|
+
|
532
|
+
##
|
533
|
+
# Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
|
534
|
+
#
|
535
|
+
class ManagerInterfaceAction
|
536
|
+
|
537
|
+
attr_reader :name, :headers, :future_resource, :action_id, :causal_event_terminator_name
|
538
|
+
def initialize(name, headers={})
|
539
|
+
@name = name.to_s.downcase.freeze
|
540
|
+
@headers = headers.stringify_keys.freeze
|
541
|
+
@action_id = new_action_id.freeze
|
542
|
+
@future_resource = FutureResource.new
|
543
|
+
@causal_event_terminator_name = ManagerInterface.causal_event_terminator_name_for name
|
544
|
+
end
|
545
|
+
|
546
|
+
##
|
547
|
+
# Used internally by ManagerInterface for the actions in AMI which break the protocol's definition and do not
|
548
|
+
# reply with an ActionID.
|
549
|
+
#
|
550
|
+
def replies_with_action_id?
|
551
|
+
ManagerInterface.replies_with_action_id?(@name, @headers)
|
552
|
+
end
|
553
|
+
|
554
|
+
##
|
555
|
+
# Some AMI actions effectively respond with many events which collectively constitute the actual response. These
|
556
|
+
# Must be handled specially by the protocol parser, so this method helps inform the parser.
|
557
|
+
#
|
558
|
+
def has_causal_events?
|
559
|
+
ManagerInterface.has_causal_events?(@name, @headers)
|
560
|
+
end
|
561
|
+
|
562
|
+
##
|
563
|
+
# Abstracts the generation of new ActionIDs. This could be implemented virutally any way, provided each
|
564
|
+
# invocation returns something unique, so this will generate a GUID and return it.
|
565
|
+
#
|
566
|
+
# @return [String] characters in GUID format (e.g. "4C5F4E1C-A0F1-4D13-8751-C62F2F783062")
|
567
|
+
#
|
568
|
+
def new_action_id
|
569
|
+
new_guid # Implemented in lib/adhearsion/foundation/pseudo_guid.rb
|
570
|
+
end
|
571
|
+
|
572
|
+
##
|
573
|
+
# Converts this action into a protocol-valid String, ready to be sent over a socket.
|
574
|
+
#
|
575
|
+
def to_s
|
576
|
+
@textual_representation ||= (
|
577
|
+
"Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
|
578
|
+
@headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
|
579
|
+
(@headers.any? ? "\r\n\r\n" : "\r\n")
|
580
|
+
)
|
581
|
+
end
|
582
|
+
|
583
|
+
##
|
584
|
+
# If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
|
585
|
+
# in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
|
586
|
+
# object.
|
587
|
+
#
|
588
|
+
def response
|
589
|
+
future_resource.resource
|
590
|
+
end
|
591
|
+
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|