diff --git a/bin/porting b/bin/porting new file mode 100755 index 0000000000000000000000000000000000000000..8313e6b881caeb8cd8ce5ab011e01e51dd54d7e9 --- /dev/null +++ b/bin/porting @@ -0,0 +1,270 @@ +#!/usr/bin/ruby +# frozen_string_literal: true + +require "date" +require "dhall" +require "em-hiredis" +require "em-http" +require "em_promise" +require "json" +require "optparse" +require "ruby-bandwidth-iris" +require "securerandom" +require "sentry-ruby" +require "time" + +@verbosity = 0 +@real_data = true +@dry_run = false + +OptionParser.new do |opts| + opts.banner = "Usage: porting [-vvf] DHALL_CONFIG" + + opts.on( + "-v", "--verbose", + "Print to terminal, run twice to not even send to customer" + ) do + @verbosity += 1 + end + + opts.on("-f", "--fake", "Run with fake ports rather than fetching") do + @real_data = false + end + + opts.on( + "-n", "--dry-run", + "Figure out what state they're in, but don't take action" + ) do + @dry_run = true + end + + opts.on("-h", "--help", "Print this help") do + puts opts + exit + end +end.parse! + +SCHEMA = "{ + bandwidth : { account: Text, username: Text, password: Text }, + xmpp: { jid: Text, password: Text }, + notification: { endpoint: Text, source_number: Text }, + pubsub: { server: Text, node: Text }, + testing_tel: Text, + admin_server: Text +}" + +raise "Need a Dhall config" unless ARGV[0] + +CONFIG = Dhall::Coder + .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) + .load("#{ARGV.first} : #{SCHEMA}", transform_keys: :to_sym) + +require_relative "../lib/blather_notify" +require_relative "../lib/expiring_lock" +require_relative "../lib/form_to_h" +require_relative "../lib/porting_step" + +Faraday.default_adapter = :em_synchrony +BandwidthIris::Client.global_options = { + account_id: CONFIG[:bandwidth][:account], + username: CONFIG[:bandwidth][:username], + password: CONFIG[:bandwidth][:password] +} + +class FullManual < PortingStepRepo::Outputs + def info(id, key, msg) + puts "[#{id}] INFO(#{key}): #{msg}" + end + + def warn(id, key, msg) + puts "[#{id}] WARN(#{key}): #{msg}" + end + + def error(id, key, e_or_msg) + puts "[#{id}] ERRR(#{key}): #{e_or_msg}" + return unless e_or_msg.respond_to?(:backtrace) + + e_or_msg.backtrace.each do |b| + puts "[#{id}] ERRR(#{key}): #{b}" + end + end + + def to_customer(id, key, tel, msg) + puts "[#{id}] CUST(#{key}, #{tel}): #{msg}" + end +end + +class ObservedAuto < FullManual + def initialize(endpoint, source_number) + @endpoint = endpoint + @src = source_number + end + + def to_customer(id, key, tel, msg) + ExpiringLock.new(lock_key(id, key)).with do + EM::HttpRequest + .new(@endpoint) + .apost( + head: { "Content-Type" => "application/json" }, + body: format_msg(tel, msg) + ) + end + end + +protected + + def lock_key(id, key) + "jmp_port_customer_msg_#{key}-#{id}" + end + + def format_msg(tel, msg) + [{ + time: DateTime.now.iso8601, + type: "message-received", + to: tel, + description: "Incoming message received", + message: actual_message(tel, msg) + }].to_json + end + + def actual_message(tel, msg) + { + id: SecureRandom.uuid, + owner: tel, + applicationId: SecureRandom.uuid, + time: DateTime.now.iso8601, + segmentCount: 1, + direction: "in", + to: [tel], from: @src, + text: msg + } + end +end + +class FullAutomatic < ObservedAuto + using FormToH + + def initialize(pubsub_addr, endpoint, source_number) + @pubsub = BlatherNotify.pubsub(pubsub_addr) + + Sentry.init do |config| + config.background_worker_threads = 0 + end + + super(endpoint, source_number) + end + + # No one's watch; swallow informational messages + def info(*); end + + def warn(id, key, msg) + ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do + entrykey = "#{id}:#{key}" + @pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey)) + end + end + + def error(id, key, e_or_msg) + Sentry.with_scope do |scope| + scope.set_context("port", { id: id, action: key }) + + if e_or_msg.is_a?(::Exception) + Sentry.capture_exception(e_or_msg) + else + Sentry.capture_message(e_or_msg.to_s) + end + end + end + +protected + + def error_entry(title, text, id) + Nokogiri::XML::Builder.new { |xml| + xml.entry(xmlns: "http://www.w3.org/2005/Atom") do + xml.updated DateTime.now.iso8601 + xml.id id + xml.title title + xml.content text.to_s, type: "text" + xml.author { xml.name "porting" } + xml.generator "porting", version: "1.0" + end + }.doc.root + end + + def warn_lock_key(id, key) + "jmp_port_warn_msg_#{key}-#{id}" + end +end + +@output = + case @verbosity + when 0 + FullAutomatic.new( + BlatherNotify::PubSub::Address.new(**CONFIG[:pubsub]), + CONFIG[:notification][:endpoint], + CONFIG[:notification][:source_number] + ) + when 1 + ObservedAuto.new( + CONFIG[:notification][:endpoint], + CONFIG[:notification][:source_number] + ) + else + FullManual.new + end + +ports = if @real_data + BandwidthIris::PortIn.list( + page: 1, + size: 50, + start_date: Date.today - 1, + end_date: Date.today + ) || [] +else + MP = Struct.new( + :order_id, + :processing_status, + :actual_foc_date, + :last_modified_date, + :customer_order_id, + :billing_telephone_number + ) + + minutes = 1.0 / (24 * 60) + + [ + # This should be ignored + MP.new("T01", "SUBMITTED", nil, DateTime.now - 1, "ignored", "9998887777"), + MP.new( + "T02", "COMPLETE", DateTime.now - 60 * minutes, + DateTime.now - 55 * minutes, "0001", "2223334444" + ) + ] +end + +EM.run do + REDIS = EM::Hiredis.connect + + BlatherNotify.start( + CONFIG[:xmpp][:jid], + CONFIG[:xmpp][:password] + ).then { + ports.reduce(EMPromise.resolve(nil)) { |promise, port| + promise.then do + @output.info(port.order_id, :start, "Here we go") + PortingStepRepo.new(output: @output).find(port).then { |s| + @output.info(port.order_id, :class, s.class) + s + }.then { |s| + if @dry_run + @output.info("DRY", :dry, "Not taking action") + else + s.perform_next_step + end + } + end + } + }.catch { |e| + @output.error("ROOT", :catch, e) + }.then { BlatherNotify.shutdown } +end