Allow infinite timeout / disabled voicemail

Stephen Paul Weber created

Use Forward to hand control completely to the target call.  If something ends up
at our voicemail due to error or similar, just hang up.

Change summary

forms/configure_calls.rb    | 33 ++++++++++++++--------
lib/configure_calls_form.rb |  7 ++--
lib/customer_fwd.rb         | 55 ++++++++++++++++++++++++++++++++++----
lib/registration.rb         |  2 
sgx_jmp.rb                  |  2 
views/forward.slim          |  3 ++
views/hangup.slim           |  3 ++
web.rb                      |  6 +++
8 files changed, 87 insertions(+), 24 deletions(-)

Detailed changes

forms/configure_calls.rb 🔗

@@ -2,21 +2,30 @@ form!
 title "Configure Calls"
 
 field(
-	var: "fwd[timeout]",
-	type: "text-single",
-	datatype: "xs:integer",
-	label: "Seconds to ring before voicemail",
-	description: "One ring is ~5 seconds. Negative means ring forever.",
-	value: @customer.fwd.timeout.to_i.to_s
-)
-
-field(
-	var: "voicemail_transcription",
+	var: "fwd[voicemail_enabled]",
 	type: "boolean",
-	label: "Voicemail transcription",
-	value: @customer.transcription_enabled.to_s
+	label: "Voicemail enabled",
+	value: @customer.fwd.voicemail_enabled?
 )
 
+if @customer.fwd.voicemail_enabled?
+	field(
+		var: "fwd[timeout]",
+		type: "text-single",
+		datatype: "xs:integer",
+		label: "Seconds to ring before voicemail",
+		description: "One ring is ~5 seconds. Negative means ring forever.",
+		value: @customer.fwd.timeout.to_i.to_s
+	)
+
+	field(
+		var: "voicemail_transcription",
+		type: "boolean",
+		label: "Voicemail transcription",
+		value: @customer.transcription_enabled.to_s
+	)
+end
+
 field(
 	var: "fwd[uri]",
 	type: "list-single",

lib/configure_calls_form.rb 🔗

@@ -33,8 +33,9 @@ protected
 	end
 
 	def parse_fwd(fwd_from_form)
-		fwd_from_form.reduce(@customer.fwd) do |fwd, (var, val)|
-			fwd.with(var.to_sym => val.strip)
-		end
+		@customer.fwd.with(fwd_from_form.each_with_object({}) { |(var, val), args|
+			args[var.to_sym] =
+				var == "voicemail_enabled" ? ["1", "true"].include?(val) : val&.strip
+		})
 	end
 end

lib/customer_fwd.rb 🔗

@@ -5,8 +5,17 @@ require "value_semantics/monkey_patched"
 require "uri"
 
 class CustomerFwd
-	def self.for(uri:, timeout:)
-		timeout = Timeout.new(timeout)
+	class InfiniteTimeout < StandardError
+		attr_reader :fwd
+
+		def initialize(fwd)
+			super "Infinite timeout"
+			@fwd = fwd
+		end
+	end
+
+	def self.for(uri:, timeout: nil, voicemail_enabled: :default)
+		timeout = Timeout.for(timeout, voicemail_enabled: voicemail_enabled)
 
 		fwd = if uri
 			if uri =~ /\Asip:(.*)@sip.cheogram.com\Z/
@@ -22,18 +31,32 @@ class CustomerFwd
 	end
 
 	class Timeout
-		def self.new(s)
-			s.is_a?(self) ? s : super
+		def self.for(s, voicemail_enabled: :default)
+			return Infinite.new unless voicemail_enabled
+
+			if s.nil? || s.is_a?(Infinite) || s.to_i.negative?
+				return new(25) if voicemail_enabled == true # ~5s / ring, 5 rings
+
+				return Infinite.new
+			end
+
+			return s if s.is_a?(self)
+
+			new(s)
 		end
 
 		def initialize(s)
-			@timeout = s.nil? || s.to_i.negative? || s.to_i > 300 ? 300 : s.to_i
+			@timeout = [s.to_i, 300].min
 		end
 
 		def zero?
 			@timeout.zero?
 		end
 
+		def infinite?
+			false
+		end
+
 		def to_i
 			@timeout
 		end
@@ -41,18 +64,38 @@ class CustomerFwd
 		def to_s
 			to_i.to_s
 		end
+
+		class Infinite < Timeout
+			def initialize; end
+
+			def zero?
+				false
+			end
+
+			def infinite?
+				1
+			end
+
+			def to_i; end
+		end
 	end
 
 	value_semantics do
 		uri Either(/:/, NilClass)
-		def_attr :timeout, Timeout, coerce: Timeout.method(:new)
+		def_attr :timeout, Timeout, coerce: Timeout.method(:for)
 	end
 
 	def with(new_attrs)
 		CustomerFwd.for(to_h.merge(new_attrs))
 	end
 
+	def voicemail_enabled?
+		!timeout.infinite?
+	end
+
 	def create_call(account)
+		raise InfiniteTimeout, self if timeout.infinite?
+
 		request = Bandwidth::ApiCreateCallRequest.new.tap { |cc|
 			cc.to = to
 			cc.call_timeout = timeout.to_i

lib/registration.rb 🔗

@@ -470,7 +470,7 @@ class Registration
 				EMPromise.all([
 					REDIS.del("pending_tel_for-#{@customer.jid}"),
 					Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
-						uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
+						uri: "xmpp:#{@customer.jid}", voicemail_enabled: true
 					))
 				])
 			}.then do

sgx_jmp.rb 🔗

@@ -527,7 +527,7 @@ Command.new(
 Command.new(
 	"ogm",
 	"Record Voicemail Greeting",
-	list_for: ->(fwd: nil, **) { !!fwd },
+	list_for: ->(fwd: nil, **) { fwd&.voicemail_enabled? },
 	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
 ) {
 	Command.customer.then do |customer|

views/forward.slim 🔗

@@ -0,0 +1,3 @@
+doctype xml
+Response
+	Forward to=fwd.to from=from callTimeout=300

web.rb 🔗

@@ -296,8 +296,10 @@ class Web < Roda
 								sgx_repo: Bwmsgsv2Repo.new,
 								ogm_url: nil
 							).then { |c|
-								c.ogm(params["from"])
+								c.ogm(params["from"]) if c.fwd.voicemail_enabled?
 							}.then { |ogm|
+								next render :hangup unless ogm
+
 								render :voicemail, locals: { ogm: ogm }
 							}
 						end
@@ -339,6 +341,8 @@ class Web < Roda
 
 						outbound_transfers[params["callId"]] = call
 						render :ring, locals: { duration: 300 }
+					}.catch_only(CustomerFwd::InfiniteTimeout) { |e|
+						render :forward, locals: { fwd: e.fwd, from: params["from"] }
 					}.catch { |e|
 						log_error(e) unless e == :voicemail
 						render :redirect, locals: { to: inbound_calls_path(:voicemail) }