adhearsion 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +339 -0
- data/Rakefile +108 -0
- data/ahn +195 -0
- data/lib/adhearsion.rb +402 -0
- data/lib/constants.rb +20 -0
- data/lib/core_extensions.rb +157 -0
- data/lib/database_functions.rb +76 -0
- data/lib/rami.rb +822 -0
- data/lib/servlet_container.rb +146 -0
- data/new_projects/Rakefile +100 -0
- data/new_projects/config/adhearsion.sqlite3 +0 -0
- data/new_projects/config/adhearsion.yml +11 -0
- data/new_projects/config/database.rb +50 -0
- data/new_projects/config/database.yml +10 -0
- data/new_projects/config/helpers/drb_server.yml +43 -0
- data/new_projects/config/helpers/factorial.alien.c.yml +1 -0
- data/new_projects/config/helpers/manager_proxy.yml +7 -0
- data/new_projects/config/helpers/micromenus.yml +1 -0
- data/new_projects/config/helpers/micromenus/collab.rb +55 -0
- data/new_projects/config/helpers/micromenus/images/tux.bmp +0 -0
- data/new_projects/config/helpers/micromenus/javascripts/builder.js +131 -0
- data/new_projects/config/helpers/micromenus/javascripts/controls.js +834 -0
- data/new_projects/config/helpers/micromenus/javascripts/dragdrop.js +944 -0
- data/new_projects/config/helpers/micromenus/javascripts/effects.js +956 -0
- data/new_projects/config/helpers/micromenus/javascripts/prototype.js +2319 -0
- data/new_projects/config/helpers/micromenus/javascripts/scriptaculous.js +51 -0
- data/new_projects/config/helpers/micromenus/javascripts/slider.js +278 -0
- data/new_projects/config/helpers/micromenus/javascripts/unittest.js +557 -0
- data/new_projects/config/helpers/micromenus/stylesheets/firefox.css +10 -0
- data/new_projects/config/helpers/micromenus/stylesheets/firefox.xul.css +44 -0
- data/new_projects/config/helpers/weather.yml +1 -0
- data/new_projects/config/helpers/xbmc.yml +1 -0
- data/new_projects/config/migration.rb +53 -0
- data/new_projects/extensions.rb +56 -0
- data/new_projects/helpers/drb_server.rb +32 -0
- data/new_projects/helpers/factorial.alien.c +32 -0
- data/new_projects/helpers/manager_proxy.rb +43 -0
- data/new_projects/helpers/micromenus.rb +374 -0
- data/new_projects/helpers/oscar_wilde_quotes.rb +197 -0
- data/new_projects/helpers/weather.rb +85 -0
- data/new_projects/helpers/xbmc.rb +12 -0
- data/new_projects/logs/database.log +0 -0
- data/test/core_extensions_test.rb +26 -0
- data/test/dial_test.rb +43 -0
- data/test/stress_tests/test.rb +13 -0
- data/test/stress_tests/test.yml +13 -0
- data/test/test_micromenus.rb +0 -0
- metadata +131 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
# Adhearsion, open source technology integrator
|
2
|
+
# Copyright 2006 Jay Phillips
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
|
18
|
+
class Definition
|
19
|
+
|
20
|
+
def initialize(*constraints)
|
21
|
+
@options = {}
|
22
|
+
if constraints then @constraints = constraints.flatten!
|
23
|
+
else @constraints = []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :options
|
28
|
+
|
29
|
+
# The method_missing method is a crown-jewel of Ruby. When a method is
|
30
|
+
# called on an object that doesn't exist, Ruby will invoke the method_missing
|
31
|
+
# method. The Kernel module holds the initial definition of this method and
|
32
|
+
# the Ruby interpreter raises a NameError when Kernel#method_missing is
|
33
|
+
# invoked.
|
34
|
+
def method_missing(name, *args)
|
35
|
+
# Let's ensure the names stay simple
|
36
|
+
super unless name.to_s =~ /^[a-z][\w_]*=?$/i || args.empty?
|
37
|
+
@options[name] = args.simplify
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# The user(), group() and use_users_and_group() methods are presently alpha and
|
42
|
+
# really shouldn't be used yet. They're experimental ways of establishing user
|
43
|
+
# and group objects in the database.rb file.
|
44
|
+
|
45
|
+
def user &options
|
46
|
+
establish_connection :internal
|
47
|
+
her = Definition.new
|
48
|
+
yield her
|
49
|
+
User.create her.options
|
50
|
+
end
|
51
|
+
|
52
|
+
def group &options
|
53
|
+
establish_connection :internal
|
54
|
+
her = Definition.new
|
55
|
+
yield her
|
56
|
+
Group.create her.options
|
57
|
+
end
|
58
|
+
|
59
|
+
def use_users_and_groups clause
|
60
|
+
estabish_connection :external
|
61
|
+
case clause.class.inspect.to_sym
|
62
|
+
when :Hash
|
63
|
+
group_table = clause[:from].delete :group_table
|
64
|
+
user_table = clause[:from].delete :user_table
|
65
|
+
# XXX: Note: User.table doesn't exist! Needs fixing!
|
66
|
+
User.table = user_table if user_table
|
67
|
+
Group.table = group_table if group_table
|
68
|
+
|
69
|
+
establish_connection :external, clause[:from]
|
70
|
+
when :String || :File
|
71
|
+
clause = File.open clause, 'a' if clause.is_a? String
|
72
|
+
raise ArgumentError.new("Not a directory!") unless clause.directory?
|
73
|
+
else raise ArgumentError.new("Wrong argument type!")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
alias use_groups_and_users use_users_and_groups
|
data/lib/rami.rb
ADDED
@@ -0,0 +1,822 @@
|
|
1
|
+
#
|
2
|
+
# RAMI - Ruby classes for implementing a proxy server/client api for the Asterisk Manager Interface
|
3
|
+
#
|
4
|
+
# Copyright (c) 2005 Chris Ochs
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
7
|
+
# this software and associated documentation files (the "Software"), to deal in
|
8
|
+
# the Software without restriction, including without limitation the rights to
|
9
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
10
|
+
# of the Software, and to permit persons to whom the Software is furnished to do
|
11
|
+
# so, subject to the following conditions:
|
12
|
+
|
13
|
+
# The above copyright notice and this permission notice shall be included in all
|
14
|
+
# copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
21
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
require 'monitor'
|
24
|
+
require 'socket'
|
25
|
+
require 'timeout'
|
26
|
+
|
27
|
+
# Rami can be used to write standalone scripts to send commands to the Asterisk manager interface, or you can
|
28
|
+
# use Drb and Rami::Server to run a Rami proxy server. You can then connect to the proxy server using Rami::Client and Drb.
|
29
|
+
# This module was written against Asterisk 1.2-beta1. There are a few minor changes to some AMI commands
|
30
|
+
# in Asterisk CVS HEAD. When 1.2 is released I will update the module accordingly.
|
31
|
+
module Rami
|
32
|
+
|
33
|
+
# The Rami client.
|
34
|
+
#
|
35
|
+
# One possible point of confusion about the client is that it takes a server instance as the sole argument to it's new()
|
36
|
+
# method. This is because Rami was designed to be used with Drb. You don't have to use Drb though.
|
37
|
+
# You can create and start a Server instance via it's run method, then in the same code create your Client instance
|
38
|
+
# and submit requests to the server. A simple example..
|
39
|
+
#
|
40
|
+
# require 'rubygems'
|
41
|
+
#
|
42
|
+
# require 'rami'
|
43
|
+
#
|
44
|
+
# include Rami
|
45
|
+
#
|
46
|
+
# server = Server.new({'host' => 'localhost', 'username' => 'asterisk', 'secret' => 'secret'})
|
47
|
+
#
|
48
|
+
# server.run
|
49
|
+
#
|
50
|
+
# client = Client.new(server)
|
51
|
+
#
|
52
|
+
# client.timeout = 10
|
53
|
+
#
|
54
|
+
# puts client.ping
|
55
|
+
#
|
56
|
+
# The above code will start the server and login, then execute the ping command and print the results, then exit, disconnecting
|
57
|
+
# from asterisk.
|
58
|
+
#
|
59
|
+
# To connect to a running server using Drb you can create a client instance like this.:
|
60
|
+
#
|
61
|
+
# c = DRbObject.new(nil,"druby://localhost:9000")
|
62
|
+
#
|
63
|
+
# client = Client.new(c)
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# All Client methods return an array of hashes, or an empty array if no data is available.
|
67
|
+
#
|
68
|
+
# Each hash will contain an asterisk response packet. Note that not all response packets are always returned. If a response
|
69
|
+
# packet is necessary for the actual communication with asterisk, but does not in itself have any meaningful content, then
|
70
|
+
# the packet is droppped. For example some actions might generate an initial response packet of something like "Response Follows",
|
71
|
+
# followed by one or more response packets with the actual data, followed by a final response packet which contains "Response Complete".
|
72
|
+
# In this case the first and last response will not be included in the array of hashes passed to the caller.
|
73
|
+
#
|
74
|
+
# I tried to document the things that need it the most. Some things should be fairly evident, such as methods for simple
|
75
|
+
# commands like Monitor or Ping.
|
76
|
+
#
|
77
|
+
# For examples, see example1.rb, example2.rb, and test.rb in the bin directory.
|
78
|
+
#
|
79
|
+
# Not all manager commands are currently supported. This is only because I have not yet had the time to add them.
|
80
|
+
# I tried to add the most complicated commands first, so adding the remaining commands is fairly simple.
|
81
|
+
# If there is a command you need let me know and I will make sure it gets included.
|
82
|
+
# If you want to add your own action command it's fairly simple. Add a method in Client for
|
83
|
+
# the new command, and then add a section in the Client::send_action loop to harvest the response.
|
84
|
+
class Client
|
85
|
+
|
86
|
+
# Number of seconds before a method call will timeout. Default 10.
|
87
|
+
attr_accessor :timeout
|
88
|
+
|
89
|
+
# Takes one argument, an instance of Server OR a DrbObject pointed at a running Rami server.
|
90
|
+
def initialize(client)
|
91
|
+
@timeout = 10
|
92
|
+
@action_id = Time.now().to_f
|
93
|
+
@client = client
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Closes the socket connection to Asterisk. If your client was created using a Server instance instead of a DrbObject, then
|
98
|
+
# the connection will be left open as long as your client instance is still valid. If for example you are making calls from
|
99
|
+
# a webserver this is bad as you will end up with a lot of open connections to Asterisk. So make sure to use stop.
|
100
|
+
def stop
|
101
|
+
@client.stop
|
102
|
+
end
|
103
|
+
|
104
|
+
def absolute_timeout(channel=nil,tout=nil)
|
105
|
+
increment_action_id
|
106
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'AbsoluteTimeout', 'Channel' => channel, 'Timeout' => tout},@timeout)
|
107
|
+
end
|
108
|
+
|
109
|
+
def agents
|
110
|
+
increment_action_id
|
111
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Agents'},@timeout)
|
112
|
+
end
|
113
|
+
|
114
|
+
def change_monitor(channel=nil,file=nil)
|
115
|
+
increment_action_id
|
116
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'ChangeMonitor', 'Channel' =>channel, 'File' => file},@timeout)
|
117
|
+
end
|
118
|
+
|
119
|
+
def command(command)
|
120
|
+
increment_action_id
|
121
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Command', 'Command' => command},@timeout)
|
122
|
+
end
|
123
|
+
|
124
|
+
def dbput(family,key,val)
|
125
|
+
increment_action_id
|
126
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'DBPut', 'Family' => family, 'Key' => key, 'Val' => val},@timeout)
|
127
|
+
end
|
128
|
+
|
129
|
+
def dbget(family,key)
|
130
|
+
increment_action_id
|
131
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'DBGet', 'Family' => family, 'Key' => key},@timeout)
|
132
|
+
end
|
133
|
+
|
134
|
+
def extension_state(context,exten)
|
135
|
+
increment_action_id
|
136
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'ExtensionState', 'Context' => context, 'Exten' => exten},@timeout)
|
137
|
+
end
|
138
|
+
|
139
|
+
# If called with key and value, searches the state queue for events matching the key and value given.
|
140
|
+
# The key is an exact match, the value is a regex. You can also call find_events with key=any, which will match
|
141
|
+
# any entry with the given value
|
142
|
+
#
|
143
|
+
# The returned results are deleted from the queue. See Server for more information on the queue structure.
|
144
|
+
def find_events(key=nil,value=nil)
|
145
|
+
return @client.find_events(key,value)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get all events from the state queue. The returned results are deleted from the queue.
|
149
|
+
# See Server for more information on the queue structure.
|
150
|
+
def get_events
|
151
|
+
return @client.get_events
|
152
|
+
end
|
153
|
+
|
154
|
+
def getvar(channel,variable)
|
155
|
+
increment_action_id
|
156
|
+
@client.send_action({'ActionID' => @action_id, 'Action' => 'GetVar', 'Channel' => channel, 'Variable' => variable},@timeout)
|
157
|
+
end
|
158
|
+
|
159
|
+
def hangup(channel=nil)
|
160
|
+
increment_action_id
|
161
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Hangup', 'Channel' => channel},@timeout)
|
162
|
+
end
|
163
|
+
|
164
|
+
# IAXpeers is bugged. The response does not contain an action id, nor does it contain any key/value pairs in the response.
|
165
|
+
# For this reason it gets put into the state queue where it can be retrieved using find_events('any','iax2 peers'). iax_peers
|
166
|
+
# will always return {'Response' => 'Success'}
|
167
|
+
def iax_peers
|
168
|
+
increment_action_id
|
169
|
+
@client.send_action({'ActionID' => @action_id, 'Action' => 'IAXpeers'},1)
|
170
|
+
return [{'Response' => 'Success'}]
|
171
|
+
end
|
172
|
+
|
173
|
+
def monitor(channel=nil,file=nil,mix=nil)
|
174
|
+
increment_action_id
|
175
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Monitor', 'Channel' =>channel, 'File' => file, 'Mix' => mix},@timeout)
|
176
|
+
end
|
177
|
+
|
178
|
+
def mailbox_status(mailbox=nil)
|
179
|
+
increment_action_id
|
180
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'MailboxStatus', 'Mailbox' => mailbox},@timeout)
|
181
|
+
end
|
182
|
+
|
183
|
+
def queue_status
|
184
|
+
increment_action_id
|
185
|
+
@client.send_action({'ActionID' => @action_id, 'Action' => 'QueueStatus'}, @timeout)
|
186
|
+
end
|
187
|
+
|
188
|
+
def mailbox_count(mailbox=nil)
|
189
|
+
increment_action_id
|
190
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'MailboxCount', 'Mailbox' => mailbox},@timeout)
|
191
|
+
end
|
192
|
+
|
193
|
+
# h is a hash with the following keys. keys that are nil will not be passed to asterisk.
|
194
|
+
# * Channel
|
195
|
+
# * Context
|
196
|
+
# * Exten
|
197
|
+
# * Priority
|
198
|
+
# * Timeout
|
199
|
+
# * CallerID
|
200
|
+
# * Variable
|
201
|
+
# * Account
|
202
|
+
# * Application
|
203
|
+
# * Data
|
204
|
+
# * Async
|
205
|
+
# If Async has a value, the method will wait until the call is hungup or fails. On hangup,
|
206
|
+
# Asterisk will response with Hangup event, and on failure it will respond with an OriginateFailed event.
|
207
|
+
# If Async is nil, the method will return immediately and the associated events can be obtained by calling
|
208
|
+
# find_events() or get_events().
|
209
|
+
def originate(h={})
|
210
|
+
increment_action_id
|
211
|
+
return @client.send_action({'ActionID' => @action_id,
|
212
|
+
'Action' => 'Originate',
|
213
|
+
'Channel' => h['Channel'],
|
214
|
+
'Context' => h['Context'],
|
215
|
+
'Exten' =>h['Exten'],
|
216
|
+
'Priority' =>h['Priority'],
|
217
|
+
'Timeout' =>h['Timeout'],
|
218
|
+
'CallerID' =>h['CallerID'],
|
219
|
+
'Variable' =>h['Variable'],
|
220
|
+
'Account' =>h['Account'],
|
221
|
+
'Application' =>h['Application'],
|
222
|
+
'Data' =>h['Data'],
|
223
|
+
'Async' => h['Async']}.delete_if {|key, value| value.nil? },@timeout)
|
224
|
+
end
|
225
|
+
|
226
|
+
def parked_calls
|
227
|
+
increment_action_id
|
228
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'ParkedCalls'},@timeout)
|
229
|
+
end
|
230
|
+
|
231
|
+
def ping
|
232
|
+
increment_action_id
|
233
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Ping'},@timeout)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Queues is just like IAXpeers. You can use find_events('any','default') to get the response from the state queue.
|
237
|
+
def queues
|
238
|
+
increment_action_id
|
239
|
+
@client.send_action({'ActionID' => @action_id, 'Action' => 'Queues'},@timeout)
|
240
|
+
return [{'Response' => 'Success'}]
|
241
|
+
end
|
242
|
+
|
243
|
+
# h is a hash with the following keys. keys that are nil will not be passed to asterisk.
|
244
|
+
# * Channel
|
245
|
+
# * ExtraChannel
|
246
|
+
# * Context
|
247
|
+
# * Exten
|
248
|
+
# * Priority
|
249
|
+
def redirect(h={})
|
250
|
+
increment_action_id
|
251
|
+
return @client.send_action({'ActionID' => @action_id,
|
252
|
+
'Action' => 'Redirect',
|
253
|
+
'Channel' => h['Channel'],
|
254
|
+
'ExtraChannel' => h['ExtraChannel'],
|
255
|
+
'Context' => h['Context'],
|
256
|
+
'Exten' => h['Exten'],
|
257
|
+
'Priority' => h['Priority']}.delete_if {|key,value| value.nil?},@timeout)
|
258
|
+
end
|
259
|
+
|
260
|
+
def setvar(channel,variable,value)
|
261
|
+
increment_action_id
|
262
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'SetVar', 'Channel' => channel, 'Variable' => variable, 'Value' => value},@timeout)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Unlike IAXpeers, SIPpeers returns an event for each peer that is easily parsed and usable.
|
266
|
+
def sip_peers
|
267
|
+
increment_action_id
|
268
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'SIPpeers'},@timeout)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Detailed information about a particular peer.
|
272
|
+
def sip_show_peer(peer)
|
273
|
+
increment_action_id
|
274
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'SIPshowpeer', 'Peer' => peer},@timeout)
|
275
|
+
end
|
276
|
+
|
277
|
+
def status(channel=nil)
|
278
|
+
increment_action_id
|
279
|
+
if channel.nil?
|
280
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Status'},@timeout)
|
281
|
+
else
|
282
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'Status', 'Channel' => channel},@timeout)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def stop_monitor(channel=nil)
|
287
|
+
increment_action_id
|
288
|
+
return @client.send_action({'ActionID' => @action_id, 'Action' => 'StopMonitor', 'Channel' =>channel},@timeout)
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
def increment_action_id
|
293
|
+
@action_id = Time.now().to_f
|
294
|
+
end
|
295
|
+
|
296
|
+
def send_action(action=nil,tout=nil)
|
297
|
+
increment_action_id
|
298
|
+
action['ActionID'] = @action_id
|
299
|
+
return @client.send_action(action,tout)
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
# To run a standalone Rami server create a Server instance and then call it's run() method.
|
305
|
+
# The server will maintain one open connection to asterisk. It uses one thread to constantly read responses and stick them into
|
306
|
+
# the appropriate queue.
|
307
|
+
#
|
308
|
+
# The server uses two queues to hold responses from asterisk. The action queue holds all responses that contain an ActionID.
|
309
|
+
# The state queue holds all responses that do not have an ActionID. The action queue is only used internally, while the state
|
310
|
+
# queue can be queried via Client.get_events and Client.find_events.
|
311
|
+
#
|
312
|
+
# For an example of how to run a standalone server, see bin/server.rb. For using the Server and Client classes together without
|
313
|
+
# starting a standalone server see the Client documentation.
|
314
|
+
class Server
|
315
|
+
# If set to 1, console logging is turned on. Default 0
|
316
|
+
attr_writer :console
|
317
|
+
# The number of responses to hold in the state queue. The state queue is a FIFO list. Default is 100.
|
318
|
+
attr_writer :event_cache
|
319
|
+
|
320
|
+
Thread.current.abort_on_exception=true
|
321
|
+
|
322
|
+
# Takes a hash with the following keys:
|
323
|
+
# * host - hostname the AMI is running on
|
324
|
+
# * port - port number the AMI is running on
|
325
|
+
# * username - AMI username
|
326
|
+
# * secret - AMI secret
|
327
|
+
# * console - Set to 1 for console logging. Default is 0 (off)
|
328
|
+
# * event_cache - Number of responses to hold in the event queue. Default 100
|
329
|
+
#
|
330
|
+
# console and event_cache are also attributes, so they can be changed after calling Server.new
|
331
|
+
def initialize(options = {})
|
332
|
+
@console = options['console'] || 0
|
333
|
+
@username = options['username'] || 'asterisk'
|
334
|
+
@secret = options['secret'] || 'secret'
|
335
|
+
@host = options['host'] || 'localhost'
|
336
|
+
@port = options['port'] || 5038
|
337
|
+
@event_cache = options['event_cache'] || 100
|
338
|
+
|
339
|
+
@eventcount = 1
|
340
|
+
|
341
|
+
@sock = nil
|
342
|
+
@socklock = nil
|
343
|
+
@socklock.extend(MonitorMixin)
|
344
|
+
|
345
|
+
@action_events = []
|
346
|
+
@action_events.extend(MonitorMixin)
|
347
|
+
@action_events_pending = @action_events.new_cond
|
348
|
+
|
349
|
+
@state_events = []
|
350
|
+
@state_events.extend(MonitorMixin)
|
351
|
+
@state_events_pending = @state_events.new_cond
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
|
356
|
+
|
357
|
+
|
358
|
+
private
|
359
|
+
|
360
|
+
def logger(type,msg)
|
361
|
+
if @console == 1
|
362
|
+
print "#{Time.now} #{type} #{@eventcount}: #{msg}"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
|
368
|
+
def connect
|
369
|
+
puts "Connecting to Asterisk Manager Interface at #{@host}:#{@port}"
|
370
|
+
@sock = nil
|
371
|
+
begin
|
372
|
+
@sock = TCPSocket.new(@host,@port)
|
373
|
+
rescue => e
|
374
|
+
$stderr.puts <<-MSG
|
375
|
+
|
376
|
+
* Adhearsion First Time Setup
|
377
|
+
*
|
378
|
+
* Whoops! Adhearsion tried to connect to the Asterisk Manager Interface
|
379
|
+
* at #{@host}:#{@port} but the connection was refused! If you don't wish
|
380
|
+
* to use AMI, set the 'enabled' option in config/helpers/manager_proxy.yml
|
381
|
+
* to false. If you *do* wish to use it, ensure /etc/asterisk/manager.conf
|
382
|
+
* is configured properly and that it matches the manager_proxy.yml file.
|
383
|
+
*
|
384
|
+
* Note: Asterisk Manager Interface integration is recommended.
|
385
|
+
|
386
|
+
MSG
|
387
|
+
$HUTDOWN.now!
|
388
|
+
return
|
389
|
+
end
|
390
|
+
login = {'Action' => 'login', 'Username' => @username, 'Secret' => @secret, 'Events' => 'Off'}
|
391
|
+
writesock(login)
|
392
|
+
accum = {}
|
393
|
+
login = 0
|
394
|
+
status = Timeout.timeout(10) do
|
395
|
+
while login == 0
|
396
|
+
@sock.each("\r\n") do |line|
|
397
|
+
if line.include?(':')
|
398
|
+
key,value = parseline(line) if line.include?(':')
|
399
|
+
accum[key] = value
|
400
|
+
end
|
401
|
+
logger('RECV',"#{line}")
|
402
|
+
if line == "\r\n" and accum['Message'] == 'Authentication accepted' and accum['Response'] == 'Success'
|
403
|
+
login =1
|
404
|
+
@eventcount += 1
|
405
|
+
break
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
return true
|
410
|
+
end
|
411
|
+
rescue Timeout::Error => e
|
412
|
+
puts "LOGIN TIMEOUT"
|
413
|
+
return false
|
414
|
+
end
|
415
|
+
|
416
|
+
|
417
|
+
def mainloop
|
418
|
+
|
419
|
+
ast_reader = Thread.new do
|
420
|
+
Thread.current.abort_on_exception=true
|
421
|
+
begin
|
422
|
+
linecount = 0
|
423
|
+
loop do
|
424
|
+
|
425
|
+
event = {}
|
426
|
+
@sock.each("\r\n") do |line|
|
427
|
+
linecount += 1
|
428
|
+
type = 'state'
|
429
|
+
logger('RECV', "#{line}")
|
430
|
+
if line == "\r\n"
|
431
|
+
if event.size == 0
|
432
|
+
logger('MSG',"RECEIVED EXTRA CR/LF #{line}")
|
433
|
+
next
|
434
|
+
end
|
435
|
+
if event['ActionID']
|
436
|
+
type = 'action'
|
437
|
+
end
|
438
|
+
|
439
|
+
logger('MSG',"finished (type=#{type}) #{line}")
|
440
|
+
|
441
|
+
if type == 'action'
|
442
|
+
@action_events.synchronize do
|
443
|
+
@action_events << event.clone
|
444
|
+
event.clear
|
445
|
+
@action_events_pending.signal
|
446
|
+
end
|
447
|
+
elsif type == 'state'
|
448
|
+
@state_events.synchronize do
|
449
|
+
@state_events << event.clone
|
450
|
+
if @state_events.size >= @event_cache
|
451
|
+
@state_events.shift
|
452
|
+
end
|
453
|
+
event.clear
|
454
|
+
@state_events_pending.signal
|
455
|
+
end
|
456
|
+
end
|
457
|
+
@eventcount += 1
|
458
|
+
elsif line =~/^[\w\s\/-]*:[\s]*.*\r\n$/
|
459
|
+
key,value = parseline(line)
|
460
|
+
if key == 'ActionID'
|
461
|
+
value = value.gsub(' ','')
|
462
|
+
end
|
463
|
+
event[key] = value
|
464
|
+
else
|
465
|
+
event[linecount] = line
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
rescue IOError => e
|
470
|
+
puts "Socket disconnected #{e}"
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def parseline(line)
|
476
|
+
if line =~/(^[\w\s\/-]*:[\s]*)(.*\r\n$)/
|
477
|
+
key = $1
|
478
|
+
value = $2
|
479
|
+
key = key.gsub(/[\s:]*/,'')
|
480
|
+
value = value.gsub(/\r\n/,'')
|
481
|
+
return [key,value]
|
482
|
+
else
|
483
|
+
return ["UNKNOWN","UNKNOWN"]
|
484
|
+
end
|
485
|
+
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
def writesock(action)
|
490
|
+
@socklock.synchronize do
|
491
|
+
action.each do |key,value|
|
492
|
+
@sock.write("#{key}: #{value}\r\n")
|
493
|
+
logger('SEND',"#{key}: #{value}\r\n")
|
494
|
+
end
|
495
|
+
@sock.write("\r\n")
|
496
|
+
logger('SEND',"\r\n")
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
public
|
502
|
+
|
503
|
+
|
504
|
+
# Starts the server and connects to asterisk.
|
505
|
+
def run
|
506
|
+
if connect
|
507
|
+
puts "#{Time.now} MSG: LOGGED IN"
|
508
|
+
else
|
509
|
+
puts "#{Time.now} MSG: LOGIN FAILED"
|
510
|
+
exit
|
511
|
+
end
|
512
|
+
mainloop
|
513
|
+
end
|
514
|
+
|
515
|
+
def stop
|
516
|
+
@sock.close
|
517
|
+
end
|
518
|
+
|
519
|
+
# Should only be called via Client
|
520
|
+
def find_events(key=nil,value=nil)
|
521
|
+
logger('find_events',"#{key}: #{value}")
|
522
|
+
found = []
|
523
|
+
@state_events.synchronize do
|
524
|
+
if @state_events.empty?
|
525
|
+
return found
|
526
|
+
else
|
527
|
+
@state_events_pending.wait_while {@state_events.empty?}
|
528
|
+
@state_events.clone.each do |e|
|
529
|
+
if key == 'any' and e.to_s =~/#{value}/
|
530
|
+
found.push(e)
|
531
|
+
@state_events.delete(e)
|
532
|
+
elsif key != 'any' and e[key] =~/#{value}/
|
533
|
+
found.push(e)
|
534
|
+
@state_events.delete(e)
|
535
|
+
end
|
536
|
+
end
|
537
|
+
return found
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# Should only be called via Client
|
543
|
+
def get_events
|
544
|
+
found = []
|
545
|
+
@state_events.synchronize do
|
546
|
+
if @state_events.empty?
|
547
|
+
return found
|
548
|
+
else
|
549
|
+
@state_events_pending.wait_while {@state_events.empty?}
|
550
|
+
@state_events.clone.each do |e|
|
551
|
+
found.push(e)
|
552
|
+
end
|
553
|
+
@state_events.clear
|
554
|
+
return found
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
# Should only be called via Client
|
560
|
+
def send_action(action=nil,t=10)
|
561
|
+
sent_id = action['ActionID'].to_s
|
562
|
+
result = []
|
563
|
+
finished = 0
|
564
|
+
status = Timeout.timeout(t) do
|
565
|
+
writesock(action)
|
566
|
+
|
567
|
+
## Some action responses have no action id or specific formatting, so we just return immediately and the caller can call
|
568
|
+
## get_events or find_events to get the response.
|
569
|
+
|
570
|
+
## IAXpeer - Just return immediately
|
571
|
+
if action['Action'] == 'IAXpeers'
|
572
|
+
finished =1
|
573
|
+
return
|
574
|
+
end
|
575
|
+
|
576
|
+
## Queues - Just return immediately
|
577
|
+
if action['Action'] == 'Queues'
|
578
|
+
finished =1
|
579
|
+
return
|
580
|
+
end
|
581
|
+
|
582
|
+
|
583
|
+
while finished == 0
|
584
|
+
@action_events.synchronize do
|
585
|
+
@action_events_pending.wait_while {@action_events.empty?}
|
586
|
+
@action_events.clone.each do |e|
|
587
|
+
|
588
|
+
## Action responses that contain an ActionID
|
589
|
+
if e['ActionID'].to_s == sent_id
|
590
|
+
|
591
|
+
## Ping - Single response has ActionID
|
592
|
+
if action['Action'] == 'Ping' and e['Response'].gsub(/\s/,'') == 'Pong'
|
593
|
+
@action_events.delete(e)
|
594
|
+
result << e
|
595
|
+
finished = 1
|
596
|
+
end
|
597
|
+
|
598
|
+
## Command - Single response has ActionID
|
599
|
+
if action['Action'] == 'Command' and e['Response'].gsub(/\s/,'') == 'Follows'
|
600
|
+
@action_events.delete(e)
|
601
|
+
result << e
|
602
|
+
finished = 1
|
603
|
+
end
|
604
|
+
|
605
|
+
## Hangup - Single response has ActionID
|
606
|
+
if action['Action'] == 'Hangup'
|
607
|
+
@action_events.delete(e)
|
608
|
+
result << e
|
609
|
+
finished = 1
|
610
|
+
end
|
611
|
+
|
612
|
+
## ExtensionState - Single response has ActionID
|
613
|
+
if action['Action'] == 'ExtensionState'
|
614
|
+
@action_events.delete(e)
|
615
|
+
result << e
|
616
|
+
finished = 1
|
617
|
+
end
|
618
|
+
|
619
|
+
## SetVar - Single response has ActionID
|
620
|
+
if action['Action'] == 'SetVar'
|
621
|
+
@action_events.delete(e)
|
622
|
+
result << e
|
623
|
+
finished = 1
|
624
|
+
end
|
625
|
+
|
626
|
+
## GetVar - Single response has ActionID
|
627
|
+
if action['Action'] == 'GetVar'
|
628
|
+
@action_events.delete(e)
|
629
|
+
result << e
|
630
|
+
finished = 1
|
631
|
+
end
|
632
|
+
|
633
|
+
## Redirect - Single response has ActionID
|
634
|
+
if action['Action'] == 'Redirect'
|
635
|
+
@action_events.delete(e)
|
636
|
+
result << e
|
637
|
+
finished = 1
|
638
|
+
end
|
639
|
+
|
640
|
+
## DBPut - Single response has ActionID
|
641
|
+
if action['Action'] == 'DBPut'
|
642
|
+
@action_events.delete(e)
|
643
|
+
result << e
|
644
|
+
finished = 1
|
645
|
+
end
|
646
|
+
|
647
|
+
## DBGet - Single response has ActionID
|
648
|
+
if action['Action'] == 'DBGet' and (e['Response'] == 'Error' or e['Event'] == 'DBGetResponse')
|
649
|
+
@action_events.delete(e)
|
650
|
+
result << e
|
651
|
+
finished = 1
|
652
|
+
end
|
653
|
+
|
654
|
+
## Monitor - Single response has ActionID
|
655
|
+
if action['Action'] == 'Monitor'
|
656
|
+
@action_events.delete(e)
|
657
|
+
result << e
|
658
|
+
finished = 1
|
659
|
+
end
|
660
|
+
|
661
|
+
## Stop Monitor - Single response has ActionID
|
662
|
+
if action['Action'] == 'StopMonitor'
|
663
|
+
@action_events.delete(e)
|
664
|
+
result << e
|
665
|
+
finished = 1
|
666
|
+
end
|
667
|
+
|
668
|
+
## ChangeMonitor - Single response has ActionID
|
669
|
+
if action['Action'] == 'ChangeMonitor'
|
670
|
+
@action_events.delete(e)
|
671
|
+
result << e
|
672
|
+
finished = 1
|
673
|
+
end
|
674
|
+
|
675
|
+
## MailboxStatus - Single response has ActionID
|
676
|
+
if action['Action'] == 'MailboxStatus'
|
677
|
+
@action_events.delete(e)
|
678
|
+
result << e
|
679
|
+
finished = 1
|
680
|
+
end
|
681
|
+
|
682
|
+
## MailboxCount - Single response has ActionID
|
683
|
+
if action['Action'] == 'MailboxCount'
|
684
|
+
@action_events.delete(e)
|
685
|
+
result << e
|
686
|
+
finished = 1
|
687
|
+
end
|
688
|
+
|
689
|
+
## AbsoluteTimeout - Single response has ActionID
|
690
|
+
if action['Action'] == 'AbsoluteTimeout'
|
691
|
+
@action_events.delete(e)
|
692
|
+
result << e
|
693
|
+
finished = 1
|
694
|
+
end
|
695
|
+
|
696
|
+
## SIPshowpeer - Single response has ActionID
|
697
|
+
if action['Action'] == 'SIPshowpeer'
|
698
|
+
@action_events.delete(e)
|
699
|
+
result << e
|
700
|
+
finished = 1
|
701
|
+
end
|
702
|
+
|
703
|
+
## Logoff - Single response has ActionID
|
704
|
+
if action['Action'] == 'Logoff'
|
705
|
+
@action_events.delete(e)
|
706
|
+
result << e
|
707
|
+
finished = 1
|
708
|
+
end
|
709
|
+
|
710
|
+
|
711
|
+
## Originate - Single response has ActionID, multiple events generated
|
712
|
+
## end event is Hangup or OriginateFailed.
|
713
|
+
if action['Action'] == 'Originate'
|
714
|
+
if action['Async']
|
715
|
+
if action['Action'] == 'Originate' and e['Message'] == 'Originate successfully queued'
|
716
|
+
@action_events.delete(e)
|
717
|
+
result << e
|
718
|
+
finished = 1
|
719
|
+
end
|
720
|
+
else
|
721
|
+
eventfinished =0
|
722
|
+
while eventfinished == 0
|
723
|
+
@state_events.synchronize do
|
724
|
+
@state_events_pending.wait_while {@state_events.empty?}
|
725
|
+
@state_events.clone.each do |s|
|
726
|
+
if s['Channel'] =~/#{action['Channel']}/ and (s['Event'] == 'Hangup' or s['Event'] == 'OriginateFailed')
|
727
|
+
@state_events.delete(s)
|
728
|
+
result << s
|
729
|
+
eventfinished =1
|
730
|
+
finished =1
|
731
|
+
elsif s['Channel'] =~/#{action['Channel']}/
|
732
|
+
@state_events.delete(s)
|
733
|
+
result << s
|
734
|
+
end
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
## ParkedCalls - multiple responses has ActionID
|
742
|
+
if action['Action'] == 'ParkedCalls'
|
743
|
+
if e['Message'] == 'Parked calls will follow'
|
744
|
+
@action_events.delete(e)
|
745
|
+
elsif e['Event'] == 'ParkedCallsComplete'
|
746
|
+
@action_events.delete(e)
|
747
|
+
finished =1
|
748
|
+
else
|
749
|
+
@action_events.delete(e)
|
750
|
+
result << e
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
## QueueStatus - multiple responses has ActionID
|
755
|
+
if action['Action'] == 'QueueStatus'
|
756
|
+
if e['Message'] == 'Queue status will follow'
|
757
|
+
@action_events.delete(e)
|
758
|
+
elsif e['Event'] == 'QueueStatusComplete'
|
759
|
+
@action_events.delete(e)
|
760
|
+
finished =1
|
761
|
+
else
|
762
|
+
@action_events.delete(e)
|
763
|
+
result << e
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
## SIPpeers - multiple responses has ActionID
|
768
|
+
if action['Action'] == 'SIPpeers'
|
769
|
+
if e['Message'] == 'Peer status list will follow'
|
770
|
+
@action_events.delete(e)
|
771
|
+
elsif e['Event'] == 'PeerlistComplete'
|
772
|
+
@action_events.delete(e)
|
773
|
+
finished =1
|
774
|
+
elsif e['Event'] == 'PeerEntry'
|
775
|
+
@action_events.delete(e)
|
776
|
+
result << e
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
## Agents - multiple responses has ActionID
|
781
|
+
if action['Action'] == 'Agents'
|
782
|
+
if e['Message'] == 'Agents will follow'
|
783
|
+
@action_events.delete(e)
|
784
|
+
elsif e['Event'] == 'AgentsComplete'
|
785
|
+
@action_events.delete(e)
|
786
|
+
finished =1
|
787
|
+
elsif e['Event'] == 'Agents'
|
788
|
+
@action_events.delete(e)
|
789
|
+
result << e
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
## Status - multiple responses has ActionID
|
794
|
+
if action['Action'] == 'Status'
|
795
|
+
if e['Message'] == 'Channel status will follow'
|
796
|
+
@action_events.delete(e)
|
797
|
+
elsif e['Event'] == 'StatusComplete'
|
798
|
+
@action_events.delete(e)
|
799
|
+
finished =1
|
800
|
+
elsif e['Event'] == 'Status'
|
801
|
+
@action_events.delete(e)
|
802
|
+
result << e
|
803
|
+
end
|
804
|
+
end
|
805
|
+
|
806
|
+
end
|
807
|
+
end
|
808
|
+
end
|
809
|
+
sleep 0.10
|
810
|
+
end
|
811
|
+
end
|
812
|
+
return result
|
813
|
+
rescue Exception => e
|
814
|
+
puts "#{e}: TIMEOUT #{t} #{sent_id}"
|
815
|
+
puts e.backtrace
|
816
|
+
return result
|
817
|
+
end
|
818
|
+
|
819
|
+
end
|
820
|
+
rescue Exception => e
|
821
|
+
puts e
|
822
|
+
end
|