M .rubocop.yml => .rubocop.yml +3 -0
@@ 15,6 15,9 @@ Metrics/MethodLength:
- test/*
Metrics/BlockLength:
+ ExcludedMethods:
+ - route
+ - "on"
Exclude:
- test/*
M Gemfile => Gemfile +3 -0
@@ 3,6 3,7 @@
source "https://rubygems.org"
gem "amazing_print"
+gem "bandwidth-sdk"
gem "blather", git: "https://github.com/singpolyma/blather.git", branch: "ergonomics"
gem "braintree"
gem "dhall"
@@ 14,9 15,11 @@ gem "em_promise.rb", "~> 0.0.3"
gem "eventmachine"
gem "money-open-exchange-rates"
gem "ougai"
+gem "roda"
gem "ruby-bandwidth-iris"
gem "sentry-ruby", "<= 4.3.1"
gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"
+gem "thin"
gem "value_semantics", git: "https://github.com/singpolyma/value_semantics"
group(:development) do
M config-schema.dhall => config-schema.dhall +1 -0
@@ 43,6 43,7 @@
, sgx : Text
, sip_host : Text
, upstream_domain : Text
+, web : < Inet : { interface : Text, port : Natural } | Unix : Text >
, web_register : { from : Text, to : Text }
, xep0157 : List { label : Text, value : Text, var : Text }
}
M config.dhall.sample => config.dhall.sample +3 -0
@@ 1,3 1,5 @@
+let ListenOn = < Inet: { interface: Text, port: Natural } | Unix: Text >
+in
{
component = {
jid = "component.localhost",
@@ 8,6 10,7 @@
port = 5347
},
sgx = "component2.localhost",
+ web = ListenOn.Inet { interface = "::1", port = env:PORT ? 8080 },
creds = {
account = "00000",
username = "dashboard user",
M lib/backend_sgx.rb => lib/backend_sgx.rb +17 -0
@@ 1,6 1,8 @@
# frozen_string_literal: true
class BackendSgx
+ VOICEMAIL_TRANSCRIPTION_DISABLED = 0
+
def initialize(customer_id, jid=CONFIG[:sgx], creds=CONFIG[:creds])
@customer_id = customer_id
@jid = jid
@@ 33,6 35,21 @@ class BackendSgx
end
end
+ def ogm_url
+ REDIS.get("catapult_ogm_url-#{from_jid}")
+ end
+
+ def catapult_flag(flagbit)
+ REDIS.getbit(
+ "catapult_setting_flags-#{from_jid}",
+ flagbit
+ ).then { |x| x != 1 }
+ end
+
+ def fwd_timeout
+ REDIS.get("catapult_fwd_timeout-#{from_jid}")
+ end
+
def set_fwd_timeout(timeout)
REDIS.set("catapult_fwd_timeout-#{from_jid}", timeout)
end
A lib/cdr.rb => lib/cdr.rb +71 -0
@@ 0,0 1,71 @@
+# frozen_string_literal: true
+
+require "value_semantics/monkey_patched"
+
+class CDR
+ module Disposition
+ def self.===(other)
+ ["NO ANSWER", "ANSWERED", "BUSY", "FAILED"].include?(other)
+ end
+
+ def self.for(cause)
+ case cause
+ when "timeout", "rejected", "cancel"
+ "NO ANSWER"
+ when "hangup"
+ "ANSWERED"
+ when "busy"
+ "BUSY"
+ else
+ "FAILED"
+ end
+ end
+ end
+
+ value_semantics do
+ cdr_id String
+ customer_id String
+ start Time
+ billsec Integer
+ disposition Disposition
+ tel(/\A\+\d+\Z/)
+ direction Either(:inbound, :outbound)
+ end
+
+ def self.for(event, **kwargs)
+ start = Time.parse(event["startTime"])
+
+ new({
+ cdr_id: "sgx-jmp/#{event['callId']}",
+ start: start,
+ billsec: (Time.parse(event["endTime"]) - start).ceil,
+ disposition: Disposition.for(event["cause"])
+ }.merge(kwargs))
+ end
+
+ def self.for_inbound(customer_id, event)
+ self.for(
+ event,
+ customer_id: customer_id,
+ tel: event["from"],
+ direction: :inbound
+ )
+ end
+
+ def self.for_outbound(event)
+ self.for(
+ event,
+ customer_id: event["from"].sub(/^\+/, ""),
+ tel: event["to"],
+ direction: :outbound
+ )
+ end
+
+ def save
+ columns, values = to_h.to_a.transpose
+ DB.query_defer(<<~SQL, values)
+ INSERT INTO cdr (#{columns.join(',')})
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ SQL
+ end
+end
M lib/customer.rb => lib/customer.rb +16 -2
@@ 5,6 5,7 @@ require "forwardable"
require_relative "./api"
require_relative "./blather_ext"
require_relative "./customer_info"
+require_relative "./customer_ogm"
require_relative "./customer_plan"
require_relative "./customer_usage"
require_relative "./backend_sgx"
@@ 20,7 21,8 @@ class Customer
attr_reader :customer_id, :balance, :jid
def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
:currency, :merchant_account, :plan_name, :auto_top_up_amount
- def_delegators :@sgx, :register!, :registered?, :set_fwd_timeout
+ def_delegators :@sgx, :register!, :registered?,
+ :fwd_timeout, :set_fwd_timeout, :catapult_flag
def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
def initialize(
@@ 67,13 69,25 @@ class Customer
stanza = stanza.dup
stanza.to = jid.with(resource: stanza.to&.resource)
stanza.from = stanza.from.with(domain: CONFIG[:component][:jid])
- BLATHER << stanza
+ block_given? ? yield(stanza) : (BLATHER << stanza)
end
def stanza_from(stanza)
BLATHER << @sgx.stanza(stanza)
end
+ def fetch_vcard_temp(from_tel=nil)
+ iq = Blather::Stanza::Iq::Vcard.new(:get)
+ iq.from = Blather::JID.new(from_tel, CONFIG[:component][:jid])
+ stanza_to(iq, &IQ_MANAGER.method(:write)).then(&:vcard)
+ end
+
+ def ogm(from_tel=nil)
+ @sgx.ogm_url.then do |url|
+ CustomerOGM.for(url, -> { fetch_vcard_temp(from_tel) })
+ end
+ end
+
def sip_account
SipAccount.find(customer_id)
end
A lib/customer_ogm.rb => lib/customer_ogm.rb +44 -0
@@ 0,0 1,44 @@
+# frozen_string_literal: true
+
+module CustomerOGM
+ def self.for(url, fetch_vcard_temp)
+ return Media.new(url) if url
+ TTS.for(fetch_vcard_temp)
+ end
+
+ class Media
+ def initialize(url)
+ @url = url
+ end
+
+ def to_render
+ [:voicemail_ogm_media, locals: { url: @url }]
+ end
+ end
+
+ class TTS
+ def self.for(fetch_vcard_temp)
+ fetch_vcard_temp.call.then { |vcard|
+ new(vcard)
+ }.catch { new(Blather::Stanza::Iq::Vcard::Vcard.new) }
+ end
+
+ def initialize(vcard)
+ @vcard = vcard
+ end
+
+ def [](k)
+ value = @vcard[k]
+ return if value.to_s.empty?
+ value
+ end
+
+ def fn
+ self["FN"] || self["NICKNAME"] || "a user of JMP.chat"
+ end
+
+ def to_render
+ [:voicemail_ogm_tts, locals: { fn: fn }]
+ end
+ end
+end
A lib/rack_fiber.rb => lib/rack_fiber.rb +29 -0
@@ 0,0 1,29 @@
+# frozen_string_literal: true
+
+require "fiber"
+
+module Rack
+ class Fiber
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ async_callback = env.delete("async.callback")
+ EM.next_tick { run_fiber(env, async_callback) }
+ throw :async
+ end
+
+ protected
+
+ def run_fiber(env, async_callback)
+ ::Fiber.new {
+ begin
+ async_callback.call(@app.call(env))
+ rescue ::Exception # rubocop:disable Lint/RescueException
+ async_callback.call([500, {}, [$!.to_s]])
+ end
+ }.resume
+ end
+ end
+end
A lib/roda_capture.rb => lib/roda_capture.rb +37 -0
@@ 0,0 1,37 @@
+# frozen-string-literal: true
+
+# Build from official params_capturing plugin
+class RodaCapture
+ module RequestMethods
+ def captures_hash
+ @captures_hash ||= {}
+ end
+
+ private
+
+ # Add the symbol to the list of capture names if capturing
+ def _match_symbol(sym)
+ @_sym_captures << sym if @_sym_captures
+
+ super
+ end
+
+ # If all arguments are strings or symbols, turn on param capturing during
+ # the matching, but turn it back off before yielding to the block. Add
+ # any captures to the params based on the param capture names added by
+ # the matchers.
+ def if_match(args)
+ @_sym_captures = [] if args.all? { |x| x.is_a?(Symbol) }
+
+ super do |*a|
+ if @_sym_captures
+ @_sym_captures.zip(a).each do |k, v|
+ captures_hash[k] = v
+ end
+ @_sym_captures = nil
+ end
+ yield(*a)
+ end
+ end
+ end
+end
A lib/roda_em_promise.rb => lib/roda_em_promise.rb +11 -0
@@ 0,0 1,11 @@
+# frozen_string_literal: true
+
+require "em_promise"
+
+module RodaEMPromise
+ module RequestMethods
+ def block_result(result)
+ super(EMPromise.resolve(result).sync)
+ end
+ end
+end
A public/beep.mp3 => public/beep.mp3 +0 -0
M sgx_jmp.rb => sgx_jmp.rb +9 -0
@@ 52,6 52,12 @@ CONFIG =
"(#{ARGV[0]}) : #{__dir__}/config-schema.dhall",
transform_keys: ->(k) { k&.to_sym }
)
+WEB_LISTEN =
+ if CONFIG[:web].is_a?(Hash)
+ [CONFIG[:web][:interface], CONFIG[:web][:port]]
+ else
+ [CONFIG[:web]]
+ end
singleton_class.class_eval do
include Blather::DSL
@@ 80,6 86,7 @@ require_relative "lib/transaction"
require_relative "lib/tel_selections"
require_relative "lib/session_manager"
require_relative "lib/statsd"
+require_relative "web"
ELECTRUM = Electrum.new(**CONFIG[:electrum])
EM::Hiredis::Client.load_scripts_from("./redis_lua")
@@ 189,6 196,8 @@ when_ready do
ping.from = CONFIG[:component][:jid]
self << ping
end
+
+ Web.run(LOG.child, CustomerRepo.new, *WEB_LISTEN)
end
# workqueue_count MUST be 0 or else Blather uses threads!
A test/test_cdr.rb => test/test_cdr.rb +42 -0
@@ 0,0 1,42 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "cdr"
+
+class CDRTest < Minitest::Test
+ def test_for_inbound
+ cdr = CDR.for_inbound(
+ "test",
+ "from" => "+15551234567",
+ "startTime" => "2020-01-01T00:00:00Z",
+ "endTime" => "2020-01-01T01:00:00Z",
+ "callId" => "a_call",
+ "cause" => "hangup"
+ )
+ assert_equal cdr.cdr_id, "sgx-jmp/a_call"
+ assert_equal cdr.customer_id, "test"
+ assert_equal cdr.start, Time.parse("2020-01-01T00:00:00Z")
+ assert_equal cdr.billsec, 60 * 60
+ assert_equal cdr.disposition, "ANSWERED"
+ assert_equal cdr.tel, "+15551234567"
+ assert_equal cdr.direction, :inbound
+ end
+
+ def test_for_outbound
+ cdr = CDR.for_outbound(
+ "to" => "+15551234567",
+ "from" => "+test",
+ "startTime" => "2020-01-01T00:00:00Z",
+ "endTime" => "2020-01-01T01:00:00Z",
+ "callId" => "a_call",
+ "cause" => "hangup"
+ )
+ assert_equal cdr.cdr_id, "sgx-jmp/a_call"
+ assert_equal cdr.customer_id, "test"
+ assert_equal cdr.start, Time.parse("2020-01-01T00:00:00Z")
+ assert_equal cdr.billsec, 60 * 60
+ assert_equal cdr.disposition, "ANSWERED"
+ assert_equal cdr.tel, "+15551234567"
+ assert_equal cdr.direction, :outbound
+ end
+end
M test/test_customer.rb => test/test_customer.rb +13 -0
@@ 8,6 8,7 @@ Customer::BRAINTREE = Minitest::Mock.new
Customer::ELECTRUM = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
Customer::DB = Minitest::Mock.new
+Customer::IQ_MANAGER = Minitest::Mock.new
CustomerPlan::DB = Minitest::Mock.new
CustomerUsage::REDIS = Minitest::Mock.new
CustomerUsage::DB = Minitest::Mock.new
@@ 109,6 110,18 @@ class CustomerTest < Minitest::Test
Customer::BLATHER.verify
end
+ def test_fetch_vcard_temp
+ result = Blather::Stanza::Iq::Vcard.new(:result)
+ result.vcard["FN"] = "name"
+ Customer::IQ_MANAGER.expect(
+ :method,
+ ->(*) { EMPromise.resolve(result) },
+ [:write]
+ )
+ assert_equal "name", customer.fetch_vcard_temp("+15551234567").sync["FN"]
+ end
+ em :test_fetch_vcard_temp
+
def test_customer_usage_report
report_for = (Date.today..(Date.today - 1))
report_for.first.downto(report_for.last).each.with_index do |day, idx|
A test/test_customer_ogm.rb => test/test_customer_ogm.rb +49 -0
@@ 0,0 1,49 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "customer_ogm"
+
+class CustomerOGMTest < Minitest::Test
+ def test_for_url
+ assert_kind_of(
+ CustomerOGM::Media,
+ CustomerOGM.for("https://example.com/test.mp3", -> {})
+ )
+ end
+
+ def test_for_no_url
+ assert_kind_of(
+ CustomerOGM::TTS,
+ CustomerOGM.for(nil, -> { EMPromise.resolve(nil) }).sync
+ )
+ end
+ em :test_for_no_url
+
+ class TTSTest < Minitest::Test
+ def test_to_render_empty_vcard
+ vcard = Blather::Stanza::Iq::Vcard::Vcard.new
+ assert_equal(
+ [:voicemail_ogm_tts, locals: { fn: "a user of JMP.chat" }],
+ CustomerOGM::TTS.new(vcard).to_render
+ )
+ end
+
+ def test_to_render_fn
+ vcard = Blather::Stanza::Iq::Vcard::Vcard.new
+ vcard["FN"] = "name"
+ assert_equal(
+ [:voicemail_ogm_tts, locals: { fn: "name" }],
+ CustomerOGM::TTS.new(vcard).to_render
+ )
+ end
+
+ def test_to_render_nickname
+ vcard = Blather::Stanza::Iq::Vcard::Vcard.new
+ vcard["NICKNAME"] = "name"
+ assert_equal(
+ [:voicemail_ogm_tts, locals: { fn: "name" }],
+ CustomerOGM::TTS.new(vcard).to_render
+ )
+ end
+ end
+end
A views/bridge.slim => views/bridge.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Bridge= call_id
A views/forward.slim => views/forward.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Forward from=from to=to /
A views/pause.slim => views/pause.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Pause duration=duration
A views/redirect.slim => views/redirect.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Redirect redirectUrl=to /
A views/voicemail.slim => views/voicemail.slim +10 -0
@@ 0,0 1,10 @@
+doctype xml
+Response
+ Pause duration=2
+ == render(*ogm.to_render)
+ PlayAudio= "/beep.mp3"
+ Record{
+ transcribe=transcription_enabled.to_s
+ recordingAvailableUrl="/inbound/calls/#{pseudo_call_id}/voicemail/audio"
+ transcriptionAvailableUrl="/inbound/calls/#{pseudo_call_id}/voicemail/transcription"
+ fileFormat="mp3"} /
A views/voicemail_ogm_media.slim => views/voicemail_ogm_media.slim +1 -0
@@ 0,0 1,1 @@
+PlayAudio= url
A views/voicemail_ogm_tts.slim => views/voicemail_ogm_tts.slim +4 -0
@@ 0,0 1,4 @@
+SpeakSentence
+ ' You have reached the voicemail of
+ = fn
+ | . Please send a text message, or leave a message after the tone.
A web.rb => web.rb +369 -0
@@ 0,0 1,369 @@
+# frozen_string_literal: true
+
+require "digest"
+require "forwardable"
+require "roda"
+require "thin"
+require "sentry-ruby"
+require "bandwidth"
+
+Faraday.default_adapter = :em_synchrony
+
+require_relative "lib/cdr"
+require_relative "lib/roda_capture"
+require_relative "lib/roda_em_promise"
+require_relative "lib/rack_fiber"
+
+BANDWIDTH_VOICE = Bandwidth::Client.new(
+ voice_basic_auth_user_name: CONFIG[:creds][:username],
+ voice_basic_auth_password: CONFIG[:creds][:password]
+).voice_client.client
+
+module CustomerFwd
+ def self.from_redis(redis, customer, tel)
+ EMPromise.all([
+ redis.get("catapult_fwd-#{tel}"),
+ customer.fwd_timeout
+ ]).then do |(fwd, stimeout)|
+ timeout = Timeout.new(stimeout)
+ next if !fwd || timeout.zero?
+ self.for(fwd, timeout)
+ end
+ end
+
+ def self.for(uri, timeout)
+ case uri
+ when /^tel:/
+ Tel.new(uri, timeout)
+ when /^sip:/
+ SIP.new(uri, timeout)
+ when /^xmpp:/
+ XMPP.new(uri, timeout)
+ else
+ raise "Unknown forward URI: #{uri}"
+ end
+ end
+
+ class Timeout
+ def initialize(s)
+ @timeout = s.nil? || s.to_i.negative? ? 300 : s.to_i
+ end
+
+ def zero?
+ @timeout.zero?
+ end
+
+ def to_i
+ @timeout
+ end
+ end
+
+ class Tel
+ attr_reader :timeout
+
+ def initialize(uri, timeout)
+ @tel = uri.sub(/^tel:/, "")
+ @timeout = timeout
+ end
+
+ def to
+ @tel
+ end
+ end
+
+ class SIP
+ attr_reader :timeout
+
+ def initialize(uri, timeout)
+ @uri = uri
+ @timeout = timeout
+ end
+
+ def to
+ @uri
+ end
+ end
+
+ class XMPP
+ attr_reader :timeout
+
+ def initialize(uri, timeout)
+ @jid = uri.sub(/^xmpp:/, "")
+ @timeout = timeout
+ end
+
+ def to
+ "sip:#{ERB::Util.url_encode(@jid)}@sip.cheogram.com"
+ end
+ end
+end
+
+# rubocop:disable Metrics/ClassLength
+class Web < Roda
+ use Rack::Fiber # Must go first!
+ use Sentry::Rack::CaptureExceptions
+ plugin :json_parser
+ plugin :public
+ plugin :render, engine: "slim"
+ plugin RodaCapture
+ plugin RodaEMPromise # Must go last!
+
+ class << self
+ attr_reader :customer_repo, :log
+ attr_reader :true_inbound_call, :outbound_transfers
+
+ def run(log, customer_repo, *listen_on)
+ plugin :common_logger, log, method: :info
+ @customer_repo = customer_repo
+ @true_inbound_call = {}
+ @outbound_transfers = {}
+ Thin::Logging.logger = log
+ Thin::Server.start(
+ *listen_on,
+ freeze.app,
+ signals: false
+ )
+ end
+ end
+
+ extend Forwardable
+ def_delegators :'self.class', :customer_repo, :true_inbound_call,
+ :outbound_transfers
+ def_delegators :request, :params
+
+ def log
+ opts[:common_logger]
+ end
+
+ def log_error(e)
+ log.error(
+ "Error raised during #{request.full_path}: #{e.class}",
+ e,
+ loggable_params
+ )
+ if e.is_a?(::Exception)
+ Sentry.capture_exception(e)
+ else
+ Sentry.capture_message(e.to_s)
+ end
+ end
+
+ def loggable_params
+ params.dup.tap do |p|
+ p.delete("to")
+ p.delete("from")
+ end
+ end
+
+ def pseudo_call_id
+ request.captures_hash[:pseudo_call_id] ||
+ Digest::SHA256.hexdigest("#{params['from']},#{params['to']}")
+ end
+
+ TEL_CANDIDATES = {
+ "Restricted" => "14",
+ "anonymous" => "15",
+ "Anonymous" => "16",
+ "unavailable" => "17",
+ "Unavailable" => "18"
+ }.freeze
+
+ def sanitize_tel_candidate(candidate)
+ if candidate.length < 3
+ "13;phone-context=anonymous.phone-context.soprani.ca"
+ elsif candidate[0] == "+" && /\A\d+\z/.match(candidate[1..-1])
+ candidate
+ elsif candidate == "Restricted"
+ TEL_CANDIDATES.fetch(candidate, "19") +
+ ";phone-context=anonymous.phone-context.soprani.ca"
+ end
+ end
+
+ def from_jid
+ Blather::JID.new(
+ sanitize_tel_candidate(params["from"]),
+ CONFIG[:component][:jid]
+ )
+ end
+
+ def inbound_calls_path(suffix)
+ ["/inbound/calls/#{pseudo_call_id}", suffix].compact.join("/")
+ end
+
+ def url(path)
+ "#{request.base_url}#{path}"
+ end
+
+ def modify_call(call_id)
+ body = Bandwidth::ApiModifyCallRequest.new
+ yield body
+ BANDWIDTH_VOICE.modify_call(
+ CONFIG[:creds][:account],
+ call_id,
+ body: body
+ )
+ end
+
+ route do |r|
+ r.on "inbound" do
+ r.on "calls" do
+ r.post "status" do
+ if params["eventType"] == "disconnect"
+ p_call_id = pseudo_call_id
+ call_id = params["callId"]
+ EM.promise_timer(2).then {
+ next unless true_inbound_call[p_call_id] == call_id
+ true_inbound_call.delete(p_call_id)
+
+ if (outbound_leg = outbound_transfers.delete(p_call_id))
+ modify_call(outbound_leg) do |call|
+ call.state = "completed"
+ end
+ end
+
+ customer_repo.find_by_tel(params["to"]).then do |customer|
+ CDR.for_inbound(customer.customer_id, params).save
+ end
+ }.catch(&method(:log_error))
+ end
+ "OK"
+ end
+
+ r.on :pseudo_call_id do |pseudo_call_id|
+ r.post "transfer_complete" do
+ outbound_leg = outbound_transfers.delete(pseudo_call_id)
+ if params["cause"] == "hangup"
+ log.debug "Normal hangup", loggable_params
+ elsif !outbound_leg
+ log.debug "Inbound disconnected", loggable_params
+ else
+ log.debug "Go to voicemail", loggable_params
+ true_call_id = true_inbound_call[pseudo_call_id]
+ modify_call(true_call_id) do |call|
+ call.redirect_url = url inbound_calls_path(:voicemail)
+ end
+ end
+ ""
+ end
+
+ r.on "voicemail" do
+ r.post "audio" do
+ duration = Time.parse(params["endTime"]) -
+ Time.parse(params["startTime"])
+ next "OK<5" unless duration > 5
+
+ jmp_media_url = params["mediaUrl"].sub(
+ /\Ahttps:\/\/voice.bandwidth.com\/api\/v2\/accounts\/\d+/,
+ "https://jmp.chat"
+ )
+
+ customer_repo.find_by_tel(params["to"]).then do |customer|
+ m = Blather::Stanza::Message.new
+ m.chat_state = nil
+ m.from = from_jid
+ m.subject = "New Voicemail"
+ m.body = jmp_media_url
+ m << OOB.new(jmp_media_url, desc: "Voicemail Recording")
+ customer.stanza_to(m)
+
+ "OK"
+ end
+ end
+
+ r.post "transcription" do
+ customer_repo.find_by_tel(params["to"]).then do |customer|
+ m = Blather::Stanza::Message.new
+ m.chat_state = nil
+ m.from = from_jid
+ m.subject = "Voicemail Transcription"
+ m.body = BANDWIDTH_VOICE.get_recording_transcription(
+ params["accountId"], params["callId"], params["recordingId"]
+ ).data.transcripts[0].text
+ customer.stanza_to(m)
+
+ "OK"
+ end
+ end
+
+ r.post do
+ customer_repo
+ .find_by_tel(params["to"])
+ .then { |customer|
+ EMPromise.all([
+ customer.ogm(params["from"]),
+ customer.catapult_flag(
+ BackendSgx::VOICEMAIL_TRANSCRIPTION_DISABLED
+ )
+ ])
+ }.then do |(ogm, transcription_disabled)|
+ render :voicemail, locals: {
+ ogm: ogm,
+ transcription_enabled: !transcription_disabled
+ }
+ end
+ end
+ end
+
+ r.post do
+ true_call_id = true_inbound_call[pseudo_call_id]
+ render :bridge, locals: { call_id: true_call_id }
+ end
+ end
+
+ r.post do
+ if true_inbound_call[pseudo_call_id]
+ true_inbound_call[pseudo_call_id] = params["callId"]
+ return render :pause, locals: { duration: 300 }
+ end
+
+ customer_repo.find_by_tel(params["to"]).then do |customer|
+ CustomerFwd.from_redis(::REDIS, customer, params["to"]).then do |fwd|
+ if fwd
+ body = Bandwidth::ApiCreateCallRequest.new.tap do |cc|
+ cc.to = fwd.to
+ cc.from = params["from"]
+ cc.application_id = params["applicationId"]
+ cc.call_timeout = fwd.timeout.to_i
+ cc.answer_url = url inbound_calls_path(nil)
+ cc.disconnect_url = url inbound_calls_path(:transfer_complete)
+ end
+ true_inbound_call[pseudo_call_id] = params["callId"]
+ outbound_transfers[pseudo_call_id] = BANDWIDTH_VOICE.create_call(
+ CONFIG[:creds][:account], body: body
+ ).data.call_id
+ render :pause, locals: { duration: 300 }
+ else
+ render :redirect, locals: { to: inbound_calls_path(:voicemail) }
+ end
+ end
+ end
+ end
+ end
+ end
+
+ r.on "outbound" do
+ r.on "calls" do
+ r.post "status" do
+ log.info "#{params['eventType']} #{params['callId']}", loggable_params
+ if params["eventType"] == "disconnect"
+ CDR.for_outbound(params).save.catch(&method(:log_error))
+ end
+ "OK"
+ end
+
+ r.post do
+ customer_id = params["from"].sub(/^\+/, "")
+ customer_repo.find(customer_id).then(:registered?).then do |reg|
+ render :forward, locals: {
+ from: reg.phone,
+ to: params["to"]
+ }
+ end
+ end
+ end
+ end
+
+ r.public
+ end
+end
+# rubocop:enable Metrics/ClassLength