adhearsion-asterisk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +6 -0
  5. data/Guardfile +5 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +143 -0
  8. data/Rakefile +23 -0
  9. data/adhearsion-asterisk.gemspec +35 -0
  10. data/lib/adhearsion-asterisk.rb +1 -0
  11. data/lib/adhearsion/asterisk.rb +12 -0
  12. data/lib/adhearsion/asterisk/config_generator.rb +103 -0
  13. data/lib/adhearsion/asterisk/config_generator/agents.rb +138 -0
  14. data/lib/adhearsion/asterisk/config_generator/queues.rb +247 -0
  15. data/lib/adhearsion/asterisk/config_generator/voicemail.rb +238 -0
  16. data/lib/adhearsion/asterisk/config_manager.rb +60 -0
  17. data/lib/adhearsion/asterisk/plugin.rb +464 -0
  18. data/lib/adhearsion/asterisk/queue_proxy.rb +177 -0
  19. data/lib/adhearsion/asterisk/queue_proxy/agent_proxy.rb +81 -0
  20. data/lib/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy.rb +132 -0
  21. data/lib/adhearsion/asterisk/version.rb +5 -0
  22. data/spec/adhearsion/asterisk/config_generators/agents_spec.rb +258 -0
  23. data/spec/adhearsion/asterisk/config_generators/queues_spec.rb +322 -0
  24. data/spec/adhearsion/asterisk/config_generators/voicemail_spec.rb +306 -0
  25. data/spec/adhearsion/asterisk/config_manager_spec.rb +125 -0
  26. data/spec/adhearsion/asterisk/plugin_spec.rb +618 -0
  27. data/spec/adhearsion/asterisk/queue_proxy/agent_proxy_spec.rb +90 -0
  28. data/spec/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy_spec.rb +145 -0
  29. data/spec/adhearsion/asterisk/queue_proxy_spec.rb +156 -0
  30. data/spec/adhearsion/asterisk_spec.rb +9 -0
  31. data/spec/spec_helper.rb +23 -0
  32. data/spec/support/the_following_code.rb +3 -0
  33. metadata +229 -0
