1#!/usr/bin/ruby
  2# frozen_string_literal: true
  3
  4require "date"
  5require "dhall"
  6require "em-hiredis"
  7require "em-http"
  8require "em_promise"
  9require "json"
 10require "optparse"
 11require "ruby-bandwidth-iris"
 12require "securerandom"
 13require "sentry-ruby"
 14require "time"
 15
 16@verbosity = 0
 17@real_data = true
 18@dry_run = false
 19
 20OptionParser.new do |opts|
 21	opts.banner = "Usage: porting [-vvf] DHALL_CONFIG"
 22
 23	opts.on(
 24		"-v", "--verbose",
 25		"Print to terminal, run twice to not even send to customer"
 26	) do
 27		@verbosity += 1
 28	end
 29
 30	opts.on("-f", "--fake", "Run with fake ports rather than fetching") do
 31		@real_data = false
 32	end
 33
 34	opts.on(
 35		"-n", "--dry-run",
 36		"Figure out what state they're in, but don't take action"
 37	) do
 38		@dry_run = true
 39	end
 40
 41	opts.on("-h", "--help", "Print this help") do
 42		puts opts
 43		exit
 44	end
 45end.parse!
 46
 47SCHEMA = "{
 48	bandwidth : { account: Text, username: Text, password: Text },
 49	xmpp: { jid: Text, password: Text },
 50	notification: { endpoint: Text, source_number: Text },
 51	pubsub: { server: Text, node: Text },
 52	testing_tel: Text,
 53	admin_server: Text
 54}"
 55
 56raise "Need a Dhall config" unless ARGV[0]
 57
 58CONFIG = Dhall::Coder
 59	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
 60	.load("#{ARGV.first} : #{SCHEMA}", transform_keys: :to_sym)
 61
 62require_relative "../lib/blather_notify"
 63require_relative "../lib/expiring_lock"
 64require_relative "../lib/form_to_h"
 65require_relative "../lib/porting_step"
 66
 67Faraday.default_adapter = :em_synchrony
 68BandwidthIris::Client.global_options = {
 69	account_id: CONFIG[:bandwidth][:account],
 70	username: CONFIG[:bandwidth][:username],
 71	password: CONFIG[:bandwidth][:password]
 72}
 73
 74class FullManual < PortingStepRepo::Outputs
 75	def info(id, key, msg)
 76		puts "[#{id}] INFO(#{key}): #{msg}"
 77	end
 78
 79	def warn(id, key, msg)
 80		puts "[#{id}] WARN(#{key}): #{msg}"
 81	end
 82
 83	def error(id, key, e_or_msg)
 84		puts "[#{id}] ERRR(#{key}): #{e_or_msg}"
 85		return unless e_or_msg.respond_to?(:backtrace)
 86
 87		e_or_msg.backtrace.each do |b|
 88			puts "[#{id}] ERRR(#{key}): #{b}"
 89		end
 90	end
 91
 92	def to_customer(id, key, tel, msg)
 93		puts "[#{id}] CUST(#{key}, #{tel}): #{msg}"
 94	end
 95end
 96
 97class ObservedAuto < FullManual
 98	def initialize(endpoint, source_number)
 99		@endpoint = endpoint
