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