rene-adhearsion 0.8.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +73 -0
- data/EVENTS +11 -0
- data/LICENSE +456 -0
- data/Rakefile +130 -0
- data/adhearsion.gemspec +173 -0
- data/app_generators/ahn/USAGE +5 -0
- data/app_generators/ahn/ahn_generator.rb +96 -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 +91 -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/disabled/xmpp_gateway/README.markdown +3 -0
- data/app_generators/ahn/templates/components/disabled/xmpp_gateway/xmpp_gateway.rb +11 -0
- data/app_generators/ahn/templates/components/disabled/xmpp_gateway/xmpp_gateway.yml +0 -0
- data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
- data/app_generators/ahn/templates/config/startup.rb +83 -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 +45 -0
- data/lib/adhearsion/cli.rb +228 -0
- data/lib/adhearsion/component_manager.rb +272 -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 +15 -0
- data/lib/adhearsion/foundation/blank_slate.rb +3 -0
- data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
- data/lib/adhearsion/foundation/event_socket.rb +204 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -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 +395 -0
- data/lib/adhearsion/initializer/asterisk.rb +87 -0
- data/lib/adhearsion/initializer/configuration.rb +321 -0
- data/lib/adhearsion/initializer/database.rb +60 -0
- data/lib/adhearsion/initializer/drb.rb +31 -0
- data/lib/adhearsion/initializer/freeswitch.rb +22 -0
- data/lib/adhearsion/initializer/ldap.rb +57 -0
- data/lib/adhearsion/initializer/rails.rb +41 -0
- data/lib/adhearsion/initializer/xmpp.rb +42 -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 +33 -0
- data/lib/adhearsion/voip/asterisk.rb +4 -0
- data/lib/adhearsion/voip/asterisk/agi_server.rb +115 -0
- data/lib/adhearsion/voip/asterisk/commands.rb +1510 -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 +705 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1680 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +340 -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 +497 -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 +246 -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 +69 -0
- data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
- data/lib/adhearsion/voip/dsl/numerical_string.rb +115 -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/adhearsion/xmpp/connection.rb +61 -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 +241 -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
|
+
ContextDefinition.new(name).tap 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
|
+
MailboxDefinition.new.tap 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,705 @@
|
|
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/display/adhearsion/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
|
+
CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls
|
33
|
+
dahdishowchannels coreshowchannels dbget
|
34
|
+
status konferencelist] unless defined? CAUSAL_EVENT_NAMES
|
35
|
+
|
36
|
+
RETRY_SLEEP = 5
|
37
|
+
|
38
|
+
class << self
|
39
|
+
|
40
|
+
def connect(*args)
|
41
|
+
new(*args).tap do |connection|
|
42
|
+
connection.connect!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def replies_with_action_id?(name, headers={})
|
47
|
+
name = name.to_s.downcase
|
48
|
+
!UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
|
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
|
+
CAUSAL_EVENT_NAMES.include? name.to_s.downcase
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Used to determine the event name for an action which has causal events.
|
69
|
+
#
|
70
|
+
# @param [String] action_name
|
71
|
+
# @return [String] The corresponding event name which signals the completion of the causal event sequence.
|
72
|
+
#
|
73
|
+
def causal_event_terminator_name_for(action_name)
|
74
|
+
return nil unless has_causal_events?(action_name)
|
75
|
+
action_name = action_name.to_s.downcase
|
76
|
+
case action_name
|
77
|
+
when "sippeers", "iaxpeers"
|
78
|
+
"peerlistcomplete"
|
79
|
+
when "dbget"
|
80
|
+
"dbgetresponse"
|
81
|
+
when "konferencelist"
|
82
|
+
"conferencelistcomplete"
|
83
|
+
else
|
84
|
+
action_name + "complete"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
DEFAULT_SETTINGS = {
|
91
|
+
:host => "localhost",
|
92
|
+
:port => 5038,
|
93
|
+
:username => "admin",
|
94
|
+
:password => "secret",
|
95
|
+
:events => true,
|
96
|
+
:auto_reconnect => true,
|
97
|
+
:event_callback => proc { |event| Events.trigger(%w[asterisk manager_interface], event) }
|
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
|
+
@host = options[:host]
|
113
|
+
@username = options[:username]
|
114
|
+
@password = options[:password]
|
115
|
+
@port = options[:port]
|
116
|
+
@events = options[:events]
|
117
|
+
@auto_reconnect = options[:auto_reconnect]
|
118
|
+
@event_callback = options[:event_callback]
|
119
|
+
|
120
|
+
@sent_messages = {}
|
121
|
+
@sent_messages_lock = Mutex.new
|
122
|
+
|
123
|
+
@actions_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
124
|
+
:message_received => :action_message_received,
|
125
|
+
:error_received => :action_error_received
|
126
|
+
|
127
|
+
@write_queue = Queue.new
|
128
|
+
|
129
|
+
if @events
|
130
|
+
@events_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
131
|
+
:message_received => :event_message_received,
|
132
|
+
:error_received => :event_error_received
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def action_message_received(message)
|
137
|
+
if message.kind_of? Manager::ManagerInterfaceEvent
|
138
|
+
# Trigger the return value of the waiting action id...
|
139
|
+
corresponding_action = @current_action_with_causal_events
|
140
|
+
event_collection = @event_collection_for_current_action
|
141
|
+
|
142
|
+
if corresponding_action
|
143
|
+
|
144
|
+
# The "DBGet" command is causal, meaning it has an separate
|
145
|
+
# event that contains the data for command's response. However,
|
146
|
+
# unlike other causal commands, AMI does not send a
|
147
|
+
# "DBGetComplete" action indicating the causal event is
|
148
|
+
# finished. This is fixed starting in Asterisk 1.8.
|
149
|
+
if message.name.downcase == "dbgetresponse"
|
150
|
+
event_collection << message
|
151
|
+
end
|
152
|
+
|
153
|
+
# If this is the meta-event which signals no more events will follow and the response is complete.
|
154
|
+
if message.name.downcase == corresponding_action.causal_event_terminator_name
|
155
|
+
# Wake up the waiting Thread
|
156
|
+
corresponding_action.future_resource.resource = event_collection.freeze
|
157
|
+
|
158
|
+
# Clear the stored action and event collection
|
159
|
+
@current_action_with_causal_events = nil
|
160
|
+
@event_collection_for_current_action = nil
|
161
|
+
else
|
162
|
+
event_collection << message
|
163
|
+
# We have more causal events coming.
|
164
|
+
end
|
165
|
+
else
|
166
|
+
ahn_log.ami.error "Got an unexpected event on actions socket! This AMI command may have a multi-message response. Try making Adhearsion treat it as CAUSAL_EVENT #{message.inspect}"
|
167
|
+
end
|
168
|
+
|
169
|
+
elsif message["ActionID"].nil?
|
170
|
+
# No ActionID! Release the write lock and wake up the waiter
|
171
|
+
else
|
172
|
+
action_id = message["ActionID"]
|
173
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
174
|
+
if corresponding_action
|
175
|
+
message.action = corresponding_action
|
176
|
+
|
177
|
+
if corresponding_action.has_causal_events?
|
178
|
+
# By this point the write loop will already have started blocking by calling the response() method on the
|
179
|
+
# action. Because we must collect more events before we wake the write loop up again, let's create these
|
180
|
+
# instance variable which will needed when the subsequent causal events come in.
|
181
|
+
@current_action_with_causal_events = corresponding_action
|
182
|
+
@event_collection_for_current_action = []
|
183
|
+
else
|
184
|
+
# Wake any Threads waiting on the response.
|
185
|
+
corresponding_action.future_resource.resource = message
|
186
|
+
end
|
187
|
+
else
|
188
|
+
ahn_log.ami.error "Received an AMI message with an unrecognized ActionID!! This may be an bug! #{message.inspect}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def action_error_received(ami_error)
|
194
|
+
action_id = ami_error["ActionID"]
|
195
|
+
|
196
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
197
|
+
|
198
|
+
if corresponding_action
|
199
|
+
corresponding_action.future_resource.resource = ami_error
|
200
|
+
else
|
201
|
+
ahn_log.ami.error "Received an AMI error with an unrecognized ActionID!! This may be an bug! #{ami_error.inspect}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Called only when this ManagerInterface is instantiated with events enabled.
|
207
|
+
#
|
208
|
+
def event_message_received(event)
|
209
|
+
return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
|
210
|
+
# TODO: convert the event name to a certain namespace.
|
211
|
+
@event_callback.call(event)
|
212
|
+
end
|
213
|
+
|
214
|
+
def event_error_received(message)
|
215
|
+
# Does this ever even occur?
|
216
|
+
ahn_log.ami.error "Hmmm, got an error on the AMI events-only socket! This must be a bug! #{message.inspect}"
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# Called when our Ragel parser encounters some unexpected syntax from Asterisk. Anytime this is called, it should
|
221
|
+
# be considered a bug in Adhearsion. Note: this same method is called regardless of whether the syntax error
|
222
|
+
# happened on the actions socket or on the events socket.
|
223
|
+
#
|
224
|
+
def syntax_error_encountered(ignored_chunk)
|
225
|
+
ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
|
226
|
+
"PLEASE REPORT THIS ON http://bugs.adhearsion.com! OFFENDING TEXT:\n#{ignored_chunk.inspect}"
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Must be called after instantiation. Also see ManagerInterface::connect().
|
231
|
+
#
|
232
|
+
# @raise [AuthenticationFailedException] if username or password are rejected
|
233
|
+
#
|
234
|
+
def connect!
|
235
|
+
establish_actions_connection
|
236
|
+
establish_events_connection if @events
|
237
|
+
self
|
238
|
+
end
|
239
|
+
|
240
|
+
def actions_connection_established
|
241
|
+
@actions_state = :connected
|
242
|
+
start_actions_writer_loop
|
243
|
+
end
|
244
|
+
|
245
|
+
def actions_connection_disconnected
|
246
|
+
@actions_state = :disconnected
|
247
|
+
ahn_log.ami.error "AMI connection for ACTION disconnected !!!"
|
248
|
+
clear_actions_connection
|
249
|
+
establish_actions_connection if @auto_reconnect
|
250
|
+
end
|
251
|
+
|
252
|
+
def events_connection_established
|
253
|
+
@events_state = :connected
|
254
|
+
end
|
255
|
+
|
256
|
+
def events_connection_disconnected
|
257
|
+
@events_state = :disconnected
|
258
|
+
ahn_log.ami.error "AMI connection for EVENT disconnected !!!"
|
259
|
+
clear_events_connection
|
260
|
+
establish_events_connection if @auto_reconnect
|
261
|
+
end
|
262
|
+
|
263
|
+
def clear_actions_connection
|
264
|
+
stop_actions_writer_loop
|
265
|
+
clear_actions_connection_resources
|
266
|
+
disconnect_actions_connection if @actions_state.equal? :connected
|
267
|
+
end
|
268
|
+
|
269
|
+
def clear_events_connection
|
270
|
+
disconnect_events_connection if @events_state.equal? :connected
|
271
|
+
end
|
272
|
+
|
273
|
+
def disconnect!
|
274
|
+
clear_actions_connection
|
275
|
+
clear_events_connection
|
276
|
+
end
|
277
|
+
|
278
|
+
def dynamic
|
279
|
+
# TODO: Return an object which responds to method_missing
|
280
|
+
end
|
281
|
+
|
282
|
+
##
|
283
|
+
# Used to directly send a new action to Asterisk. Note: NEVER supply an ActionID; these are handled internally.
|
284
|
+
#
|
285
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
286
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
287
|
+
# @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.
|
288
|
+
#
|
289
|
+
def send_action_asynchronously(action_name, headers={})
|
290
|
+
check_action_name action_name
|
291
|
+
action = ManagerInterfaceAction.new(action_name, headers)
|
292
|
+
if action.replies_with_action_id?
|
293
|
+
@write_queue << action
|
294
|
+
action
|
295
|
+
else
|
296
|
+
raise NotImplementedError
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
##
|
301
|
+
# Sends an action over the AMI connection and blocks your Thread until the response comes in. If there was an error
|
302
|
+
# for some reason, the error will be raised as an ManagerInterfaceError.
|
303
|
+
#
|
304
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
305
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
306
|
+
# @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.
|
307
|
+
# @return [ManagerInterfaceResponse, ImmediateResponse] Contains the response from Asterisk and all headers
|
308
|
+
#
|
309
|
+
def send_action_synchronously(*args)
|
310
|
+
send_action_asynchronously(*args).response.tap do |response|
|
311
|
+
raise response if response.kind_of?(ManagerInterfaceError)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
alias send_action send_action_synchronously
|
316
|
+
|
317
|
+
|
318
|
+
####### #######
|
319
|
+
########### ###########
|
320
|
+
################# SOON-DEPRECATED COMMANDS #################
|
321
|
+
########### ###########
|
322
|
+
####### #######
|
323
|
+
|
324
|
+
# ping sends an action to the Asterisk Manager Interface that returns a pong
|
325
|
+
# more details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Ping
|
326
|
+
def ping
|
327
|
+
#deprecation_warning
|
328
|
+
send_action "Ping"
|
329
|
+
true
|
330
|
+
end
|
331
|
+
|
332
|
+
def deprecation_warning
|
333
|
+
ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
|
334
|
+
"and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
|
335
|
+
"SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
|
336
|
+
" See http://docs.adhearsion.com/AMI for more information."
|
337
|
+
end
|
338
|
+
|
339
|
+
# The originate method launches a call to Asterisk, full details here:
|
340
|
+
# http://www.voip-info.org/tiki-index.php?page=Asterisk+Manager+API+Action+Originate
|
341
|
+
# Takes these arguments as a hash:
|
342
|
+
#
|
343
|
+
# Channel: Channel on which to originate the call (The same as you specify in the Dial application command)
|
344
|
+
# Context: Context to use on connect (must use Exten & Priority with it)
|
345
|
+
# Exten: Extension to use on connect (must use Context & Priority with it)
|
346
|
+
# Priority: Priority to use on connect (must use Context & Exten with it)
|
347
|
+
# Timeout: Timeout (in milliseconds) for the originating connection to happen(defaults to 30000 milliseconds)
|
348
|
+
# CallerID: CallerID to use for the call
|
349
|
+
# Variable: Channels variables to set (max 32). Variables will be set for both channels (local and connected).
|
350
|
+
# Account: Account code for the call
|
351
|
+
# Application: Application to use on connect (use Data for parameters)
|
352
|
+
# Data : Data if Application parameter is used
|
353
|
+
# Async: For the origination to be asynchronous (allows multiple calls to be generated without waiting for a response)
|
354
|
+
# ActionID: The request identifier. It allows you to identify the response to this request.
|
355
|
+
# You may use a number or a string. Useful when you make several simultaneous requests.
|
356
|
+
#
|
357
|
+
# For example:
|
358
|
+
# originate { :channel => 'SIP/1000@sipnetworks.com',
|
359
|
+
# :context => 'my_context',
|
360
|
+
# :exten => 's',
|
361
|
+
# :priority => '1' }
|
362
|
+
def originate(options={})
|
363
|
+
#deprecation_warning
|
364
|
+
options = options.clone
|
365
|
+
options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
|
366
|
+
options[:exten] = options.delete :extension if options.has_key? :extension
|
367
|
+
if options[:variables] && options[:variables].kind_of?(Hash)
|
368
|
+
options[:variable] = options[:variables].map {|pair| pair.join('=')}.join(@coreSettings["ArgumentDelimiter"])
|
369
|
+
end
|
370
|
+
send_action "Originate", options
|
371
|
+
end
|
372
|
+
|
373
|
+
# An introduction connects two endpoints together. The first argument is
|
374
|
+
# the first person the PBX will call. When she's picked up, Asterisk will
|
375
|
+
# play ringing while the second person is being dialed.
|
376
|
+
#
|
377
|
+
# The first argument is the person called first. Pass this as a canonical
|
378
|
+
# IAX2/server/user type argument. Destination takes the same format, but
|
379
|
+
# comma-separated Dial() arguments can be optionally passed after the
|
380
|
+
# technology.
|
381
|
+
#
|
382
|
+
# TODO: Provide an example when this works.
|
383
|
+
#
|
384
|
+
def introduce(caller, callee, opts={})
|
385
|
+
#deprecation_warning
|
386
|
+
dial_args = callee
|
387
|
+
dial_args += "|#{opts[:options]}" if opts[:options]
|
388
|
+
call_and_exec caller, "Dial", :args => dial_args, :caller_id => opts[:caller_id]
|
389
|
+
end
|
390
|
+
|
391
|
+
# hangup terminates a call accepts a channel as the argument
|
392
|
+
# full details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Hangup
|
393
|
+
def hangup(channel)
|
394
|
+
#deprecation_warning
|
395
|
+
send_action "Hangup", :channel => channel
|
396
|
+
end
|
397
|
+
|
398
|
+
# call_and_exec allows you to make a call to a channel and then execute an Astersik application
|
399
|
+
# on that call
|
400
|
+
def call_and_exec(channel, app, opts={})
|
401
|
+
#deprecation_warning
|
402
|
+
args = { :channel => channel, :application => app }
|
403
|
+
args[:caller_id] = opts[:caller_id] if opts[:caller_id]
|
404
|
+
args[:data] = opts[:args] if opts[:args]
|
405
|
+
args[:variables] = opts[:variables] if opts[:variables]
|
406
|
+
originate args
|
407
|
+
end
|
408
|
+
|
409
|
+
# call_into_context is syntactic sugar for the Asterisk originate command that allows you to
|
410
|
+
# launch a call into a particular context. For example:
|
411
|
+
#
|
412
|
+
# call_into_context('SIP/1000@sipnetworks.com', 'my_context', { :variables => { :session_guid => new_guid }})
|
413
|
+
def call_into_context(channel, context, options={})
|
414
|
+
#deprecation_warning
|
415
|
+
args = {:channel => channel, :context => context}
|
416
|
+
args[:priority] = options[:priority] || 1
|
417
|
+
args[:exten] = options[:extension] if options[:extension]
|
418
|
+
args[:caller_id] = options[:caller_id] if options[:caller_id]
|
419
|
+
args[:variables] = options[:variables] if options[:variables]
|
420
|
+
originate args
|
421
|
+
end
|
422
|
+
|
423
|
+
####### #######
|
424
|
+
########### ###########
|
425
|
+
################# END SOON-DEPRECATED COMMANDS #################
|
426
|
+
########### ###########
|
427
|
+
####### #######
|
428
|
+
|
429
|
+
|
430
|
+
protected
|
431
|
+
|
432
|
+
##
|
433
|
+
# This class will be removed once this AMI library fully supports all known protocol anomalies.
|
434
|
+
#
|
435
|
+
class UnsupportedActionName < ArgumentError
|
436
|
+
UNSUPPORTED_ACTION_NAMES = %w[
|
437
|
+
queues
|
438
|
+
] unless defined? UNSUPPORTED_ACTION_NAMES
|
439
|
+
|
440
|
+
# Blacklist some actions depends on the Asterisk version
|
441
|
+
def self.preinitialize(version)
|
442
|
+
if version < 1.8
|
443
|
+
%w[iaxpeers muteaudio mixmonitormute aocmessage].each do |action|
|
444
|
+
UNSUPPORTED_ACTION_NAMES << action
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
if version < 1.6
|
449
|
+
%w[skinnydevices skinnyshowdevice skinnylines skinnyshowline coreshowchannels
|
450
|
+
sipshowregistry getconfigjson bridge listallvoicemailusers dbdel dbdeltree
|
451
|
+
insert jitterbufstats atxfer iaxregistry queuereload queuereset].each do |action|
|
452
|
+
UNSUPPORTED_ACTION_NAMES << action
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def initialize(name)
|
458
|
+
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."
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
def check_action_name(name)
|
464
|
+
name = name.to_s.downcase
|
465
|
+
raise UnsupportedActionName.new(name) if UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
|
466
|
+
true
|
467
|
+
end
|
468
|
+
|
469
|
+
def start_actions_writer_loop
|
470
|
+
@actions_writer_thread = Thread.new(&method(:actions_writer_loop))
|
471
|
+
end
|
472
|
+
|
473
|
+
def stop_actions_writer_loop
|
474
|
+
if @actions_writer_thread
|
475
|
+
@write_queue << :STOP!
|
476
|
+
@actions_writer_thread.join
|
477
|
+
@actions_writer_thread = nil
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def actions_writer_loop
|
482
|
+
loop do
|
483
|
+
begin
|
484
|
+
next_action = @write_queue.shift
|
485
|
+
return :stopped if next_action.equal? :STOP!
|
486
|
+
register_action_with_metadata next_action
|
487
|
+
|
488
|
+
ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
|
489
|
+
@actions_connection.send_data next_action.to_s
|
490
|
+
# If it's "causal event" action, we must wait here until it's fully responded
|
491
|
+
next_action.response if next_action.has_causal_events?
|
492
|
+
rescue Object => e
|
493
|
+
ahn_log.ami.debug "Error in AMI writer loop: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
##
|
499
|
+
# When we send out an AMI action, we need to track the ActionID and have the other Thread handling the socket IO
|
500
|
+
# notify the sending Thread that a response has been received. This method instantiates a new FutureResource and
|
501
|
+
# keeps it around in a synchronized Hash for the IO-handling Thread to notify when a response with a matching
|
502
|
+
# ActionID is seen again. See also data_for_message_received_with_action_id() which is how the IO-handling Thread
|
503
|
+
# gets the metadata registered in the method back later.
|
504
|
+
#
|
505
|
+
# @param [ManagerInterfaceAction] action The ManagerInterfaceAction to send
|
506
|
+
# @param [Hash] headers The other key/value pairs being sent with this message
|
507
|
+
#
|
508
|
+
def register_action_with_metadata(action)
|
509
|
+
raise ArgumentError, "Must supply an action!" if action.nil?
|
510
|
+
@sent_messages_lock.synchronize do
|
511
|
+
@sent_messages[action.action_id] = action
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
def data_for_message_received_with_action_id(action_id)
|
516
|
+
@sent_messages_lock.synchronize do
|
517
|
+
@sent_messages.delete action_id
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# Give an error response to any outstanding messages -- they
|
522
|
+
# won't be completed now
|
523
|
+
def clear_actions_connection_resources
|
524
|
+
# Fail all outstanding messages and reset the message list
|
525
|
+
@sent_messages_lock.synchronize do
|
526
|
+
@sent_messages.each do |action_id, action|
|
527
|
+
error = ManagerInterfaceError.new
|
528
|
+
error.message = "Connection terminated to AMI server"
|
529
|
+
|
530
|
+
action.future_resource.resource = error
|
531
|
+
end
|
532
|
+
|
533
|
+
@sent_messages = {}
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
##
|
538
|
+
# Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
|
539
|
+
#
|
540
|
+
# @return [EventSocket]
|
541
|
+
#
|
542
|
+
def establish_actions_connection
|
543
|
+
@actions_connection = EventSocket.connect(@host, @port) do |handler|
|
544
|
+
handler.receive_data { |data| @actions_lexer << data }
|
545
|
+
handler.connected { actions_connection_established }
|
546
|
+
handler.disconnected { actions_connection_disconnected }
|
547
|
+
end
|
548
|
+
login_actions
|
549
|
+
rescue Errno::ECONNREFUSED => e
|
550
|
+
ahn_log.ami.warn "ACTIONS thread connection refused! Retrying in #{RETRY_SLEEP} seconds..."
|
551
|
+
sleep RETRY_SLEEP
|
552
|
+
retry
|
553
|
+
end
|
554
|
+
|
555
|
+
def disconnect_actions_connection
|
556
|
+
# Clean up the EventSocket we may have
|
557
|
+
if @actions_connection
|
558
|
+
@actions_connection.disconnect!
|
559
|
+
@actions_connection.join
|
560
|
+
@actions_connection = nil
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
##
|
565
|
+
# Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
|
566
|
+
#
|
567
|
+
# @return [EventSocket]
|
568
|
+
#
|
569
|
+
def establish_events_connection
|
570
|
+
|
571
|
+
# Note: the @events_connection instance variable is set in login()
|
572
|
+
@events_connection = EventSocket.connect(@host, @port) do |handler|
|
573
|
+
handler.receive_data { |data| @events_lexer << data }
|
574
|
+
handler.connected { events_connection_established }
|
575
|
+
handler.disconnected { events_connection_disconnected }
|
576
|
+
end
|
577
|
+
login_events
|
578
|
+
ahn_log.ami "Successful AMI events-only connection into #{@username}@#{@host}"
|
579
|
+
rescue Errno::ECONNREFUSED => e
|
580
|
+
ahn_log.ami.warn "EVENTS thread connection refused! Retrying in #{RETRY_SLEEP} seconds..."
|
581
|
+
sleep RETRY_SLEEP
|
582
|
+
retry
|
583
|
+
end
|
584
|
+
|
585
|
+
def login_actions
|
586
|
+
action = send_action_asynchronously "Login", "Username" => @username, "Secret" => @password, "Events" => "Off"
|
587
|
+
response = action.response
|
588
|
+
if response.kind_of? ManagerInterfaceError
|
589
|
+
raise AuthenticationFailedException, "Incorrect username and password! #{response.message}"
|
590
|
+
else
|
591
|
+
ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
|
592
|
+
if @actions_lexer.ami_version < 1.1
|
593
|
+
@coreSettings = Hash.new
|
594
|
+
@coreSettings["AsteriskVersion"] = "1.4.0"
|
595
|
+
@coreSettings["AMIversion"] = "1.0"
|
596
|
+
@coreSettings["ArgumentDelimiter"] = "|"
|
597
|
+
else
|
598
|
+
@coreSettings = send_action_synchronously("CoreSettings").headers
|
599
|
+
@coreSettings["ArgumentDelimiter"] = ","
|
600
|
+
end
|
601
|
+
UnsupportedActionName::preinitialize(@coreSettings["AsteriskVersion"].to_f)
|
602
|
+
response
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
def disconnect_events_connection
|
607
|
+
# Clean up the EventSocket we may have
|
608
|
+
if @events_connection
|
609
|
+
@events_connection.disconnect!
|
610
|
+
@events_connection.join
|
611
|
+
@events_connection = nil
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
##
|
616
|
+
# Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
|
617
|
+
# been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
|
618
|
+
# it goes straight to the EventSocket connection (bypassing the @write_queue).
|
619
|
+
#
|
620
|
+
def login_events
|
621
|
+
login_action = ManagerInterfaceAction.new "Login", "Username" => @username, "Secret" => @password, "Events" => "On"
|
622
|
+
@events_connection.send_data login_action.to_s
|
623
|
+
end
|
624
|
+
|
625
|
+
def parse_options(options)
|
626
|
+
unrecognized_keys = options.keys.map { |key| key.to_sym } - DEFAULT_SETTINGS.keys
|
627
|
+
if unrecognized_keys.any?
|
628
|
+
raise ArgumentError, "Unrecognized named argument(s): #{unrecognized_keys.to_sentence}"
|
629
|
+
end
|
630
|
+
DEFAULT_SETTINGS.merge options
|
631
|
+
end
|
632
|
+
|
633
|
+
##
|
634
|
+
# Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
|
635
|
+
#
|
636
|
+
class AuthenticationFailedException < StandardError; end
|
637
|
+
|
638
|
+
class NotConnectedError < StandardError; end
|
639
|
+
|
640
|
+
##
|
641
|
+
# Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
|
642
|
+
#
|
643
|
+
class ManagerInterfaceAction
|
644
|
+
|
645
|
+
attr_reader :name, :headers, :future_resource, :action_id, :causal_event_terminator_name
|
646
|
+
def initialize(name, headers={})
|
647
|
+
@name = name.to_s.downcase.freeze
|
648
|
+
@headers = headers.stringify_keys.freeze
|
649
|
+
@action_id = new_action_id.freeze
|
650
|
+
@future_resource = FutureResource.new
|
651
|
+
@causal_event_terminator_name = ManagerInterface.causal_event_terminator_name_for name
|
652
|
+
end
|
653
|
+
|
654
|
+
##
|
655
|
+
# Used internally by ManagerInterface for the actions in AMI which break the protocol's definition and do not
|
656
|
+
# reply with an ActionID.
|
657
|
+
#
|
658
|
+
def replies_with_action_id?
|
659
|
+
ManagerInterface.replies_with_action_id?(@name, @headers)
|
660
|
+
end
|
661
|
+
|
662
|
+
##
|
663
|
+
# Some AMI actions effectively respond with many events which collectively constitute the actual response. These
|
664
|
+
# Must be handled specially by the protocol parser, so this method helps inform the parser.
|
665
|
+
#
|
666
|
+
def has_causal_events?
|
667
|
+
ManagerInterface.has_causal_events?(@name, @headers)
|
668
|
+
end
|
669
|
+
|
670
|
+
##
|
671
|
+
# Abstracts the generation of new ActionIDs. This could be implemented virutally any way, provided each
|
672
|
+
# invocation returns something unique, so this will generate a GUID and return it.
|
673
|
+
#
|
674
|
+
# @return [String] characters in GUID format (e.g. "4C5F4E1C-A0F1-4D13-8751-C62F2F783062")
|
675
|
+
#
|
676
|
+
def new_action_id
|
677
|
+
new_guid # Implemented in lib/adhearsion/foundation/pseudo_guid.rb
|
678
|
+
end
|
679
|
+
|
680
|
+
##
|
681
|
+
# Converts this action into a protocol-valid String, ready to be sent over a socket.
|
682
|
+
#
|
683
|
+
def to_s
|
684
|
+
@textual_representation ||= (
|
685
|
+
"Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
|
686
|
+
@headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
|
687
|
+
(@headers.any? ? "\r\n\r\n" : "\r\n")
|
688
|
+
)
|
689
|
+
end
|
690
|
+
|
691
|
+
##
|
692
|
+
# If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
|
693
|
+
# in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
|
694
|
+
# object.
|
695
|
+
#
|
696
|
+
def response
|
697
|
+
future_resource.resource
|
698
|
+
end
|
699
|
+
|
700
|
+
end
|
701
|
+
end
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|