100		@src = source_number
101	end
102
103	def to_customer(id, key, tel, msg)
104		ExpiringLock.new(lock_key(id, key)).with do
105			EM::HttpRequest
106				.new(@endpoint)
107				.apost(
108					head: { "Content-Type" => "application/json" },
109					body: format_msg(tel, msg)
110				)
111		end
112	end
113
114protected
115
116	def lock_key(id, key)
117		"jmp_port_customer_msg_#{key}-#{id}"
118	end
119
120	def format_msg(tel, msg)
121		[{
122			time: DateTime.now.iso8601,
123			type: "message-received",
124			to: tel,
125			description: "Incoming message received",
126			message: actual_message(tel, msg)
127		}].to_json
128	end
129
130	def actual_message(tel, msg)
131		{
132			id: SecureRandom.uuid,
133			owner: tel,
134			applicationId: SecureRandom.uuid,
135			time: DateTime.now.iso8601,
136			segmentCount: 1,
137			direction: "in",
138			to: [tel], from: @src,
139			text: msg
140		}
141	end
142end
143
144class FullAutomatic < ObservedAuto
145	using FormToH
146
147	def initialize(pubsub_addr, endpoint, source_number)
148		@pubsub = BlatherNotify.pubsub(pubsub_addr)
149
150		Sentry.init do |config|
151			config.background_worker_threads = 0
152		end
153
154		super(endpoint, source_number)
155	end
156
157	# No one's watch; swallow informational messages
158	def info(*); end
159
160	def warn(id, key, msg)
161		ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do
162			entrykey = "#{id}:#{key}"
163			@pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey))
164		end
165	end
166
167	def error(id, key, e_or_msg)
168		Sentry.with_scope do |scope|
169			scope.set_context("port", { id: id, action: key })
170
171			if e_or_msg.is_a?(::Exception)
172				Sentry.capture_exception(e_or_msg)
173			else
174				Sentry.capture_message(e_or_msg.to_s)
175			end
176		end
177	end
178
179protected
180
181	def error_entry(title, text, id)
182		Nokogiri::XML::Builder.new { |xml|
183			xml.entry(xmlns: "http://www.w3.org/2005/Atom") do
184				xml.updated DateTime.now.iso8601
185				xml.id id
186				xml.title title
187				xml.content text.to_s, type: "text"
188				xml.author { xml.name "porting" }
189				xml.generator "porting", version: "1.0"
190			end
191		}.doc.root
192	end
193
194	def warn_lock_key(id, key)
195		"jmp_port_warn_msg_#{key}-#{id}"
196	end
197end
198
199@output =
200	case @verbosity
201	when 0
202		FullAutomatic.new(
203			BlatherNotify::PubSub::Address.new(**CONFIG[:pubsub]),
204			CONFIG[:notification][:endpoint],
205			CONFIG[:notification][:source_number]
206		)
207	when 1
208		ObservedAuto.new(
209			CONFIG[:notification][:endpoint],
210			CONFIG[:notification][:source_number]
211		)
212	else
213		FullManual.new
214	end
215
216ports = if @real_data
217	BandwidthIris::PortIn.list(
218		page: 1,
219		size: 50,
220		start_date: Date.today - 1,
221		end_date: Date.today
222	) || []
223else
224	MP = Struct.new(
225		:order_id,
226		:processing_status,
227		:actual_foc_date,
228		:last_modified_date,
229		:customer_order_id,
230		:billing_telephone_number
231	)
232
233	minutes = 1.0 / (24 * 60)
234
235	[
236		# This should be ignored
237		MP.new("T01", "SUBMITTED", nil, DateTime.now - 1, "ignored", "9998887777"),
238		MP.new(
239			"T02", "COMPLETE", DateTime.now - 60 * minutes,
240			DateTime.now - 55 * minutes, "0001", "2223334444"
241		)
242	]
243end
244
245EM.run do
246	REDIS = EM::Hiredis.connect
247
248	BlatherNotify.start(
249		CONFIG[:xmpp][:jid],
250		CONFIG[:xmpp][:password]
251	).then {
252		ports.reduce(EMPromise.resolve(nil)) { |promise, port|
253			promise.then do
254				@output.info(port.order_id, :start, "Here we go")
255				PortingStepRepo.new(output: @output).find(port).then { |s|
256					@output.info(port.order_id, :class, s.class)
257					s
258				}.then { |s|
259					if @dry_run
260						@output.info("DRY", :dry, "Not taking action")
261					else
262						s.perform_next_step
263					end
264				}
265			end
266		}
267	}.catch { |e|
268		@output.error("ROOT", :catch, e)
269	}.then { BlatherNotify.shutdown }
270end