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