diff --git a/Gemfile b/Gemfile
index 1434dcd91915aa18af9fb265380c573ce9fe7ea1..c49fa1ab1781b561ddee05379edf86cde4f6fcb8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -14,6 +14,7 @@ gem "em_promise.rb", "~> 0.0.4"
gem "em-synchrony"
gem "eventmachine"
gem "faraday-em_http", git: "https://github.com/singpolyma/faraday-em_http", branch: "fix-gzip"
+gem "link-header-parser"
gem "money-open-exchange-rates"
gem "multibases"
gem "multihashes"
diff --git a/forms/registration/snikket.rb b/forms/registration/snikket.rb
new file mode 100644
index 0000000000000000000000000000000000000000..912b1abaca9002a93fdb3bd0eb997eb35d0cf692
--- /dev/null
+++ b/forms/registration/snikket.rb
@@ -0,0 +1,33 @@
+form!
+title "Setup Snikket Instance"
+
+instructions(
+ "Phone number #{@tel} has been activated. " \
+ "Now you will need a Jabber ID to associate with your new JMP account. " \
+ "JMP has partnered with Snikket CIC to provide you with your very own " \
+ "Snikket instance that will provide Jabber service to you and anyone you " \
+ "invite.\n\n" \
+ "A Jabber ID is kind of like an email address, and looks similar too. " \
+ "People who contact you using your Jabber ID instead of your phone number " \
+ "will get additional features, such as video calling and end-to-end " \
+ "encryption.\n\n" \
+ "You will be prompted to choose a username after your instance is ready."
+)
+
+field(
+ label: "Choose a unique name for your Snikket instance",
+ var: "subdomain",
+ suffix: ".snikket.chat",
+ description: @error ||
+ "Your Jabber ID will be of the form username@instance-name.snikket.chat"
+)
+
+field(
+ label: "Action",
+ type: "list-single",
+ var: "http://jabber.org/protocol/commands#actions",
+ options: [
+ { label: "Next", value: "next" },
+ { label: "Use Your Own Domain Name", value: "custom_domain" }
+ ]
+)
diff --git a/lib/registration.rb b/lib/registration.rb
index f4afc671130e6a3ac3bbc6a473584c7505405bf1..aa30b164090b3967d3903947c42e390575c84fdc 100644
--- a/lib/registration.rb
+++ b/lib/registration.rb
@@ -514,6 +514,135 @@ class Registration
put_default_fwd
])
}.then do
+ FinishOnboarding.for(@customer, @tel).write
+ end
+ end
+ end
+
+ module FinishOnboarding
+ def self.for(customer, tel)
+ jid = ProxiedJID.new(customer.jid).unproxied
+ if jid.domain == CONFIG[:onboarding_domain]
+ Snikket.new(tel)
+ else
+ NotOnboarding.new(customer, tel)
+ end
+ end
+
+ class Snikket
+ def initialize(tel, error: nil)
+ @tel = tel
+ @error = error
+ end
+
+ ACTION_VAR = "http://jabber.org/protocol/commands#actions"
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << form
+ }.then do |iq|
+ if iq.form.field(ACTION_VAR)&.value == "custom_domain"
+ CustomDomain.new(@tel).write
+ else
+ launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
+ end
+ end
+ end
+
+ def form
+ FormTemplate.render(
+ "registration/snikket",
+ tel: @tel,
+ error: @error
+ )
+ end
+
+ def launch(domain)
+ IQ_MANAGER.write(::Snikket::Launch.new(
+ nil, CONFIG[:snikket_hosting_api],
+ domain: domain
+ )).then { |launched|
+ GetInvite.for(domain, launched).then(&:write)
+ }.catch { |e|
+ next EMPromise.reject(e) unless e.respond_to?(:text)
+
+ Snikket.new(@tel, error: e.text).write
+ }
+ end
+
+ class GetInvite
+ def self.for(domain, launched)
+ launched.fetch_invite(domain).then do |xmpp_uri|
+ if xmpp_uri
+ GoToInvite.new(xmpp_uri)
+ else
+ new(domain, launched)
+ end
+ end
+ end
+
+ def initialize(domain, launched)
+ @domain = domain
+ @launched = launched
+ end
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.note_type = :info
+ reply.note_text =
+ "Your instance #{@domain} is starting up. " \
+ "This may take several minutes. " \
+ "Press next to check if it is ready."
+ }.then { GetInvite.for(@domain, @launched).then(&:write) }
+ end
+ end
+
+ class GoToInvite
+ def initialize(xmpp_uri)
+ @xmpp_uri = xmpp_uri
+ end
+
+ def write
+ Command.finish do |reply|
+ oob = OOB.find_or_create(reply.command)
+ oob.url = @xmpp_uri
+ end
+ end
+ end
+ end
+
+ class CustomDomain
+ def initialize(tel)
+ @tel = tel
+ end
+
+ CONTACT_SUPPORT =
+ "Please contact JMP support to set up " \
+ "an instance on an existing domain."
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:prev]
+ reply.status = :canceled
+ reply.note_type = :info
+ reply.note_text = CONTACT_SUPPORT
+ }.then do |iq|
+ raise "Action not allowed" unless iq.prev?
+
+ Snikket.new(@tel).write
+ end
+ end
+ end
+
+ class NotOnboarding
+ def initialize(customer, tel)
+ @customer = customer
+ @tel = tel
+ end
+
+ def write
WelcomeMessage.new(@customer, @tel).welcome
Command.finish("Your JMP account has been activated as #{@tel}")
end
diff --git a/lib/snikket.rb b/lib/snikket.rb
index 3403d1c6335b5b2075da209cc5cef09036bee366..aa69e8e8530a2b13f96b7f2155b6fdda09090bd4 100644
--- a/lib/snikket.rb
+++ b/lib/snikket.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
require "blather"
+require "em-http"
+require "em_promise"
+require "em-synchrony/em-http" # For apost vs post
+require "link-header-parser"
module Snikket
class Launch < Blather::Stanza::Iq
@@ -47,6 +51,19 @@ module Snikket
"https://#{instance_domain}/invites_bootstrap?token=#{bootstrap_token}"
end
+ def fetch_invite(instance_domain)
+ url = bootstrap_uri(instance_domain)
+ EM::HttpRequest.new(
+ url, tls: { verify_peer: true }
+ ).ahead(redirects: 5).then { |res|
+ LinkHeaderParser.parse(
+ Array(res.response_header["LINK"]), base: url
+ ).group_by_relation_type[:alternate]&.find do |header|
+ URI.parse(header.target_uri).scheme == "xmpp"
+ end&.target_uri
+ }.catch { nil }
+ end
+
def query
at_xpath("./ns:launched", ns: self.class.registered_ns)
end
diff --git a/sgx_jmp.rb b/sgx_jmp.rb
index e6455d724d2f7f20ebf933711ccf31ce5d1ae8f3..b449e30889cc9f1c1ccea2a3ffc530fe8e49c781 100644
--- a/sgx_jmp.rb
+++ b/sgx_jmp.rb
@@ -103,6 +103,7 @@ require_relative "lib/transaction"
require_relative "lib/tel_selections"
require_relative "lib/sim_repo"
require_relative "lib/snikket"
+require_relative "lib/welcome_message"
require_relative "web"
require_relative "lib/statsd"
@@ -900,6 +901,10 @@ Command.new(
}
}.then {
StatsD.increment("changejid.completed")
+ jid = ProxiedJID.new(customer.jid).unproxied
+ if jid.domain == CONFIG[:onboarding_domain]
+ WelcomeMessage.new(customer, customer.registered?.phone).welcome
+ end
Command.finish { |reply|
reply.note_type = :info
reply.note_text = "Customer JID Changed"
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 6d479f2815f6b7dbf911e65dff121939db02aa5c..be130085d6b42f80758333b9ce6ad6575cc382a1 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -113,6 +113,8 @@ CONFIG = {
bandwidth_site: "test_site",
bandwidth_peer: "test_peer",
keepgo: { api_key: "keepgokey", access_token: "keepgotoken" },
+ snikket_hosting_api: "snikket.example.com",
+ onboarding_domain: "onboarding.example.com",
adr: "A Mailing Address",
interac: "interac@example.com",
support_link: ->(*) { "https://support.com" }
diff --git a/test/test_registration.rb b/test/test_registration.rb
index fca1b8d5ec2b81c2b0a3e29093c40c17574a3eff..dae782c3fec67e3c6c145cd794e254d842fb47da 100644
--- a/test/test_registration.rb
+++ b/test/test_registration.rb
@@ -810,6 +810,89 @@ class RegistrationTest < Minitest::Test
end
em :test_write
+ def test_write_onboarding
+ create_order = stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders"
+ ).to_return(status: 201, body: <<~RESPONSE)
+
+
+ test_order
+
+
+ RESPONSE
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order"
+ ).to_return(status: 201, body: <<~RESPONSE)
+
+ COMPLETE
+
+
+ 5555550000
+
+
+
+ RESPONSE
+ stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//sites//sippeers//movetns"
+ )
+ Registration::Finish::REDIS.expect(
+ :del,
+ nil,
+ ["pending_tel_for-test\\40onboarding.example.com@proxy"]
+ )
+ Bwmsgsv2Repo::REDIS.expect(
+ :set,
+ nil,
+ [
+ "catapult_fwd-+15555550000",
+ "xmpp:test\\40onboarding.example.com@proxy"
+ ]
+ )
+ Bwmsgsv2Repo::REDIS.expect(
+ :set,
+ nil,
+ ["catapult_fwd_timeout-customer_test@component", 25]
+ )
+ result = execute_command do
+ @sgx.expect(
+ :register!,
+ EMPromise.resolve(@sgx.with(
+ registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr|
+ ibr.phone = "+15555550000"
+ end
+ )),
+ ["+15555550000"]
+ )
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::Finish.new(
+ customer(
+ sgx: @sgx,
+ jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+ ),
+ "+15555550000"
+ ).write.catch { |e| e }
+ end
+ assert_equal :test_result, result
+ assert_requested create_order
+ assert_mock @sgx
+ assert_mock Registration::Finish::REDIS
+ assert_mock Bwmsgsv2Repo::REDIS
+ assert_mock Command::COMMAND_MANAGER
+ end
+ em :test_write_onboarding
+
def test_write_tn_fail
create_order = stub_request(
:post,
@@ -856,4 +939,227 @@ class RegistrationTest < Minitest::Test
end
em :test_write_tn_fail
end
+
+ class SnikketTest < Minitest::Test
+ Command::COMMAND_MANAGER = Minitest::Mock.new
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER = Minitest::Mock.new
+
+ def setup
+ @sgx = Minitest::Mock.new(TrivialBackendSgxRepo.new.get("test"))
+ @snikket = Registration::FinishOnboarding::Snikket.new("+15555550000")
+ end
+
+ def test_write
+ xmpp_uri = "xmpp:test.snikket.chat?register;preauth=NEWTOKEN"
+
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new
+ launched << Niceogiri::XML::Node.new(
+ :launched, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |inner|
+ inner << Niceogiri::XML::Node.new(
+ :bootstrap, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |bootstrap|
+ bootstrap << Niceogiri::XML::Node.new(
+ :token, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |token|
+ token.content = "TOKEN"
+ }
+ }
+ }
+
+ blather = Minitest::Mock.new
+ blather.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal(
+ xmpp_uri,
+ OOB.find_or_create(reply.command).url
+ )
+ end]
+ )
+
+ execute_command(blather: blather) do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ # Webmock doesn't support redirects properly, but they work live
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_return(
+ status: 200,
+ headers: {
+ "link" => "<#{xmpp_uri}>; rel=\"alternate\""
+ }
+ )
+
+ @snikket.write
+ end
+
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ assert_mock blather
+ end
+ em :test_write
+
+ def test_write_not_yet
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new
+ launched << Niceogiri::XML::Node.new(
+ :launched, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |inner|
+ inner << Niceogiri::XML::Node.new(
+ :bootstrap, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |bootstrap|
+ bootstrap << Niceogiri::XML::Node.new(
+ :token, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |token|
+ token.content = "TOKEN"
+ }
+ }
+ }
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_timeout
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal :info, iq.note_type
+ assert iq.note.content =~ / test\.snikket\.chat /
+ end]
+ )
+
+ @snikket.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ end
+ em :test_write_not_yet
+
+ def test_write_already_taken
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new.as_error(
+ "internal-server-error",
+ :wait,
+ "This is an error"
+ )
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.reject(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_timeout
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert iq.executing?
+ assert_equal(
+ "This is an error",
+ iq.form.field("subdomain").desc
+ )
+ end]
+ )
+
+ @snikket.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ end
+ em :test_write_already_taken
+ end
end