@@ -0,0 +1,247 @@
1
+ require File.join(File.dirname(__FILE__), '../config_generator')
2
+
3
+ module Adhearsion
4
+ module Asterisk
5
+ class ConfigGenerator
6
+ # This will generate a queues.conf file. If there is no documentation on what a method
7
+ # actually does, take a look at the documentation for its original key/value pair in
8
+ # an unedited queues.conf file. WARNING! Don't get too embedded with these method names.
9
+ # I'm still not satisfied. These settings will be greatly abstracted eventually.
10
+ class Queues < ConfigGenerator
11
+
12
+ DEFAULT_GENERAL_SECTION = {
13
+ :autofill => "yes"
14
+ }
15
+
16
+ attr_reader :general_section, :queue_definitions, :properties
17
+ def initialize
18
+ @general_section = DEFAULT_GENERAL_SECTION.clone
19
+ @properties = {}
20
+ @queue_definitions = []
21
+ super
22
+ end
23
+
24
+ def queue(name)
25
+ new_queue = QueueDefinition.new name
26
+ yield new_queue if block_given?
27
+ queue_definitions << new_queue
28
+ new_queue
29
+ end
30
+
31
+ def to_s
32
+ ConfigGenerator.warning_message +
33
+ general_section.inject("[general]") { |section,(key,value)| section + "\n#{key}=#{value}" } + "\n\n" +
34
+ queue_definitions.map(&:to_s).join("\n\n")
35
+ end
36
+ alias conf to_s
37
+
38
+ def persistent_members(yes_no)
39
+ boolean :persistentmembers => yes_no, :with => general_section
40
+ end
41
+
42
+ def monitor_type(symbol)
43
+ criteria = {:monitor => "Monitor", :mix_monitor => "MixMonitor"}
44
+ one_of_and_translate criteria, 'monitor-type' => symbol, :with => general_section
45
+ end
46
+
47
+
48
+ class QueueDefinition < ConfigGenerator
49
+
50
+ DEFAULT_QUEUE_PROPERTIES = {
51
+ :autofill => 'yes',
52
+ :eventwhencalled => 'vars',
53
+ :eventmemberstatus => 'yes',
54
+ :setinterfacevar => 'yes'
55
+ }
56
+
57
+ SUPPORTED_RING_STRATEGIES = [:ringall, :roundrobin, :leastrecent, :fewestcalls, :random, :rrmemory]
58
+
59
+ DEFAULT_SOUND_FILES = {
60
+ 'queue-youarenext' => 'queue-youarenext',
61
+ 'queue-thereare' => 'queue-thereare',
62
+ 'queue-callswaiting' => 'queue-callswaiting',
63
+ 'queue-holdtime' => 'queue-holdtime',
64
+ 'queue-minutes' => 'queue-minutes',
65
+ 'queue-seconds' => 'queue-seconds',
66
+ 'queue-thankyou' => 'queue-thankyou',
67
+ 'queue-lessthan' => 'queue-less-than',
68
+ 'queue-reporthold' => 'queue-reporthold',
69
+ 'periodic-announce' => 'queue-periodic-announce'
70
+ }
71
+
72
+ SOUND_FILE_SYMBOL_INTERPRETATIONS = {
73
+ :you_are_next => 'queue-youarenext',
74
+ :there_are => 'queue-thereare',
75
+ :calls_waiting => 'queue-callswaiting',
76
+ :hold_time => 'queue-holdtime',
77
+ :minutes => 'queue-minutes',
78
+ :seconds => 'queue-seconds',
79
+ :thank_you => 'queue-thankyou',
80
+ :less_than => 'queue-lessthan',
81
+ :report_hold => 'queue-reporthold',
82
+ :periodic_announcement => 'periodic-announce'
83
+ }
84
+
85
+ attr_reader :members, :name, :properties
86
+ def initialize(name)
87
+ @name = name
88
+ @members = []
89
+ @properties = DEFAULT_QUEUE_PROPERTIES.clone
90
+ @sound_files = DEFAULT_SOUND_FILES.clone
91
+ end
92
+
93
+ def to_s
94
+ "[#{name}]\n" +
95
+ properties.merge(@sound_files).map { |key, value| "#{key}=#{value}" }.sort.join("\n") + "\n\n" +
96
+ members.map { |member| "member => #{member}" }.join("\n")
97
+ end
98
+
99
+ def music_class(moh_identifier)
100
+ string :musicclass => moh_identifier
101
+ end
102
+
103
+ def play_on_connect(sound_file)
104
+ string :announce => sound_file
105
+ end
106
+
107
+ def strategy(symbol)
108
+ one_of SUPPORTED_RING_STRATEGIES, :strategy => symbol
109
+ end
110
+
111
+ def service_level(seconds)
112
+ int :servicelevel => seconds
113
+ end
114
+
115
+ # A context may be specified, in which if the user types a SINGLE
116
+ # digit extension while they are in the queue, they will be taken out
117
+ # of the queue and sent to that extension in this context. This context
118
+ # should obviously be a different context other than the one that
119
+ # normally forwards to Adhearsion (though the context that handles
120
+ # these digits should probably go out to Adhearsion too).
121
+ def exit_to_context_on_digit_press(context_name)
122
+ string :context => context_name
123
+ end
124
+
125
+ # Ex: ring_timeout 15.seconds
126
+ def ring_timeout(seconds)
127
+ int :timeout => seconds
128
+ end
129
+
130
+ # Ex: retry_after_waiting 5.seconds
131
+ def retry_after_waiting(seconds)
132
+ int :retry => seconds
133
+ end
134
+
135
+ # THIS IS UNSUPPORTED
136
+ def weight(number)
137
+ int :weight => number
138
+ end
139
+
140
+ # Ex: wrapup_time 1.minute
141
+ def wrapup_time(seconds)
142
+ int :wrapuptime => seconds
143
+ end
144
+
145
+
146
+ def autopause(yes_no)
147
+ boolean :autopause => yes_no
148
+ end
149
+
150
+ def maximum_length(number)
151
+ int :maxlen => number
152
+ end
153
+
154
+ def queue_status_announce_frequency(seconds)
155
+ int "announce-frequency" => seconds
156
+ end
157
+
158
+ def periodically_announce(sound_file, options={})
159
+ frequency = options.delete(:every) || 1.minute
160
+
161
+ string 'periodic-announce' => sound_file
162
+ int 'periodic-announce-frequency' => frequency
163
+ end
164
+
165
+ def announce_hold_time(seconds)
166
+ one_of [true, false, :once], "announce-holdtime" => seconds
167
+ end
168
+
169
+ def announce_round_seconds(yes_no_or_once)
170
+ int "announce-round-seconds" => yes_no_or_once
171
+ end
172
+
173
+ def monitor_format(symbol)
174
+ one_of [:wav, :gsm, :wav49], 'monitor-format' => symbol
175
+ end
176
+
177
+ def monitor_type(symbol)
178
+ criteria = {:monitor => "Monitor", :mix_monitor => "MixMonitor"}
179
+ one_of_and_translate criteria, 'monitor-type' => symbol
180
+ end
181
+
182
+ # Ex: join_empty true
183
+ # Ex: join_empty :strict
184
+ def join_empty(yes_no_or_strict)
185
+ one_of [true, false, :strict], :joinempty => yes_no_or_strict
186
+ end
187
+
188
+ def leave_when_empty(yes_no)
189
+ boolean :leavewhenempty => yes_no
190
+ end
191
+
192
+ def report_hold_time(yes_no)
193
+ boolean :reportholdtime => yes_no
194
+ end
195
+
196
+ def ring_in_use(yes_no)
197
+ boolean :ringinuse => yes_no
198
+ end
199
+
200
+ # Number of seconds to wait when an agent is to be bridged with
201
+ # a caller. Normally you'd want this to be zero.
202
+ def delay_connection_by(seconds)
203
+ int :memberdelay => seconds
204
+ end
205
+
206
+ def timeout_restart(yes_no)
207
+ boolean :timeoutrestart => yes_no
208
+ end
209
+
210
+ # Give a Hash argument here to override the default sound files for this queue.
211
+ #
212
+ # Usage:
213
+ #
214
+ # queue.sound_files :you_are_next => 'queue-youarenext',
215
+ # :there_are => 'queue-thereare',
216
+ # :calls_waiting => 'queue-callswaiting',
217
+ # :hold_time => 'queue-holdtime',
218
+ # :minutes => 'queue-minutes',
219
+ # :seconds => 'queue-seconds',
220
+ # :thank_you => 'queue-thankyou',
221
+ # :less_than => 'queue-less-than',
222
+ # :report_hold => 'queue-reporthold',
223
+ # :periodic_announcement => 'queue-periodic-announce'
224
+ #
225
+ # Note: the Hash values are the defaults. You only need to specify the ones you
226
+ # wish to override.
227
+ def sound_files(hash_of_files)
228
+ hash_of_files.each_pair do |key, value|
229
+ unless SOUND_FILE_SYMBOL_INTERPRETATIONS.has_key? key
230
+ message = "Unrecogized sound file identifier #{key.inspect}. " +
231
+ "Supported: " + SOUND_FILE_SYMBOL_INTERPRETATIONS.keys.map(&:inspect).to_sentence
232
+ raise ArgumentError, message
233
+ end
234
+ @sound_files[SOUND_FILE_SYMBOL_INTERPRETATIONS[key]] = value
235
+ end
236
+ end
237
+
238
+ def member(driver)
239
+ members << (driver.kind_of?(String) && driver =~ %r'/' ? driver : "Agent/#{driver}")
240
+ end
241
+
242
+ end
243
+
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,238 @@
1
+ require File.join(File.dirname(__FILE__), '../config_generator')
2
+
3
+ module Adhearsion
4
+ module Asterisk
5
+ class ConfigGenerator
6
+ class Voicemail < ConfigGenerator
7
+
8
+ DEFAULT_GENERAL_SECTION = {
9
+ :format => :wav
10
+ }
11
+
12
+ # Don't worry. These will be overridable soon.
13
+ STATIC_ZONEMESSAGES_CONTEXT = <<-ZONEMESSAGES.strip_heredoc
14
+ [zonemessages]
15
+ eastern=America/New_York|'vm-received' Q 'digits/at' IMp
16
+ central=America/Chicago|'vm-received' Q 'digits/at' IMp
17
+ central24=America/Chicago|'vm-received' q 'digits/at' H N 'hours'
18
+ military=Zulu|'vm-received' q 'digits/at' H N 'hours' 'phonetic/z_p'
19
+ european=Europe/Copenhagen|'vm-received' a d b 'digits/at' HM
20
+ ZONEMESSAGES
21
+
22
+ attr_reader :properties, :context_definitions
23
+ def initialize
24
+ @properties = DEFAULT_GENERAL_SECTION.clone
25
+ @mailboxes = {}
26
+ @context_definitions = []
27
+ super
28
+ end
29
+
30
+ def context(name)
31
+ raise ArgumentError, "Name cannot be 'general'!" if name.to_s.downcase == 'general'
32
+ raise ArgumentError, "A name can only be characters, numbers, and underscores!" if name.to_s !~ /^[\w_]+$/
33
+
34
+ ContextDefinition.new(name).tap do |context_definition|
35
+ yield context_definition
36
+ context_definitions << context_definition
37
+ end
38
+ end
39
+
40
+ def greeting_maximum(seconds)
41
+ int "maxgreet" => seconds
42
+ end
43
+
44
+ def execute_on_pin_change(command)
45
+ string "externpass" => command
46
+ end
47
+
48
+ def recordings
49
+ @recordings ||= RecordingDefinition.new
50
+ yield @recordings if block_given?
51
+ @recordings
52
+ end
53
+
54
+ def emails
55
+ @emails ||= EmailDefinition.new
56
+ if block_given?
57
+ yield @emails
58
+ else
59
+ @emails
60
+ end
61
+ end
62
+
63
+ def to_s
64
+ email_properties = @emails ? @emails.properties : {}
65
+ ConfigGenerator.warning_message +
66
+ "[general]\n" +
67
+ properties.merge(email_properties).map { |(key,value)| "#{key}=#{value}" }.sort.join("\n") + "\n\n" +
68
+ STATIC_ZONEMESSAGES_CONTEXT +
69
+ context_definitions.map(&:to_s).join("\n\n")
70
+ end
71
+
72
+ private
73
+
74
+ class ContextDefinition < ConfigGenerator
75
+
76
+ attr_reader :mailboxes
77
+ def initialize(name)
78
+ @name = name
79
+ @mailboxes = []
80
+ super()
81
+ end
82
+
83
+ # TODO: This will hold a lot of the methods from the [general] section!
84
+
85
+ def to_s
86
+ (%W[[#@name]] + mailboxes.map(&:to_s)).join "\n"
87
+ end
88
+
89
+ def mailbox(mailbox_number)
90
+ box = MailboxDefinition.new(mailbox_number)
91
+ yield box
92
+ mailboxes << box
93
+ end
94
+
95
+ private
96
+
97
+ def mailbox_entry(options)
98
+ MailboxDefinition.new.tap do |mailbox|
99
+ yield mailbox if block_given?
100
+ mailboxes << definition
101
+ end
102
+ end
103
+
104
+ class MailboxDefinition
105
+
106
+ attr_reader :mailbox_number
107
+ def initialize(mailbox_number)
108
+ check_numeric mailbox_number
109
+ @mailbox_number = mailbox_number
110
+ @definition = {}
111
+ super()
112
+ end
113
+
114
+ def pin_number(number)
115
+ check_numeric number
116
+ @definition[:pin_number] = number
117
+ end
118
+
119
+ def name(str)
120
+ @definition[:name] = str
121
+ end
122
+
123
+ def email(str)
124
+ @definition[:email] = str
125
+ end
126
+
127
+ def to_hash
128
+ @definition
129
+ end
130
+
131
+ def to_s
132
+ %(#{mailbox_number} => #{@definition[:pin_number]},#{@definition[:name]},#{@definition[:email]})[/^(.+?),*$/,1]
133
+ end
134
+
135
+ private
136
+
137
+ def check_numeric(number)
138
+ raise ArgumentError, number.inspect + " is not numeric!" unless number.to_s =~ /^\d+$/
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ class EmailDefinition < ConfigGenerator
145
+ EMAIL_VARIABLE_CONVENIENCES = {
146
+ :name => '${VM_NAME}',
147
+ :duration => '${VM_DUR}',
148
+ :message_number => '${VM_MSGNUM}',
149
+ :mailbox => '${VM_MAILBOX}',
150
+ :caller_id => '${VM_CALLERID}',
151
+ :date => '${VM_DATE}',
152
+ :caller_id_number => '${VM_CIDNUM}',
153
+ :caller_id_name => '${VM_CIDNAME}'
154
+ }
155
+
156
+ attr_reader :properties
157
+ def initialize
158
+ @properties = {}
159
+ super
160
+ end
161
+
162
+ def [](email_variable)
163
+ if EMAIL_VARIABLE_CONVENIENCES.has_key? email_variable
164
+ EMAIL_VARIABLE_CONVENIENCES[email_variable]
165
+ else
166
+ raise ArgumentError, "Unrecognized variable #{email_variable.inspect}"
167
+ end
168
+ end
169
+
170
+ def disable!
171
+ raise NotImpementedError
172
+ end
173
+
174
+ def from(options)
175
+ name, email = options.values_at :name, :email
176
+ string :serveremail => email
177
+ string :fromstring => name
178
+ end
179
+
180
+ def attach_recordings(true_or_false)
181
+ boolean :attach => true_or_false
182
+ end
183
+
184
+ def attach_recordings?
185
+ properties[:attach] == 'yes'
186
+ end
187
+
188
+ def body(str)
189
+ str = str.gsub("\r", '').gsub("\n", '\n')
190
+ if str.length > 512
191
+ raise ArgumentError, "Asterisk has an email body limit of 512 characters! Your body is too long!\n" +
192
+ ("-" * 10) + "\n" + str
193
+ end
194
+ string :emailbody => str
195
+ end
196
+
197
+ def subject(str)
198
+ string :emailsubject => str
199
+ end
200
+
201
+ def command(cmd)
202
+ string :mailcmd => cmd
203
+ end
204
+
205
+ end
206
+
207
+ class RecordingDefinition < ConfigGenerator
208
+
209
+ attr_reader :properties
210
+ def initialize
211
+ @properties = {}
212
+ super
213
+ end
214
+
215
+ def format(symbol)
216
+ one_of [:gsm, :wav49, :wav], :format => symbol
217
+ end
218
+
219
+ def allowed_length(seconds)
220
+ case seconds
221
+ when Fixnum
222
+ int :maxmessage => "value"
223
+ when Range
224
+ int :minmessage => seconds.first
225
+ int :maxmessage => seconds.last
226
+ else
227
+ raise ArgumentError, "Argument must be a Fixnum or Range!"
228
+ end
229
+ end
230
+
231
+ def maximum_silence(seconds)
232
+ int :maxsilence => seconds
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end