1# frozen_string_literal: true
2
3require "date"
4require "value_semantics/monkey_patched"
5require "em_promise"
6
7require_relative "blather_notify"
8require_relative "utils"
9
10class PortingStepRepo
11 # Any thing that debounces messages must happen inside this.
12 # The porting logic will pass everything in every time.
13 class Outputs
14 def info(port_id, kind, msg); end
15
16 def warn(port_id, kind, msg); end
17
18 def error(port_id, kind, e_or_msg); end
19
20 def to_customer(port_id, kind, tel, msg); end
21 end
22
23 value_semantics do
24 redis Anything(), default: LazyObject.new { REDIS }
25 blather_notify Anything(), default: BlatherNotify
26 admin_server Anything(), default: CONFIG[:admin_server]
27 testing_tel Anything(), default: CONFIG[:testing_tel]
28 output Outputs, default: Outputs.new
29 end
30
31 def find(port)
32 if_processing(port) do
33 case port.processing_status
34 when "FOC"
35 FOC.new(**to_h).find(port)
36 when "COMPLETE"
37 Complete.new(**to_h).find(port)
38 else
39 Wait.new(port, output: output)
40 end
41 end
42 end
43
44 def if_processing(port)
45 redis.exists("jmp_port_freeze-#{port.id}").then do |v|
46 next Frozen.new(port, output: output) if v == 1
47
48 yield
49 end
50 end
51
52 class FOC < self
53 # This is how long we'll wait for a port to move from FOC to COMPLETED
54 # It's in fractional days because DateTime
55 GRACE_PERIOD = 15.0 / (24 * 60)
56
57 def find(port)
58 Alert.for(
59 port,
60 grace_period: GRACE_PERIOD,
61 output: output, key: :late_foc,
62 msg: "⚠ Port is still in FOC state a while past FOC",
63 real_step: Wait.new(port, output: output)
64 )
65 end
66 end
67
68 class Complete < self
69 # If it's been 35 minutes and the number isn't reachable, a human
70 # should get involved
71 GRACE_PERIOD = 35.0 / (24 * 60)
72
73 def find(port)
74 if_not_complete(port) do
75 exe = blather_notify.command_execution(admin_server, "customer info")
76 AdminCommand.new(exe: exe, **to_h).find(port).then do |step|
77 Alert.for(
78 port,
79 grace_period: GRACE_PERIOD, output: output,
80 key: :late_finish, msg: msg(port), real_step: step
81 )
82 end
83 end
84 end
85
86 def if_not_complete(port)
87 redis.exists("jmp_port_complete-#{port.id}").then do |v|
88 next Done.new(port, output: output) if v == 1
89
90 yield
91 end
92 end
93
94 def msg(port)
95 "⚠ Port still hasn't finished. We'll keep trying unless you set redis " \
96 "key `jmp_port_freeze-#{port.id}`"
97 end
98
99 class AdminCommand < self
100 def initialize(exe:, **kwargs)
101 @exe = exe
102 super(**kwargs)
103 end
104
105 def to_h
106 super.merge(exe: @exe)
107 end
108
109 def find(port)
110 h = to_h
111 @exe.fetch_and_submit(q: port.customer_id).then do |form|
112 if port.tel == form.tel && form.route == port.backend_sgx.to_s
113 GoodNumber.new(**h).find(port)
114 else
115 WrongNumber.new(port: port, execution: @exe)
116 end
117 end
118 end
119
120 class GoodNumber < self
121 def find(port)
122 Reachability.new(type: "voice", **to_h).test(port) do
123 Reachability.new(type: "sms", **to_h).test(port) do
124 FinishUp.new(port, redis: redis, output: output)
125 end
126 end
127 rescue StandardError => e
128 ReachabilityFailure.for(error: e, port: port, **to_h)
129 end
130
131 class ReachabilityFailure < self
132 attr_reader :error
133
134 NO_GRACE_PERIOD = 0
135
136 # @param error [StandardError]
137 def self.for(port:, error:, **kwargs)
138 Alert.for(
139 port,
140 grace_period: NO_GRACE_PERIOD,
141 real_step: nil,
142 output: kwargs[:output],
143 msg: msg(port, error),
144 key: :reachability_failure
145 )
146 end
147
148 def self.msg(port, error)
149 "⚠ Error checking #{port.id} reachability: #{error}"
150 end
151 end
152
153 class Reachability < self
154 def initialize(type:, **args)
155 @type = type
156 super(**args)
157 end
158
159 def test(port)
160 execute_command(port).then { |response|
161 next yield unless response.count == "0"
162
163 args = to_h.slice(
164 :blather_notify, :testing_tel, :admin_server, :output
165 )
166 RunTest.new(type: @type, tel: port.tel, port: port, **args)
167 }.catch do |e|
168 ReachabilityFailure.for(port: port, error: e, output: output)
169 end
170 end
171
172 class RunTest
173 # This is here for tests
174 attr_reader :type
175
176 def initialize(
177 type:, tel:, port:, output:,
178 blather_notify:, testing_tel:, admin_server:
179 )
180 @type = type
181 @tel = tel
182 @port = port
183 @output = output
184 @blather_notify = blather_notify
185 @testing_tel = testing_tel
186 @admin_server = admin_server
187 end
188
189 def perform_next_step
190 @blather_notify.command_execution(@admin_server, "reachability")
191 .fetch_and_submit(
192 tel: @tel, type: @type, reachability_tel: @testing_tel
193 ).catch do |e|
194 ReachabilityFailure.for(
195 port: @port, error: e, output: @output
196 ).perform_next_step
197 end
198 end
199 end
200
201 protected
202
203 def execute_command(port)
204 blather_notify.command_execution(admin_server, "reachability")
205 .fetch_and_submit(tel: port.tel, type: @type)
206 end
207 end
208
209 class FinishUp
210 MESSAGE = "Hi! This is JMP support - your number has " \
211 "successfully transferred in to JMP! All calls/messages " \
212 "will now use your transferred-in number - your old JMP " \
213 "number has been disabled. Let us know if you have any " \
214 "questions and thanks for using JMP!"
215
216 def initialize(port, redis:, output:)
217 @port = port
218 @redis = redis
219 @output = output
220 end
221
222 def set_key
223 @redis.set(
224 "jmp_port_complete-#{@port.id}",
225 DateTime.now.iso8601,
226 "EX",
227 60 * 60 * 24 * 2 ### 2 Days should be enough to not see it listed
228 )
229 end
230
231 def perform_next_step
232 set_key.then do
233 EMPromise.all([
234 @output.info(@port.id, :complete, "Port Complete!"),
235 @output.to_customer(
236 @port.id, :complete,
237 Tel.new(@port.tel), MESSAGE
238 )
239 ])
240 end
241 end
242 end
243 end
244
245 class WrongNumber
246 attr_reader :new_backend
247
248 def initialize(port:, execution:)
249 @new_backend = port.backend_sgx
250 @right_number = port.tel
251 @exe = execution
252 end
253
254 def perform_next_step
255 @exe.fetch_and_submit(action: "number_change").then do |_form|
256 @exe.fetch_and_submit(
257 new_backend: @new_backend,
258 new_tel: @right_number,
259 should_delete: "true"
260 )
261 end
262 end
263 end
264 end
265 end
266
267 # This doesn't do anything and just waits for something to happen later
268 class Wait
269 def initialize(port, output:)
270 @port = port
271 @output = output
272 end
273
274 def perform_next_step
275 @output.info(@port.id, :wait, "Waiting...")
276 end
277 end
278
279 # This also doesn't do anything but is more content about it
280 class Done
281 def initialize(port, output:)
282 @port = port
283 @output = output
284 end
285
286 def perform_next_step
287 @output.info(@port.id, :done, "Done.")
288 end
289 end
290
291 # This also also doesn't do anything but is intentional
292 class Frozen
293 def initialize(port, output:)
294 @port = port
295 @output = output
296 end
297
298 def perform_next_step
299 @output.info(@port.id, :frozen, "Frozen.")
300 end
301 end
302
303 # This class sends and error to the human to check things out
304 class Alert
305 # @param [PortingStepRepo, NilClass] real_step If `nil`, just notify
306 def self.for(port, grace_period:, real_step:, **args)
307 if (DateTime.now - port.actual_foc_date.to_datetime) > grace_period
308 new(port, real_step: real_step, **args)
309 else
310 real_step
311 end
312 end
313
314 # For tests
315 attr_reader :key
316 attr_reader :real_step
317
318 def initialize(port, real_step:, output:, msg:, key:)
319 @port = port
320 @real_step = real_step
321 @output = output
322 @msg = msg
323 @key = key
324 end
325
326 def perform_next_step
327 @output.warn(@port.id, @key, @msg).then {
328 @real_step&.perform_next_step
329 }
330 end
331 end
332end
333
334class FullManual < PortingStepRepo::Outputs
335 def info(id, key, msg)
336 puts "[#{id}] INFO(#{key}): #{msg}"
337 end
338
339 def warn(id, key, msg)
340 puts "[#{id}] WARN(#{key}): #{msg}"
341 end
342
343 def error(id, key, e_or_msg)
344 puts "[#{id}] ERRR(#{key}): #{e_or_msg}"
345 return unless e_or_msg.respond_to?(:backtrace)
346
347 e_or_msg.backtrace.each do |b|
348 puts "[#{id}] ERRR(#{key}): #{b}"
349 end
350 end
351
352 def to_customer(id, key, tel, msg)
353 puts "[#{id}] CUST(#{key}, #{tel}): #{msg}"
354 end
355end
356
357class ObservedAuto < FullManual
358 def initialize(endpoint, source_number)
359 @endpoint = endpoint
360 @src = source_number
361 end
362
363 def to_customer(id, key, tel, msg)
364 ExpiringLock.new(lock_key(id, key)).with do
365 EM::HttpRequest
366 .new(@endpoint)
367 .apost(
368 head: { "Content-Type" => "application/json" },
369 body: format_msg(tel, msg)
370 )
371 end
372 end
373
374protected
375
376 def lock_key(id, key)
377 "jmp_port_customer_msg_#{key}-#{id}"
378 end
379
380 def format_msg(tel, msg)
381 [{
382 time: DateTime.now.iso8601,
383 type: "message-received",
384 to: tel,
385 description: "Incoming message received",
386 message: actual_message(tel, msg)
387 }].to_json
388 end
389
390 def actual_message(tel, msg)
391 {
392 id: SecureRandom.uuid,
393 owner: tel,
394 applicationId: SecureRandom.uuid,
395 time: DateTime.now.iso8601,
396 segmentCount: 1,
397 direction: "in",
398 to: [tel], from: @src,
399 text: msg
400 }
401 end
402end
403
404class FullAutomatic < ObservedAuto
405 using FormToH
406
407 def initialize(pubsub_addr, endpoint, source_number)
408 @pubsub = BlatherNotify.pubsub(pubsub_addr)
409
410 Sentry.init do |config|
411 config.background_worker_threads = 0
412 end
413
414 super(endpoint, source_number)
415 end
416
417 # No one's watch; swallow informational messages
418 def info(*); end
419
420 def warn(id, key, msg)
421 ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do
422 entrykey = "#{id}:#{key}"
423 @pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey))
424 end
425 end
426
427 def error(id, key, e_or_msg)
428 Sentry.with_scope do |scope|
429 scope.set_context("port", { id: id, action: key })
430
431 if e_or_msg.is_a?(::Exception)
432 Sentry.capture_exception(e_or_msg)
433 else
434 Sentry.capture_message(e_or_msg.to_s)
435 end
436 end
437 end
438
439protected
440
441 def error_entry(title, text, id)
442 Nokogiri::XML::Builder.new { |xml|
443 xml.entry(xmlns: "http://www.w3.org/2005/Atom") do
444 xml.updated DateTime.now.iso8601
445 xml.id id
446 xml.title title
447 xml.content text.to_s, type: "text"
448 xml.author { xml.name "porting" }
449 xml.generator "porting", version: "1.0"
450 end
451 }.doc.root
452 end
453
454 def warn_lock_key(id, key)
455 "jmp_port_warn_msg_#{key}-#{id}"
456 end
457end