diff --git a/Gemfile b/Gemfile index a1704d5bf35cd7becf740ba345b52b470234a9da..aa2984b2d9bee21fe73f7e11471a56487d0ec2ac 100644 --- a/Gemfile +++ b/Gemfile @@ -14,9 +14,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 diff --git a/lib/cdr.rb b/lib/cdr.rb new file mode 100644 index 0000000000000000000000000000000000000000..efac8ab0e76a3b59fc07eaacc52352d10873cee9 --- /dev/null +++ b/lib/cdr.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "value_semantics/monkey_patched" + +class CDR + value_semantics do + cdr_id String + customer_id String + start Time + billsec Integer + disposition Either("NO ANSWER", "ANSWERED", "BUSY", "FAILED") + tel(/\A\+\d+\Z/) + direction Either(:inbound, :outbound) + end + + def self.for_disconnect(event) + start = Time.parse(event["startTime"]) + + new( + cdr_id: "sgx-jmp/#{event['callId']}", + customer_id: event["from"].sub(/^\+/, ""), + start: start, + billsec: (Time.parse(event["endTime"]) - start).ceil, + disposition: Disposition.for(event["cause"]), + 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 + + module Disposition + def self.for(cause) + case cause + when "timeout", "rejected", "cancel" + "NO ANSWER" + when "hangup" + "ANSWERED" + when "busy" + "BUSY" + else + "FAILED" + end + end + end +end diff --git a/lib/rack_fiber.rb b/lib/rack_fiber.rb new file mode 100644 index 0000000000000000000000000000000000000000..c30e06e18dd3dc5f79c5c03df75035a49e5807b9 --- /dev/null +++ b/lib/rack_fiber.rb @@ -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 diff --git a/lib/roda_em_promise.rb b/lib/roda_em_promise.rb new file mode 100644 index 0000000000000000000000000000000000000000..d26ea66cdaf3a6698a2bc75438d8d603975e40d7 --- /dev/null +++ b/lib/roda_em_promise.rb @@ -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 diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 90efd568108a24fe3fa789a723be21f84769b46d..24810406d109a84ffa2c434b5f8626507b693a9f 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -80,6 +80,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 +190,8 @@ when_ready do ping.from = CONFIG[:component][:jid] self << ping end + + Web.run(LOG.child, CustomerRepo.new) end # workqueue_count MUST be 0 or else Blather uses threads! diff --git a/views/forward.slim b/views/forward.slim new file mode 100644 index 0000000000000000000000000000000000000000..cb264e2937a4d3b552e9173329b797e20de56ce4 --- /dev/null +++ b/views/forward.slim @@ -0,0 +1,3 @@ +doctype xml +Response + Forward from=from to=to / diff --git a/web.rb b/web.rb new file mode 100644 index 0000000000000000000000000000000000000000..1e97e46afd1f9fad9b53e54cce4c01722338e43b --- /dev/null +++ b/web.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "roda" +require "thin" +require "sentry-ruby" + +require_relative "lib/cdr" +require_relative "lib/roda_em_promise" +require_relative "lib/rack_fiber" + +class Web < Roda + use Rack::Fiber # Must go first! + use Sentry::Rack::CaptureExceptions + plugin :json_parser + plugin :render, engine: "slim" + plugin RodaEMPromise # Must go last! + + class << self + attr_reader :customer_repo, :log + end + + def customer_repo + Web.customer_repo + end + + def log + Web.log + end + + def params + request.params + end + + def self.run(log, customer_repo) + plugin :common_logger, log, method: :info + @log = log + @customer_repo = customer_repo + Thin::Logging.logger = log + Thin::Server.start( + "::1", + ENV.fetch("PORT", 8080), + freeze.app, + signals: false + ) + end + + route do |r| + r.on "outbound" do + r.on "calls" do + r.post "status" do + loggable = params.dup.tap { |p| p.delete("to") } + log.info "#{params['eventType']} #{params['callId']}", loggable + if params["eventType"] == "disconnect" + CDR.for_disconnect(params).save.catch do |e| + log.error("Error raised during /outbound/calls/status", e, loggable) + Sentry.capture_exception(e) + end + 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 + end +end