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.
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