@@ -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