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