adhearsion-asterisk 0.1.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 (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