punchblock 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/connection.rb +1 -0
- data/lib/punchblock/connection/asterisk.rb +0 -1
- data/lib/punchblock/connection/freeswitch.rb +49 -0
- data/lib/punchblock/event/offer.rb +1 -1
- data/lib/punchblock/translator.rb +5 -0
- data/lib/punchblock/translator/asterisk.rb +16 -28
- data/lib/punchblock/translator/asterisk/call.rb +4 -21
- data/lib/punchblock/translator/asterisk/component.rb +0 -5
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
- data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
- data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
- data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
- data/lib/punchblock/translator/freeswitch.rb +153 -0
- data/lib/punchblock/translator/freeswitch/call.rb +265 -0
- data/lib/punchblock/translator/freeswitch/component.rb +92 -0
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
- data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
- data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
- data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
- data/lib/punchblock/translator/input_component.rb +108 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +3 -2
- data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
- data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
- data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
- data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
- data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
- data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
- data/spec/punchblock_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- metadata +52 -7
@@ -54,13 +54,11 @@ module Punchblock
|
|
54
54
|
when :unimrcp
|
55
55
|
send_ref
|
56
56
|
@call.send_agi_action! 'EXEC MRCPSynth', escaped_doc, mrcpsynth_options do |complete_event|
|
57
|
-
pb_logger.debug "MRCPSynth completed with #{complete_event}."
|
58
57
|
output_component.send_complete_event! success_reason
|
59
58
|
end
|
60
59
|
when :swift
|
61
60
|
send_ref
|
62
61
|
@call.send_agi_action! 'EXEC Swift', swift_doc do |complete_event|
|
63
|
-
pb_logger.debug "Swift completed with #{complete_event}."
|
64
62
|
output_component.send_complete_event! success_reason
|
65
63
|
end
|
66
64
|
end
|
@@ -89,10 +87,8 @@ module Punchblock
|
|
89
87
|
end
|
90
88
|
|
91
89
|
def playback(path)
|
92
|
-
pb_logger.debug "Playing an audio file (#{path}) via Playback"
|
93
90
|
op = current_actor
|
94
91
|
@call.send_agi_action! 'EXEC Playback', path do |complete_event|
|
95
|
-
pb_logger.debug "File playback completed with #{complete_event}. Sending complete event"
|
96
92
|
op.send_complete_event! success_reason
|
97
93
|
end
|
98
94
|
end
|
@@ -31,7 +31,6 @@ module Punchblock
|
|
31
31
|
send_ref
|
32
32
|
|
33
33
|
if @component_node.start_beep
|
34
|
-
pb_logger.debug "Playing a beep via STREAM FILE before recording"
|
35
34
|
@call.send_agi_action! 'STREAM FILE', 'beep', '""' do
|
36
35
|
component.signal! :beep_finished
|
37
36
|
end
|
@@ -41,7 +40,6 @@ module Punchblock
|
|
41
40
|
call.send_ami_action! 'Monitor', 'Channel' => call.channel, 'File' => filename, 'Format' => @format, 'Mix' => true
|
42
41
|
unless max_duration == -1
|
43
42
|
after max_duration/1000 do
|
44
|
-
pb_logger.trace "Max duration encountered, stopping recording"
|
45
43
|
call.send_ami_action! 'StopMonitor', 'Channel' => call.channel
|
46
44
|
end
|
47
45
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'celluloid'
|
4
|
+
require 'ruby_fs'
|
5
|
+
|
6
|
+
module Punchblock
|
7
|
+
module Translator
|
8
|
+
class Freeswitch
|
9
|
+
include Celluloid
|
10
|
+
include HasGuardedHandlers
|
11
|
+
|
12
|
+
extend ActiveSupport::Autoload
|
13
|
+
|
14
|
+
autoload :Call
|
15
|
+
autoload :Component
|
16
|
+
|
17
|
+
attr_reader :connection, :media_engine, :default_voice, :calls
|
18
|
+
|
19
|
+
trap_exit :actor_died
|
20
|
+
|
21
|
+
def initialize(connection, media_engine = nil, default_voice = nil)
|
22
|
+
@connection, @media_engine, @default_voice = connection, media_engine, default_voice
|
23
|
+
@calls, @components = {}, {}
|
24
|
+
setup_handlers
|
25
|
+
end
|
26
|
+
|
27
|
+
def register_call(call)
|
28
|
+
@calls[call.id] ||= call
|
29
|
+
end
|
30
|
+
|
31
|
+
def deregister_call(call)
|
32
|
+
@calls.delete call.id
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_with_id(call_id)
|
36
|
+
@calls[call_id]
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_component(component)
|
40
|
+
@components[component.id] ||= component
|
41
|
+
end
|
42
|
+
|
43
|
+
def component_with_id(component_id)
|
44
|
+
@components[component_id]
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_handlers
|
48
|
+
register_handler :es, RubyFS::Stream::Connected do
|
49
|
+
handle_pb_event Connection::Connected.new
|
50
|
+
throw :halt
|
51
|
+
end
|
52
|
+
|
53
|
+
register_handler :es, RubyFS::Stream::Disconnected do
|
54
|
+
throw :halt
|
55
|
+
end
|
56
|
+
|
57
|
+
register_handler :es, :event_name => 'CHANNEL_PARK' do |event|
|
58
|
+
throw :pass if es_event_known_call? event
|
59
|
+
call = Call.new event[:unique_id], current_actor, event.content.select { |k,v| k.to_s =~ /variable/ }, stream, @media_engine, @default_voice
|
60
|
+
link call
|
61
|
+
register_call call
|
62
|
+
call.send_offer!
|
63
|
+
end
|
64
|
+
|
65
|
+
register_handler :es, [:has_key?, :other_leg_unique_id] => true do |event|
|
66
|
+
call = call_with_id event[:other_leg_unique_id]
|
67
|
+
call.handle_es_event! event if call
|
68
|
+
end
|
69
|
+
|
70
|
+
register_handler :es, lambda { |event| es_event_known_call? event } do |event|
|
71
|
+
call = call_with_id event[:unique_id]
|
72
|
+
call.handle_es_event! event
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def stream
|
77
|
+
connection.stream
|
78
|
+
end
|
79
|
+
|
80
|
+
def finalize
|
81
|
+
@calls.values.each(&:terminate)
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_es_event(event)
|
85
|
+
trigger_handler :es, event
|
86
|
+
end
|
87
|
+
exclusive :handle_es_event
|
88
|
+
|
89
|
+
def handle_pb_event(event)
|
90
|
+
connection.handle_event event
|
91
|
+
end
|
92
|
+
|
93
|
+
def execute_command(command, options = {})
|
94
|
+
command.request!
|
95
|
+
|
96
|
+
command.target_call_id ||= options[:call_id]
|
97
|
+
command.component_id ||= options[:component_id]
|
98
|
+
|
99
|
+
if command.target_call_id
|
100
|
+
execute_call_command command
|
101
|
+
elsif command.component_id
|
102
|
+
execute_component_command command
|
103
|
+
else
|
104
|
+
execute_global_command command
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def execute_call_command(command)
|
109
|
+
if call = call_with_id(command.target_call_id)
|
110
|
+
call.execute_command! command
|
111
|
+
else
|
112
|
+
command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def execute_component_command(command)
|
117
|
+
if (component = component_with_id(command.component_id))
|
118
|
+
component.execute_command! command
|
119
|
+
else
|
120
|
+
command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def execute_global_command(command)
|
125
|
+
case command
|
126
|
+
when Punchblock::Command::Dial
|
127
|
+
call = Call.new_link Punchblock.new_uuid, current_actor, nil, stream, @media_engine, @default_voice
|
128
|
+
register_call call
|
129
|
+
call.dial! command
|
130
|
+
else
|
131
|
+
command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def actor_died(actor, reason)
|
136
|
+
return unless reason
|
137
|
+
pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
|
138
|
+
if id = @calls.key(actor)
|
139
|
+
@calls.delete id
|
140
|
+
end_event = Punchblock::Event::End.new :target_call_id => id,
|
141
|
+
:reason => :error
|
142
|
+
handle_pb_event end_event
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def es_event_known_call?(event)
|
149
|
+
event[:unique_id] && call_with_id(event[:unique_id])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
class Call
|
7
|
+
include HasGuardedHandlers
|
8
|
+
include Celluloid
|
9
|
+
include DeadActorSafety
|
10
|
+
|
11
|
+
HANGUP_CAUSE_TO_END_REASON = Hash.new :error
|
12
|
+
|
13
|
+
HANGUP_CAUSE_TO_END_REASON['USER_BUSY'] = :busy
|
14
|
+
|
15
|
+
%w{
|
16
|
+
NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN MANAGER_REQUEST
|
17
|
+
BLIND_TRANSFER ATTENDED_TRANSFER PICKED_OFF NORMAL_UNSPECIFIED
|
18
|
+
}.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :hangup }
|
19
|
+
|
20
|
+
%w{
|
21
|
+
NO_USER_RESPONSE NO_ANSWER SUBSCRIBER_ABSENT ALLOTTED_TIMEOUT
|
22
|
+
MEDIA_TIMEOUT PROGRESS_TIMEOUT
|
23
|
+
}.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :timeout }
|
24
|
+
|
25
|
+
%w{CALL_REJECTED NUMBER_CHANGED
|
26
|
+
REDIRECTION_TO_NEW_DESTINATION FACILITY_REJECTED NORMAL_CIRCUIT_CONGESTION
|
27
|
+
SWITCH_CONGESTION USER_NOT_REGISTERED FACILITY_NOT_SUBSCRIBED
|
28
|
+
OUTGOING_CALL_BARRED INCOMING_CALL_BARRED BEARERCAPABILITY_NOTAUTH
|
29
|
+
BEARERCAPABILITY_NOTAVAIL SERVICE_UNAVAILABLE BEARERCAPABILITY_NOTIMPL
|
30
|
+
CHAN_NOT_IMPLEMENTED FACILITY_NOT_IMPLEMENTED SERVICE_NOT_IMPLEMENTED
|
31
|
+
}.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :reject }
|
32
|
+
|
33
|
+
REJECT_TO_HANGUP_REASON = Hash.new 'NORMAL_TEMPORARY_FAILURE'
|
34
|
+
REJECT_TO_HANGUP_REASON.merge! :busy => 'USER_BUSY', :decline => 'CALL_REJECTED'
|
35
|
+
|
36
|
+
attr_reader :id, :translator, :es_env, :direction, :stream, :media_engine, :default_voice
|
37
|
+
|
38
|
+
trap_exit :actor_died
|
39
|
+
|
40
|
+
def initialize(id, translator, es_env = nil, stream = nil, media_engine = nil, default_voice = nil)
|
41
|
+
@id, @translator, @stream, @media_engine, @default_voice = id, translator, stream, media_engine, default_voice
|
42
|
+
@es_env = es_env || {}
|
43
|
+
@components = {}
|
44
|
+
@pending_joins, @pending_unjoins = {}, {}
|
45
|
+
@answered = false
|
46
|
+
setup_handlers
|
47
|
+
end
|
48
|
+
|
49
|
+
def register_component(component)
|
50
|
+
@components[component.id] ||= component
|
51
|
+
end
|
52
|
+
|
53
|
+
def component_with_id(component_id)
|
54
|
+
@components[component_id]
|
55
|
+
end
|
56
|
+
|
57
|
+
def send_offer
|
58
|
+
@direction = :inbound
|
59
|
+
send_pb_event offer_event
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
"#<#{self.class}:#{id}>"
|
64
|
+
end
|
65
|
+
alias :inspect :to_s
|
66
|
+
|
67
|
+
def setup_handlers
|
68
|
+
register_handler :es, :event_name => 'CHANNEL_ANSWER' do
|
69
|
+
@answered = true
|
70
|
+
send_pb_event Event::Answered.new
|
71
|
+
end
|
72
|
+
|
73
|
+
register_handler :es, :event_name => 'CHANNEL_STATE', [:[], :channel_call_state] => 'RINGING' do
|
74
|
+
send_pb_event Event::Ringing.new
|
75
|
+
end
|
76
|
+
|
77
|
+
register_handler :es, :event_name => 'CHANNEL_HANGUP' do |event|
|
78
|
+
@components.dup.each_pair do |id, component|
|
79
|
+
safe_from_dead_actors do
|
80
|
+
component.call_ended if component.alive?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
send_end_event HANGUP_CAUSE_TO_END_REASON[event[:hangup_cause]]
|
84
|
+
end
|
85
|
+
|
86
|
+
register_handler :es, :event_name => 'CHANNEL_BRIDGE' do |event|
|
87
|
+
command = @pending_joins[event[:other_leg_unique_id]]
|
88
|
+
command.response = true if command
|
89
|
+
|
90
|
+
other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
|
91
|
+
send_pb_event Event::Joined.new(:call_id => other_call_id)
|
92
|
+
end
|
93
|
+
|
94
|
+
register_handler :es, :event_name => 'CHANNEL_UNBRIDGE' do |event|
|
95
|
+
command = @pending_unjoins[event[:other_leg_unique_id]]
|
96
|
+
command.response = true if command
|
97
|
+
|
98
|
+
other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
|
99
|
+
send_pb_event Event::Unjoined.new(:call_id => other_call_id)
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
register_handler :es, [:has_key?, :scope_variable_punchblock_component_id] => true do |event|
|
104
|
+
if component = component_with_id(event[:scope_variable_punchblock_component_id])
|
105
|
+
safe_from_dead_actors { component.handle_es_event event }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_es_event(event)
|
111
|
+
trigger_handler :es, event
|
112
|
+
end
|
113
|
+
|
114
|
+
def application(*args)
|
115
|
+
stream.application id, *args
|
116
|
+
end
|
117
|
+
|
118
|
+
def sendmsg(*args)
|
119
|
+
stream.sendmsg id, *args
|
120
|
+
end
|
121
|
+
|
122
|
+
def uuid_foo(app, args = '')
|
123
|
+
stream.bgapi "uuid_#{app} #{id} #{args}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def dial(dial_command)
|
127
|
+
@direction = :outbound
|
128
|
+
|
129
|
+
cid_number, cid_name = dial_command.from, nil
|
130
|
+
dial_command.from.match(/(?<cid_name>.*) <(?<cid_number>.*)>/) do |m|
|
131
|
+
cid_name = m[:cid_name]
|
132
|
+
cid_number = m[:cid_number]
|
133
|
+
end
|
134
|
+
|
135
|
+
options = {
|
136
|
+
:return_ring_ready => true,
|
137
|
+
:origination_uuid => id,
|
138
|
+
:origination_caller_id_number => "'#{cid_number}'"
|
139
|
+
}
|
140
|
+
options[:origination_caller_id_name] = "'#{cid_name}'" if cid_name
|
141
|
+
options[:originate_timeout] = dial_command.timeout/1000 if dial_command.timeout
|
142
|
+
opts = options.inject([]) do |a, (k, v)|
|
143
|
+
a << "#{k}=#{v}"
|
144
|
+
end.join(',')
|
145
|
+
|
146
|
+
stream.bgapi "originate {#{opts}}#{dial_command.to} &park()"
|
147
|
+
|
148
|
+
dial_command.response = Ref.new :id => id
|
149
|
+
end
|
150
|
+
|
151
|
+
def outbound?
|
152
|
+
direction == :outbound
|
153
|
+
end
|
154
|
+
|
155
|
+
def inbound?
|
156
|
+
direction == :inbound
|
157
|
+
end
|
158
|
+
|
159
|
+
def answered?
|
160
|
+
@answered
|
161
|
+
end
|
162
|
+
|
163
|
+
def execute_command(command)
|
164
|
+
if command.component_id
|
165
|
+
if component = component_with_id(command.component_id)
|
166
|
+
component.execute_command command
|
167
|
+
else
|
168
|
+
command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id} for call #{id}", id, command.component_id
|
169
|
+
end
|
170
|
+
end
|
171
|
+
case command
|
172
|
+
when Command::Accept
|
173
|
+
application 'respond', '180 Ringing'
|
174
|
+
command.response = true
|
175
|
+
when Command::Answer
|
176
|
+
command_id = Punchblock.new_uuid
|
177
|
+
register_tmp_handler :es, :event_name => 'CHANNEL_ANSWER', [:[], :scope_variable_punchblock_command_id] => command_id do
|
178
|
+
@answered = true
|
179
|
+
command.response = true
|
180
|
+
end
|
181
|
+
application 'answer', "%[punchblock_command_id=#{command_id}]"
|
182
|
+
when Command::Hangup
|
183
|
+
hangup
|
184
|
+
command.response = true
|
185
|
+
when Command::Join
|
186
|
+
@pending_joins[command.call_id] = command
|
187
|
+
uuid_foo :bridge, command.call_id
|
188
|
+
when Command::Unjoin
|
189
|
+
@pending_unjoins[command.call_id] = command
|
190
|
+
uuid_foo :transfer, '-both park inline'
|
191
|
+
when Command::Reject
|
192
|
+
hangup REJECT_TO_HANGUP_REASON[command.reason]
|
193
|
+
command.response = true
|
194
|
+
when Punchblock::Component::Output
|
195
|
+
case media_engine
|
196
|
+
when :freeswitch, :native, nil
|
197
|
+
execute_component Component::Output, command
|
198
|
+
when :flite
|
199
|
+
execute_component Component::FliteOutput, command, media_engine, default_voice
|
200
|
+
else
|
201
|
+
execute_component Component::TTSOutput, command, media_engine, default_voice
|
202
|
+
end
|
203
|
+
when Punchblock::Component::Input
|
204
|
+
execute_component Component::Input, command
|
205
|
+
when Punchblock::Component::Record
|
206
|
+
execute_component Component::Record, command
|
207
|
+
else
|
208
|
+
command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for call #{id}", id
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def hangup(reason = 'NORMAL_CLEARING')
|
213
|
+
sendmsg :call_command => 'hangup', :hangup_cause => reason
|
214
|
+
end
|
215
|
+
|
216
|
+
def logger_id
|
217
|
+
"#{self.class}: #{id}"
|
218
|
+
end
|
219
|
+
|
220
|
+
def actor_died(actor, reason)
|
221
|
+
return unless reason
|
222
|
+
pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
|
223
|
+
if id = @components.key(actor)
|
224
|
+
@components.delete id
|
225
|
+
complete_event = Punchblock::Event::Complete.new :component_id => id, :reason => Punchblock::Event::Complete::Error.new
|
226
|
+
send_pb_event complete_event
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def send_end_event(reason)
|
233
|
+
send_pb_event Event::End.new(:reason => reason)
|
234
|
+
translator.deregister_call current_actor
|
235
|
+
after(5) { terminate }
|
236
|
+
end
|
237
|
+
|
238
|
+
def execute_component(type, command, *execute_args)
|
239
|
+
type.new_link(command, current_actor).tap do |component|
|
240
|
+
register_component component
|
241
|
+
component.execute!(*execute_args)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def send_pb_event(event)
|
246
|
+
event.target_call_id = id
|
247
|
+
translator.handle_pb_event event
|
248
|
+
end
|
249
|
+
|
250
|
+
def offer_event
|
251
|
+
Event::Offer.new :to => es_env[:variable_sip_to_uri],
|
252
|
+
:from => "#{es_env[:variable_effective_caller_id_name]} <#{es_env[:variable_sip_from_uri]}>",
|
253
|
+
:headers => headers
|
254
|
+
end
|
255
|
+
|
256
|
+
def headers
|
257
|
+
es_env.to_a.inject({}) do |accumulator, element|
|
258
|
+
accumulator[('x_' + element[0].to_s).to_sym] = element[1] || ''
|
259
|
+
accumulator
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|