adhearsion-asterisk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +6 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +143 -0
- data/Rakefile +23 -0
- data/adhearsion-asterisk.gemspec +35 -0
- data/lib/adhearsion-asterisk.rb +1 -0
- data/lib/adhearsion/asterisk.rb +12 -0
- data/lib/adhearsion/asterisk/config_generator.rb +103 -0
- data/lib/adhearsion/asterisk/config_generator/agents.rb +138 -0
- data/lib/adhearsion/asterisk/config_generator/queues.rb +247 -0
- data/lib/adhearsion/asterisk/config_generator/voicemail.rb +238 -0
- data/lib/adhearsion/asterisk/config_manager.rb +60 -0
- data/lib/adhearsion/asterisk/plugin.rb +464 -0
- data/lib/adhearsion/asterisk/queue_proxy.rb +177 -0
- data/lib/adhearsion/asterisk/queue_proxy/agent_proxy.rb +81 -0
- data/lib/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy.rb +132 -0
- data/lib/adhearsion/asterisk/version.rb +5 -0
- data/spec/adhearsion/asterisk/config_generators/agents_spec.rb +258 -0
- data/spec/adhearsion/asterisk/config_generators/queues_spec.rb +322 -0
- data/spec/adhearsion/asterisk/config_generators/voicemail_spec.rb +306 -0
- data/spec/adhearsion/asterisk/config_manager_spec.rb +125 -0
- data/spec/adhearsion/asterisk/plugin_spec.rb +618 -0
- data/spec/adhearsion/asterisk/queue_proxy/agent_proxy_spec.rb +90 -0
- data/spec/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy_spec.rb +145 -0
- data/spec/adhearsion/asterisk/queue_proxy_spec.rb +156 -0
- data/spec/adhearsion/asterisk_spec.rb +9 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/the_following_code.rb +3 -0
- 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
|