Switch from unbilled to direct targets

Stephen Paul Weber created

Instead of just having a list of targets we don't charge for and that don't
count towards limits, instead a list of targets where we know their JID and send
them messages directly, bypassing even the SGX.

In practise we will use this for support.  This means support messages will not
traverse billing code, or the sgx, and will work even if the sgx is down or the
customer has no configuration there at all (or a broken configuration there)
including if the customer has no phone number.

Support will get messages from customer_<customerid>@jmp.chat and can reply to
those.  Customers will still message the support phone number via @cheogram.com
and see the replies coming from the support number.

Anyone messaging the support phone number from outside our system (ie from
carrier SMS) will still show up as phonenumber@cheogram.com to support, since
that won't traverse this path.  If someone messages support without a cheogram
route that will still come as whispers since those don't traverse sgx-jmp at all
right now.

In the unlikely case that someone has a cheogram route set, but no customer id
at all, then these will come from <escaped jid>@jmp.chat and currently cannot be
replied to, but at least support will see that something is up and be able to
take action.

Change summary

config-schema.dhall |  2 +-
config.dhall.sample |  4 +++-
sgx_jmp.rb          | 32 +++++++++++++++++++++++++++++---
3 files changed, 33 insertions(+), 5 deletions(-)

Detailed changes

config-schema.dhall 🔗

@@ -15,6 +15,7 @@
 , component : { jid : Text, secret : Text }
 , credit_card_url : forall (jid : Text) -> forall (customer_id : Text) -> Text
 , creds : { account : Text, password : Text, username : Text }
+, direct_targets : List { mapKey : Text, mapValue : Text }
 , electrum : { rpc_password : Text, rpc_uri : Text, rpc_username : Text }
 , electrum_notify_url :
     forall (address : Text) -> forall (customer_id : Text) -> Text
@@ -45,7 +46,6 @@
 , sip : { app : Text, realm : Text }
 , sip_host : Text
 , snikket_hosting_api : Text
-, unbilled_targets : List Text
 , upstream_domain : Text
 , web : < Inet : { interface : Text, port : Natural } | Unix : Text >
 , web_register : { from : Text, to : Text }

config.dhall.sample 🔗

@@ -76,7 +76,9 @@ in
 	payable = "",
 	notify_from = "+15551234567@example.net",
 	admins = ["test\\40example.com@example.net"],
-	unbilled_targets = ["+14169938000"],
+	direct_targets = toMap {
+		`+15551234567` = "support@example.com"
+	},
 	keep_area_codes = ["555"],
 	keep_area_codes_in = { account = "", site_id = "", sip_peer_id = "" },
 	snikket_hosting_api = "",

sgx_jmp.rb 🔗

@@ -310,11 +310,9 @@ end
 # Especially if we have the component join MUC for notifications
 message(type: :groupchat) { true }
 
-UNBILLED_TARGETS = Set.new(CONFIG[:unbilled_targets])
 def billable_message(m)
 	b = m.body
-	!UNBILLED_TARGETS.member?(m.to.node) && \
-		(b && !b.empty? || m.find("ns:x", ns: OOB.registered_ns).first)
+	b && !b.empty? || m.find("ns:x", ns: OOB.registered_ns).first
 end
 
 class OverLimit < StandardError
@@ -337,6 +335,34 @@ end
 
 class CustomerExpired < StandardError; end
 
+CONFIG[:direct_targets].each do |(tel, jid)|
+	customer_repo = CustomerRepo.new(
+		sgx_repo: TrivialBackendSgxRepo.new(jid: jid),
+		set_user: Sentry.method(:set_user)
+	)
+
+	message to: /\A#{Regexp.escape(tel)}@#{CONFIG[:component][:jid]}\/?/ do |m|
+		customer_repo.find_by_jid(m.from.stripped).then { |customer|
+			customer.stanza_from(m)
+		}.catch_only(CustomerRepo::NotFound) {
+			# This should not happen, but let's still get the message
+			# to support at least if it does
+			m.from = ProxiedJID.proxy(m.from, CONFIG[:component][:jid])
+			m.to = jid
+			BLATHER << m
+		}
+	end
+
+	message to: /\Acustomer_/, from: /\A#{Regexp.escape(jid)}\/?/ do |m|
+		customer_repo.find(m.to.node.delete_prefix("customer_")).then { |customer|
+			m.from = "#{tel}@sgx-jmp" # stanza_to will fix domain
+			customer.stanza_to(m)
+		}.catch_only(CustomerRepo::NotFound) { |e|
+			BLATHER << m.as_error("item-not-found", :cancel, e.message)
+		}
+	end
+end
+
 message do |m|
 	StatsD.increment("message")