adhearsion 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/LICENSE +339 -0
  2. data/Rakefile +108 -0
  3. data/ahn +195 -0
  4. data/lib/adhearsion.rb +402 -0
  5. data/lib/constants.rb +20 -0
  6. data/lib/core_extensions.rb +157 -0
  7. data/lib/database_functions.rb +76 -0
  8. data/lib/rami.rb +822 -0
  9. data/lib/servlet_container.rb +146 -0
  10. data/new_projects/Rakefile +100 -0
  11. data/new_projects/config/adhearsion.sqlite3 +0 -0
  12. data/new_projects/config/adhearsion.yml +11 -0
  13. data/new_projects/config/database.rb +50 -0
  14. data/new_projects/config/database.yml +10 -0
  15. data/new_projects/config/helpers/drb_server.yml +43 -0
  16. data/new_projects/config/helpers/factorial.alien.c.yml +1 -0
  17. data/new_projects/config/helpers/manager_proxy.yml +7 -0
  18. data/new_projects/config/helpers/micromenus.yml +1 -0
  19. data/new_projects/config/helpers/micromenus/collab.rb +55 -0
  20. data/new_projects/config/helpers/micromenus/images/tux.bmp +0 -0
  21. data/new_projects/config/helpers/micromenus/javascripts/builder.js +131 -0
  22. data/new_projects/config/helpers/micromenus/javascripts/controls.js +834 -0
  23. data/new_projects/config/helpers/micromenus/javascripts/dragdrop.js +944 -0
  24. data/new_projects/config/helpers/micromenus/javascripts/effects.js +956 -0
  25. data/new_projects/config/helpers/micromenus/javascripts/prototype.js +2319 -0
  26. data/new_projects/config/helpers/micromenus/javascripts/scriptaculous.js +51 -0
  27. data/new_projects/config/helpers/micromenus/javascripts/slider.js +278 -0
  28. data/new_projects/config/helpers/micromenus/javascripts/unittest.js +557 -0
  29. data/new_projects/config/helpers/micromenus/stylesheets/firefox.css +10 -0
  30. data/new_projects/config/helpers/micromenus/stylesheets/firefox.xul.css +44 -0
  31. data/new_projects/config/helpers/weather.yml +1 -0
  32. data/new_projects/config/helpers/xbmc.yml +1 -0
  33. data/new_projects/config/migration.rb +53 -0
  34. data/new_projects/extensions.rb +56 -0
  35. data/new_projects/helpers/drb_server.rb +32 -0
  36. data/new_projects/helpers/factorial.alien.c +32 -0
  37. data/new_projects/helpers/manager_proxy.rb +43 -0
  38. data/new_projects/helpers/micromenus.rb +374 -0
  39. data/new_projects/helpers/oscar_wilde_quotes.rb +197 -0
  40. data/new_projects/helpers/weather.rb +85 -0
  41. data/new_projects/helpers/xbmc.rb +12 -0
  42. data/new_projects/logs/database.log +0 -0
  43. data/test/core_extensions_test.rb +26 -0
  44. data/test/dial_test.rb +43 -0
  45. data/test/stress_tests/test.rb +13 -0
  46. data/test/stress_tests/test.yml +13 -0
  47. data/test/test_micromenus.rb +0 -0
  48. 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
@@ -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