#!/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