From 1145cc7bced4e6aa1b4443efdfc3ee5965c33834 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Mon, 13 Feb 2023 12:17:58 -0500 Subject: [PATCH] Reachability Command This creates the keys that the previous commits use, and also reports their current values. It's a pretty sparse command in terms of UX, but only admins will use it so I kept it simple. This also optionally sends the prompt to one of the chosen reachability numbers. It's optional so a human running the command can query the result without looking in redis, but also without spamming their phone with another message. --- forms/reachability.rb | 28 ++++++++++++ forms/reachability_result.rb | 8 ++++ lib/backend_sgx.rb | 2 +- lib/reachability_form.rb | 84 ++++++++++++++++++++++++++++++++++++ lib/reachability_repo.rb | 9 ++++ sgx_jmp.rb | 30 +++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 forms/reachability.rb create mode 100644 forms/reachability_result.rb create mode 100644 lib/reachability_form.rb diff --git a/forms/reachability.rb b/forms/reachability.rb new file mode 100644 index 0000000000000000000000000000000000000000..6e00ef76dc98c5b141d2b233816fb36f8a5e2a7f --- /dev/null +++ b/forms/reachability.rb @@ -0,0 +1,28 @@ +form! +title "Reachability" + +field( + var: "tel", + datatype: "html:tel", + required: true, + label: "Number to test" +) + +field( + var: "reachability_tel", + type: "list-single", + options: CONFIG[:reachability_senders] + .map { |tel| { label: tel, value: tel } }, + label: "Number to send test prompt to", + desc: "leave blank to not send a prompt" +) + +field( + var: "type", + type: "list-single", + options: [ + { label: "SMS", value: "sms" }, + { label: "Voice", value: "voice" } + ], + label: "Type of test to perform" +) diff --git a/forms/reachability_result.rb b/forms/reachability_result.rb new file mode 100644 index 0000000000000000000000000000000000000000..e20125a6e0e26ea1f76d9e531c78dd0ce8719564 --- /dev/null +++ b/forms/reachability_result.rb @@ -0,0 +1,8 @@ +result! +title "Reachability Result" + +field( + var: "count", + value: @count, + label: "Received requests so far" +) diff --git a/lib/backend_sgx.rb b/lib/backend_sgx.rb index a0c53b844870f91372ea6e9b455ac868fc5f0279..e2540fee283e7dc9d34e764bfe58f7a4769f075e 100644 --- a/lib/backend_sgx.rb +++ b/lib/backend_sgx.rb @@ -40,7 +40,7 @@ class BackendSgx domain: jid.domain, node: jid.node || stanza.to.node ) - stanza.from = from_jid.with(resource: stanza.from.resource) + stanza.from = from_jid.with(resource: stanza.from&.resource) end end diff --git a/lib/reachability_form.rb b/lib/reachability_form.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d43d03a26ecfdfe1be0bca85adab845105b692f --- /dev/null +++ b/lib/reachability_form.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "value_semantics/monkey_patched" + +require_relative "customer" +require_relative "form_template" +require_relative "form_to_h" +require_relative "reachability_repo" + +class ReachabilityForm + using FormToH + + REPOS = { + "sms" => ReachabilityRepo::SMS, + "voice" => ReachabilityRepo::Voice + }.freeze + + class Result + value_semantics do + repo ReachabilityRepo + target Customer + sender Either(String, nil) + end + + def prompt + return unless sender + + Blather::Stanza::Message.new.tap do |m| + m.body = "/#{repo.type}" + # stanza_from will fix domain + m.to = Blather::JID.new("#{sender}@sgx-jmp") + end + end + end + + def initialize(customer_repo) + @customer_repo = customer_repo + end + + def render + FormTemplate.render("reachability") + end + + def render_result(count) + FormTemplate.render("reachability_result", count: count) + end + + def parse(form) + params = form.to_h + tel = cleanup_tel(params["tel"]) + + sender = cleanup_sender(params["reachability_tel"]) + + repo = REPOS[params["type"]] + raise "Type is invalid" unless repo + + find_target(tel).then { |target| + Result.new(target: target, repo: repo.new, sender: sender) + } + end + +protected + + def find_target(tel) + @customer_repo.find_by_tel(tel) + end + + def cleanup_sender(str) + return nil unless str + + str = str.strip + return nil if str.empty? + + unless CONFIG[:reachability_senders].include?(str) + raise "Sender not in whitelist" + end + + str + end + + def cleanup_tel(str) + "+1#{str.gsub(/\A\+?1?/, '')}" + end +end diff --git a/lib/reachability_repo.rb b/lib/reachability_repo.rb index db074b7b364e390b910ff88151fb3bd86494a129..341d81c6643276c71ae9993590c4808f21801ea9 100644 --- a/lib/reachability_repo.rb +++ b/lib/reachability_repo.rb @@ -8,6 +8,8 @@ class ReachabilityRepo senders ArrayOf(String), default: CONFIG[:reachability_senders] end + EXPIRY = 60 * 60 # 1 hr + class SMS < self def type "sms" @@ -47,6 +49,13 @@ class ReachabilityRepo end end + # This creates the keys if they don't exist, and returns the value + def get_or_create(customer) + redis.set(key(customer), 0, "NX", "EX", EXPIRY).then { + redis.get(key(customer)) + } + end + protected # This is basically an optimization to bail early without hitting a diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 7f8d1b02ec9c6c541b42cf6d737f8350c2bbf732..3262feeab40722d0b24d60e2798ccbc446410fbc 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -96,6 +96,7 @@ require_relative "lib/patches_for_sentry" require_relative "lib/payment_methods" require_relative "lib/paypal_done" require_relative "lib/postgres" +require_relative "lib/reachability_form" require_relative "lib/reachability_repo" require_relative "lib/registration" require_relative "lib/transaction" @@ -824,6 +825,35 @@ Command.new( end }.register(self).then(&CommandList.method(:register)) +Command.new( + "reachability", + "Test Reachability", + list_for: ->(customer: nil, **) { customer&.admin? } +) { + Command.customer.then do |customer| + raise AuthError, "You are not an admin" unless customer&.admin? + + form = ReachabilityForm.new(CustomerRepo.new) + + Command.reply { |reply| + reply.allowed_actions = [:next] + reply.command << form.render + }.then { |response| + form.parse(response.form) + }.then { |result| + result.repo.get_or_create(result.target).then { |v| + result.target.stanza_from(result.prompt) if result.prompt + + Command.finish { |reply| + reply.command << form.render_result(v) + } + } + }.catch_only(RuntimeError) { |e| + Command.finish(e, type: :error) + } + end +}.register(self).then(&CommandList.method(:register)) + Command.new( "snikket", "Launch Snikket Instance",