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,10 @@
1
+ body {
2
+ font-size: 20px;
3
+ font-family: "Bitstream Vera Sans", Tahoma, "Trebuchet MS", sans-serif; }
4
+ a {
5
+ padding: 3px;
6
+ color: black; }
7
+
8
+ a:hover {
9
+ background-color: black;
10
+ color: white; }
@@ -0,0 +1,44 @@
1
+ window {
2
+ background-color: grey;
3
+ padding: 20px 10px;
4
+ }
5
+ #haupt {
6
+ text-align: center;
7
+ display: block;
8
+ -moz-border-radius: 20px;
9
+ border: 10px solid black;
10
+ background-color: white;
11
+ padding: 10px;
12
+ padding-bottom: 20px;
13
+ }
14
+
15
+ label.header {
16
+ font-size: 30px;
17
+ text-align: center;
18
+ }
19
+
20
+ a {
21
+ background-color: #EEE;
22
+ padding: 2px;
23
+ display: block;
24
+ margin: 4px 0;
25
+ border: 5px #EEE solid;
26
+ -moz-border-radius: 5px;
27
+ color: black;
28
+ text-align: left;
29
+ text-decoration: none;
30
+ }
31
+
32
+ a:hover {
33
+ color: white;
34
+ background-color: black;
35
+ padding: 2px;
36
+ border: 5px black solid;
37
+ -moz-border-radius: 5px;
38
+ }
39
+
40
+ description {
41
+ text-align: center;
42
+ padding: 15px;
43
+ width: 100%;
44
+ }
@@ -0,0 +1 @@
1
+ units: fahrenheit # can be 'fahrenheit' or 'celsuis' (case sensitive)
@@ -0,0 +1 @@
1
+ ip: 192.168.1.136:80
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'rubygems'
4
+ require 'active_record'
5
+
6
+ # A migration script uses a database configuration and creates tables
7
+ # very conveniently in a database-agnostic way. Below, add any customizations
8
+ # to the sample schema or leave it as-is. When done, execute this script.
9
+
10
+ ActiveRecord::Base.establish_connection YAML.load_file('config/database.yml')
11
+
12
+ class CreateUsers < ActiveRecord::Migration
13
+ # Available column types are :primary_key, :string, :text, :integer,
14
+ # :float, :datetime, :timestamp, :time, :date, :binary, and :boolean
15
+ def self.up
16
+ create_table :users do |t|
17
+ t.column :name, :string
18
+ t.column :callerid_name, :string
19
+ t.column :callerid_num, :string
20
+ t.column :group_id, :integer # Foreign key
21
+ t.column :ivr_extension, :string
22
+ t.column :extension, :string
23
+ t.column :email, :string
24
+ t.column :im_username, :string
25
+ t.column :im_provider, :string
26
+ # t.column :billed_time, :integer, :null => false
27
+ end
28
+ end
29
+
30
+ def self.down
31
+ drop_table :users
32
+ end
33
+ end
34
+
35
+ class CreateGroups < ActiveRecord::Migration
36
+ def self.up
37
+ create_table :groups do |t|
38
+ t.column :name, :string
39
+ t.column :administrator_email, :string
40
+ t.column :callerid_name, :string
41
+ t.column :callerid_num, :string
42
+ #t.column :usage_limit, :integer
43
+ end
44
+ end
45
+
46
+ def self.down
47
+ drop_table :groups
48
+ end
49
+ end
50
+
51
+ # Run "rake migrate" to run this script properly.
52
+ # CreateUsers.up
53
+ # CreateGroups.up
@@ -0,0 +1,56 @@
1
+
2
+ from_pwnyourphone {
3
+ # You rock! Be awesome and record us a message! We'll put supportive messages on the podcast!"
4
+ # Get ready to record. 5, 4, 3, 1 *BEEP*
5
+ play %(you-rock record-us-a-message we-put-messages-on-podcast get-ready five-countdown beep)
6
+ record :for => 3.minutes, :to => "/recordings/pwner.#{Time.now.to_i}.#{calleridnumber}.gsm"
7
+ }
8
+
9
+ internal {
10
+ callee = User.find_by_ivr_extension extension
11
+ if callee
12
+
13
+ voicemail extension if last_dial_status != :answer
14
+
15
+ #elsif extension.to_s =~ //#/^(((\d{1,3})?\d{1{3})?[1-9]\d{2})?[1-9]\d{6}$/
16
+ # puts "got here"
17
+ # dial "SIP/#{extension}@sipphone"
18
+ else
19
+ case extension
20
+ when 21 then play weather_report
21
+ when 32 then dial "SIP/zoip@demo.zoip.org"
22
+ when 888 then loop { print '.'; XBMC.sendkey XBMC.translate(wait_for_digit) }
23
+ when 9999
24
+ if rand(20) == 0
25
+ play %W(a-connect-charge-of #{rand(50) + 10} cents-per-minute will-apply)
26
+ sleep 1.second
27
+ play %w(just-kidding-not-upset)
28
+ end
29
+ check_voicemail caller.voicemailbox
30
+ else
31
+ play 'all-your-base'
32
+ end
33
+ end
34
+ }
35
+
36
+ old {
37
+ case extension
38
+ when 1 then play weather_report("Richardson, Texas")
39
+ when 2 then play weather_report(input(5, :play => 'zip-code'))
40
+ when 3 then +xbmc
41
+ when 4
42
+ dial :tweedledee
43
+ end
44
+ }
45
+
46
+ xbmc {
47
+ loop {
48
+ print '.'
49
+ XBMC.sendkey XBMC.translate(wait_for_digit)
50
+ }
51
+ }
52
+
53
+ from_gizmo {
54
+ #dial :tweedledum
55
+ +xbmc
56
+ }
@@ -0,0 +1,32 @@
1
+
2
+ require 'drb'
3
+ require 'drb/acl'
4
+ require 'thread'
5
+
6
+ # Load the access control list
7
+ config = $HELPERS['drb_server']
8
+
9
+ permissions = []
10
+ # For greater control over the ACL
11
+ if config['raw_acl']
12
+ permissions = config['raw_acl'].flatten
13
+ else
14
+ [config['deny']].flatten.each { |ip| permissions << "deny" << ip }
15
+ [config['allow']].flatten.each { |ip| permissions << "allow" << ip }
16
+ end
17
+
18
+ DRb.install_acl ACL.new(permissions)
19
+
20
+ host = config['host'] || 'localhost'
21
+ port = config['port'] || 9050
22
+ DRb.start_service "druby://#{host}:#{port}", PBX
23
+
24
+ puts "Started DRb server on #{DRb.uri}."
25
+ puts "DRb Server Access Control List:"
26
+ 0.step permissions.length-1, 2 do |i|
27
+ puts " #{permissions[i].upcase} #{permissions[i+1]}"
28
+ end
29
+
30
+ $HUTDOWN.hook do
31
+ DRb.stop_service
32
+ end
@@ -0,0 +1,32 @@
1
+
2
+ /*
3
+ =begin Adhearsion metadata
4
+
5
+ name: Native Factorial
6
+ author:
7
+ name: Jay Phillips
8
+ blog: http://jicksta.com
9
+ email: Jicksta -at- Gmail.com
10
+ gems:
11
+ - soap4r
12
+ - rubyinline: >= 0.8.2
13
+ instructions: >
14
+ Yes, this is a pure C file!!!
15
+ This is an example of writing Adhearsion extensions in
16
+ other languages. The first time this file is executed
17
+ it will be compiled and the binary form will be cached.
18
+
19
+ If your Adhearsion system is heavily dependent on
20
+ an intensive helper, it may be advantageous to rewrite
21
+ it in a language such as C or C++ and use it like this.
22
+
23
+ =end
24
+ */
25
+
26
+ int fast_factorial(int input) {
27
+ int sum = 0, count = 1;
28
+ while(count <= input) {
29
+ sum += count++;
30
+ }
31
+ return sum;
32
+ }
@@ -0,0 +1,43 @@
1
+ require 'rami'
2
+ class PBX
3
+ include Rami
4
+
5
+ @@sip_users = {}
6
+
7
+ @@rami_server_thread = Thread.current
8
+
9
+ @@rami_server = Rami::Server.new $HELPERS.manager_proxy
10
+ @@rami_server.console = 1
11
+ @@rami_server.run
12
+
13
+ @@rami_client = Client.new @@rami_server
14
+ @@rami_client.timeout = 10
15
+
16
+ def self.rami_client() @@rami_client end
17
+
18
+ $HUTDOWN.hook do
19
+ @@rami_client.stop
20
+ end
21
+
22
+ def self.sip_users
23
+ if !@@sip_users[:expiration] || @@sip_users[:expiration] <= Time.now
24
+ sip_db = PBX.rami_client.command("database show SIP/Registry").first
25
+ sip_db = sip_db[ sip_db.keys.select { |x| x.is_a? Fixnum }.first ]
26
+ sip_db = sip_db.gsub( /--[A-Z ]+?--/ , '').strip
27
+ users = sip_db.split "\n"
28
+ users.collect! do |user|
29
+ fields = user.split ':'
30
+ { :username => fields[4],
31
+ :ip => fields[1].strip,
32
+ :port => fields[2],
33
+ :address => fields[6] }
34
+ end
35
+ @@sip_users[:users] = users
36
+ @@sip_users[:expiration] = 90.seconds.from_now
37
+ end
38
+ @@sip_users[:users]
39
+ end
40
+ def self.record channel, file, format, mix
41
+
42
+ end
43
+ end
@@ -0,0 +1,374 @@
1
+ # Micromenus Adhearsion helper
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
+ require 'rubygems'
19
+ require 'builder'
20
+ require 'webrick'
21
+ require 'stringio'
22
+
23
+ class WEBrick::HTTPRequest
24
+ def ip() @peeraddr[3] end
25
+ end
26
+
27
+ # Micromenu catchers are special hooks that allow integration
28
+ # between micromenus and incoming calls (specifically, incoming
29
+ # calls generated by the micromenus)
30
+ $MICROMENU_CALL_HOOKS = []
31
+ class << $MICROMENU_CALL_HOOKS
32
+ def purge_expired!
33
+ self.synchronize { |hooks| hooks.delete_if { |h| h.expiration < Time.now } }
34
+ end
35
+ end
36
+
37
+ class MicromenusServlet < WEBrick::HTTPServlet::AbstractServlet
38
+
39
+ class MicromenuGenerator
40
+
41
+ def initialize request, model, io=$stdout
42
+ @request, @io, @config = request, io, []
43
+ @xml = Builder::XmlMarkup.new(:target => @io, :indent => 3)
44
+ self.extend model
45
+ end
46
+
47
+ attr_accessor :name, :io, :config, :request
48
+
49
+ def process request
50
+ route = request.dup
51
+
52
+ if route.empty?
53
+ start "Error" do
54
+ build_text "Bad URL!"
55
+ build_text "Must point to a micromenu!"
56
+ end
57
+ return
58
+ end
59
+
60
+ title = " Adhearsion Micromenus"
61
+
62
+ filename = route.shift
63
+ load_menu filename
64
+ until route.empty?
65
+ segment = route.shift
66
+ broken = segment.match(/^([\w_.]+);?(\d*)$/)
67
+ segment, id = broken[1], broken[2]
68
+ id = nil if id.empty?
69
+
70
+ matches = @config.select do |x|
71
+ x[:type] == :menu && x[:text].nameify == segment
72
+ end
73
+ dest = matches[(id.simplify || 1) - 1]
74
+
75
+
76
+ if dest then
77
+ @config.clear
78
+ title = dest[:text]
79
+ dest[:block].call
80
+ else
81
+ start "Error" do
82
+ item '404 Not found!'
83
+ item "Req: #{request.inspect}"
84
+ return
85
+ end
86
+ end
87
+ end
88
+
89
+ # Let any headers override the default title
90
+ @config.each do |item|
91
+ if item[:type] == :heading
92
+ title = @config.delete(item)[:text]
93
+ break
94
+ end
95
+ end
96
+
97
+ start title do
98
+ @config.each do |item|
99
+ case item[:type]
100
+ when :menu
101
+ build_menu item[:text], item[:uri], request
102
+ when :item
103
+ build_text item[:text]
104
+ when :heading
105
+ build_header item[:text]
106
+ when :image
107
+ build_image item[:text]
108
+ when :call
109
+ build_call item[:number], item[:text]
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def load_menu filename
116
+ @config.clear
117
+ file = File.join('config','helpers', 'micromenus', filename + '.rb')
118
+ unless File.readable? file
119
+ item "Bad URL!"
120
+ item "File inexistent!"
121
+ else
122
+ eval File.read(File.join('config','helpers', 'micromenus', filename + '.rb'))
123
+ end
124
+ @config
125
+ end
126
+ def join_url url, *pages
127
+ url *= '/' if url.is_a? Array
128
+ '/' + if url.empty?
129
+ pages * '/'
130
+ else
131
+ ((url[-1] == ?/) ? url : url + "/") + pages * '/'
132
+ end
133
+ end
134
+
135
+ def get_refresh() @refresh end
136
+
137
+ module PolycomPhone
138
+
139
+ def content_type() "text/html" end
140
+
141
+ def start name='', &block
142
+ @xml.html do
143
+ @xml.head do
144
+ @xml.title name
145
+ end
146
+ @xml.body do
147
+ yield
148
+ end
149
+ end
150
+ end
151
+
152
+ def build_menu str, uri, request
153
+ #build_menu item[:text], item[:uri], request
154
+ #request.flatten! if request.is_a? Array
155
+ @xml.p { @xml.a str, :href => join_url(request, uri) }
156
+ end
157
+
158
+ def build_text str
159
+ @xml.p str
160
+ end
161
+
162
+ def build_prompt
163
+
164
+ end
165
+
166
+ def build_image filename, hash=nil
167
+ @xml.img :src => "/images/#{filename}"
168
+ end
169
+
170
+ def build_header str
171
+ @xml.h1 str
172
+ end
173
+
174
+ def build_call number, name=number
175
+ @xml.a name, :href => "tel://#{number}"
176
+ @xml.br
177
+ end
178
+ end
179
+
180
+ module XulUi
181
+
182
+ include PolycomPhone
183
+ def content_type() "application/vnd.mozilla.xul+xml" end
184
+
185
+ def start name='', &block
186
+ @xml.instruct!
187
+ @xml.instruct! 'xml-stylesheet', :href => "chrome://global/skin/", :type => "text/css"
188
+ @xml.instruct! 'xml-stylesheet', :href => "/stylesheets/firefox.xul.css", :type => "text/css"
189
+ @xml.window :title => name,
190
+ 'xmlns:html' => "http://www.w3.org/1999/xhtml",
191
+ :xmlns => "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" do
192
+ @xml.vbox :id => 'haupt' do
193
+ #@xml.label name, :class => 'header'
194
+ yield
195
+ end
196
+ end
197
+ end
198
+
199
+ def build_call number, name=number
200
+ # The tel:// UI doesn't do Firefox much good. Mexuar, maybe?
201
+ @xml.html:a, name, :href => "tel://#{number}"
202
+ @xml.html:br
203
+ end
204
+
205
+ def build_menu str, uri, request
206
+ @xml.html:a, str, :href => join_url(request, uri)
207
+ end
208
+
209
+ def build_text str
210
+ @xml.description str
211
+ @xml.html:br
212
+ end
213
+
214
+ def build_image filename, hash=nil
215
+ @xml.html:img, :src => "/images/#{filename}"
216
+ end
217
+
218
+ def build_header str
219
+ @xml.label str, :class => 'header'
220
+ end
221
+
222
+ end
223
+
224
+
225
+ module FirefoxUi
226
+
227
+ include PolycomPhone
228
+
229
+ def start name='', &block
230
+ @xml.html do
231
+ @xml.head do
232
+ @xml.title name
233
+ @xml.link :rel => 'stylesheet', :media => 'all', :href => '/stylesheets/firefox.css'
234
+ end
235
+ @xml.body do
236
+ yield
237
+ end
238
+ end
239
+ end
240
+ def build_call number, name=number
241
+ # The tel:// UI doesn't do Firefox much good. Mexuar, maybe?
242
+ @xml.a name, :href => "tel://#{number}"
243
+ @xml.br
244
+ end
245
+
246
+ end
247
+
248
+
249
+ private
250
+
251
+
252
+ def image name
253
+ name += '.bmp' unless name.index ?.
254
+ @config << {:type => :image, :text => name}
255
+ end
256
+
257
+ def refresh_every time
258
+ @refresh = time
259
+ end
260
+
261
+ def heading str
262
+ @config << {:type => :heading, :text => str}
263
+ end
264
+ alias header heading
265
+
266
+ def item title, &block
267
+ hash = {:text => title, :type => :item }
268
+ if block_given?
269
+ hash[:block], hash[:type], hash[:uri] = block, :menu, title.nameify
270
+ collisions = @config.select { |c| c[:type] == :menu && c[:text].nameify == hash[:uri]}.length
271
+ hash[:uri] += ";#{collisions + 1}" if collisions.nonzero?
272
+ end
273
+ @config << hash
274
+ end
275
+ def items array
276
+ array.each { |x| item x }
277
+ end
278
+
279
+ def guess_sip_user
280
+ return @guessed_user if @guessed_user
281
+ selection = PBX.sip_users.select { |x| x[:ip] == request.ip }.first
282
+ @guessed_user = selection ? selection.username : nil
283
+ end
284
+
285
+ def call number, name=number, &block
286
+ instance = {:type => :call, :text => name, :number => number}
287
+ if block_given?
288
+ $MICROMENU_CALL_HOOKS.purge_expired!
289
+ num = "555551337#{rand(8_999_999_999) + 1_000_000_000}"
290
+ instance[:number] = num
291
+ $MICROMENU_CALL_HOOKS.synchronize do |hooks|
292
+ hooks << { :expiration => 90.seconds.from_now, :extension => num, :hook => block }
293
+ end
294
+ end
295
+ @config << instance
296
+ end
297
+
298
+ def action title, &block
299
+ # Just like menu() but without a submenu.
300
+ # Useful for performing an action and refreshing.
301
+ end
302
+ end
303
+
304
+ USER_AGENT_MAP = {
305
+ "Polycom" => MicromenuGenerator::PolycomPhone,
306
+ "Firefox" => MicromenuGenerator::XulUi#FirefoxUi
307
+ }
308
+
309
+ def do_GET(request, response)
310
+ puts 'trying do'
311
+ response.status = 200
312
+ puts "Request from: " + request['User-Agent']
313
+
314
+ route = request.path[1..-1].split '/'
315
+ if route.first == 'images'
316
+ file = File.join(%w(config helpers micromenus images), route[1..-1])
317
+ # TODO: Handle missing files
318
+ response.content_type = WEBrick::HTTPUtils::mime_type file, WEBrick::HTTPUtils::DefaultMimeTypes
319
+ response.body = File.read file
320
+
321
+ elsif route.first == 'stylesheets'
322
+ file = File.join %w(config helpers micromenus stylesheets), route[1..-1]
323
+ response.content_type = 'text/css'
324
+ response.body = File.read file
325
+ else
326
+ mg = MicromenuGenerator.new request, resolve_brand(request['User-Agent']), StringIO.new
327
+ response.content_type = mg.content_type
328
+ response['Expires'] = 2
329
+
330
+ mg.process route
331
+
332
+ refresh = mg.get_refresh
333
+ response['Refresh'] = refresh if refresh
334
+
335
+ response.body = mg.io.string
336
+ end
337
+ end
338
+
339
+ def resolve_brand useragent
340
+ USER_AGENT_MAP.each do |k,v|
341
+ return v if useragent.index k
342
+ end
343
+ MicromenuGenerator::PolycomPhone # Polycom's the default since it's XHTML
344
+ end
345
+ end
346
+
347
+ $MICROMENU_THREAD = Thread.new do
348
+ micromenu_server = WEBrick::HTTPServer.new :Port => ($HELPERS.micromenus.port || 1337)
349
+ micromenu_server.logger = Logger.new 'logs/database.log', 10, 1.megabyte
350
+ micromenu_server.mount '/', MicromenusServlet
351
+ $HUTDOWN.hook {
352
+ micromenu_server.stop
353
+ }
354
+ micromenu_server.start
355
+ end
356
+
357
+
358
+ # This before_call hook is the magic behind the call() method in the micromenus.
359
+ before_call :low do
360
+ # PSEUDOCODE
361
+ # Check extension for format. next unless it matches
362
+ # Delete all expired hooks
363
+ # Find first match in the collection of hooks
364
+ # Pull the first match out of the collection
365
+ # Execute that match's block finish
366
+ extension = Thread.current[:VARS]['extension'].to_s
367
+
368
+ next unless extension.length == 19 && extension.starts_with?("555551337")
369
+ $MICROMENU_CALL_HOOKS.purge_expired!
370
+ match = $MICROMENU_CALL_HOOKS.detect { |x| x.extension == extension }
371
+ next unless match
372
+ Thread.current[:VARS]['context'] = :interrupted
373
+ +match.hook
374
+ end