Detailed changes
@@ -0,0 +1,22 @@
+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: "Domain (or subdomain) name",
+ var: "domain",
+ description: @error ||
+ "Your Jabber ID will be of the form username@domain"
+)
@@ -0,0 +1,16 @@
+form!
+title "Setup Snikket Instance"
+
+instructions(
+ "To complete setup of your instance, please set these DNS records, " \
+ "then press next to check if they have propagated."
+)
+
+table(
+ @records,
+ name: "Name",
+ type: "Type",
+ status: "Status",
+ expected: "Expected",
+ found: "Found"
+)
@@ -612,28 +612,16 @@ class Registration
end
end
- def initialize(customer, tel, error: nil, db:)
+ def initialize(customer, tel, error: nil, old: nil, db:)
@customer = customer
@tel = tel
@error = error
@db = db
+ @old = old
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",
@@ -642,6 +630,25 @@ class Registration
)
end
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << form
+ }.then(&method(:next_step))
+ end
+
+ def next_step(iq)
+ subdomain = iq.form.field("subdomain")&.value
+ domain = "#{subdomain}.snikket.chat"
+ if iq.form.field(ACTION_VAR)&.value == "custom_domain"
+ CustomDomain.new(@tel, old: @old).write
+ elsif @old && (!subdomain || domain == @old.domain)
+ GetInvite.for(@old, @tel).then(&:write)
+ else
+ launch(domain)
+ end
+ end
+
def launch(domain)
IQ_MANAGER.write(::Snikket::Launch.new(
nil, CONFIG[:snikket_hosting_api], domain: domain
@@ -650,40 +657,88 @@ class Registration
}.catch { |e|
next EMPromise.reject(e) unless e.respond_to?(:text)
- Snikket.new(@customer, @tel, error: e.text, db: @db).write
+ Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
}
end
def save_instance_and_wait(domain, launched)
instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
- ::Snikket::Repo.new(db: @db).put(instance).then do
- GetInvite.for(instance).then(&:write)
+ repo = ::Snikket::Repo.new(db: @db)
+ repo.del(@old).then { repo.put(instance) }.then do
+ if launched.status == :needs_dns
+ NeedsDNS.new(@customer, instance, @tel, launched.records).write
+ else
+ GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
+ end
+ end
+ end
+
+ class NeedsDNS < Snikket
+ def initialize(customer, instance, tel, records, db: DB)
+ @customer = customer
+ @instance = instance
+ @tel = tel
+ @records = records
+ @db = db
+ end
+
+ def form
+ FormTemplate.render(
+ "registration/snikket_needs_dns",
+ records: @records
+ )
+ end
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:prev, :next]
+ reply.command << form
+ }.then do |iq|
+ if iq.prev?
+ CustomDomain.new(@tel, old: @instance).write
+ else
+ launch(@instance.domain)
+ end
+ end
end
end
class GetInvite
- def self.for(instance)
+ def self.for(customer, instance, tel, db: DB)
instance.fetch_invite.then do |xmpp_uri|
if xmpp_uri
GoToInvite.new(xmpp_uri)
else
- new(instance)
+ new(customer, instance, tel, db: db)
end
end
end
- def initialize(instance)
+ def initialize(customer, instance, tel, db: DB)
+ @customer = customer
@instance = instance
+ @tel = tel
+ @db = db
+ end
+
+ def form
+ FormTemplate.render(
+ "registration/snikket_wait",
+ domain: @instance.domain
+ )
end
def write
Command.reply { |reply|
- reply.allowed_actions = [:next]
- reply.command << FormTemplate.render(
- "registration/snikket_wait",
- domain: @instance.domain
- )
- }.then { GetInvite.for(@instance).then(&:write) }
+ reply.allowed_actions = [:prev, :next]
+ reply.command << form
+ }.then do |iq|
+ if iq.prev?
+ Snikket.new(@customer, @tel, old: @instance, db: @db).write
+ else
+ GetInvite.for(@instance).then(&:write)
+ end
+ end
end
end
@@ -701,25 +756,33 @@ class Registration
end
end
- class CustomDomain
- def initialize(tel)
+ class CustomDomain < Snikket
+ def initialize(customer, tel, old: nil, error: nil, db: DB)
+ @customer = customer
@tel = tel
+ @error = error
+ @old = old
+ @db = db
end
- CONTACT_SUPPORT =
- "Please contact JMP support to set up " \
- "an instance on an existing domain."
+ def form
+ FormTemplate.render(
+ "registration/snikket_custom",
+ tel: @tel,
+ error: @error
+ )
+ end
def write
Command.reply { |reply|
- reply.allowed_actions = [:prev]
- reply.status = :canceled
- reply.note_type = :info
- reply.note_text = CONTACT_SUPPORT
+ reply.allowed_actions = [:prev, :next]
+ reply.command << form
}.then do |iq|
- raise "Action not allowed" unless iq.prev?
-
- Snikket.new(@tel).write
+ if iq.prev?
+ Snikket.new(@customer, @tel, db: @db, old: @old).write
+ else
+ launch(iq.form.field("domain")&.value)
+ end
end
end
end
@@ -47,11 +47,61 @@ module Snikket
&.content
end
+ def status
+ query
+ .at_xpath("./ns:status", ns: self.class.registered_ns)
+ &.content&.to_sym
+ end
+
+ def records
+ query
+ .xpath("./ns:records/ns:record", ns: self.class.registered_ns)
+ &.map(&Record.method(:new))
+ end
+
def query
at_xpath("./ns:launched", ns: self.class.registered_ns)
end
end
+ class Record
+ NS = "xmpp:snikket.org/hosting/v1"
+
+ def initialize(element)
+ @element = element
+ end
+
+ def name
+ @element
+ .at_xpath("./ns:name", ns: NS)
+ &.content
+ end
+
+ def type
+ @element
+ .at_xpath("./ns:type", ns: NS)
+ &.content
+ end
+
+ def status
+ @element
+ .at_xpath("./ns:status", ns: NS)
+ &.content&.to_sym
+ end
+
+ def expected
+ @element
+ .xpath("./ns:expected/ns:value", ns: NS)
+ &.map(&:content)
+ end
+
+ def found
+ @element
+ .xpath("./ns:found/ns:value", ns: NS)
+ &.map(&:content)
+ end
+ end
+
class Instance < Blather::Stanza::Iq
register :snikket_instance, "instance", "xmpp:snikket.org/hosting/v1"
@@ -102,11 +152,36 @@ module Snikket
end
end
+ class Delete < Blather::Stanza::Iq
+ register nil, "delete", "xmpp:snikket.org/hosting/v1"
+
+ def self.new(type=nil, to=nil, id=nil, instance_id: nil)
+ stanza = super(type || :set, to, id)
+ node = Nokogiri::XML::Node.new("delete", stanza.document)
+ node.default_namespace = registered_ns
+ stanza << node
+ stanza.instance_id = instance_id if instance_id
+ stanza
+ end
+
+ def instance_id=(instance_id)
+ query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
+ node = Nokogiri::XML::Node.new("intance-id", document)
+ node.default_namespace = self.class.registered_ns
+ node.content = instance_id
+ query << node
+ end
+
+ def query
+ at_xpath("./ns:delete", ns: self.class.registered_ns)
+ end
+ end
+
class CustomerInstance
def self.for(customer, domain, launched)
new(
instance_id: launched.instance_id,
- bootstrap_token: launched.bootstrap_token,
+ bootstrap_token: launched.bootstrap_token || "",
customer_id: customer.customer_id,
domain: domain
)
@@ -153,6 +228,21 @@ module Snikket
end
end
+ def del(instance)
+ return EMPromise.resolve(nil) unless instance
+
+ params = [instance.instance_id, instance.customer_id, instance.domain]
+
+ IQ_MANAGER.write(Delete.new(
+ nil, CONFIG[:snikket_hosting_api], instance_id: instance.instance_id
+ )).then do
+ @db.exec_defer(<<~SQL, params)
+ DELETE FROM snikket_instances
+ WHERE instance_id=$1 AND customer_id=$2 AND domain=$3
+ SQL
+ end
+ end
+
def put(instance)
params = [
instance.instance_id, instance.bootstrap_token,
@@ -163,6 +253,8 @@ module Snikket
(instance_id, bootstrap_token, customer_id, domain)
VALUES
($1, $2, $3, $4)
+ ON CONFLICT (instance_id)
+ DO UPDATE SET bootstrap_token=$2
SQL
end
end
@@ -1443,4 +1443,124 @@ class RegistrationTest < Minitest::Test
end
em :test_write_already_taken
end
+
+ class SnikketCustomDomainTest < Minitest::Test
+ def setup
+ @snikket = Registration::FinishOnboarding::CustomDomain.new(
+ customer,
+ "+15555550000",
+ db: FakeDB.new
+ )
+ end
+
+ def test_write_needs_dns
+ domain_form = Blather::Stanza::Iq::Command.new
+ domain_form.form.fields = [
+ { var: "domain", value: "snikket.example.com" }
+ ]
+
+ launched = Snikket::Launched.new
+ launched << Niceogiri::XML::Node.new(
+ :launched, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |inner|
+ inner << Niceogiri::XML::Node.new(
+ :'instance-id', launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |id|
+ id.content = "si-1234"
+ }
+ inner << Niceogiri::XML::Node.new(
+ :status, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |id|
+ id.content = "needs_dns"
+ }
+ inner << Niceogiri::XML::Node.new(
+ :records, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |records|
+ records << Niceogiri::XML::Node.new(
+ :record, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |record|
+ record << Niceogiri::XML::Node.new(
+ :name, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |name| name.content = "snikket.example.com" }
+ record << Niceogiri::XML::Node.new(
+ :type, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |type| type.content = "AAAA" }
+ record << Niceogiri::XML::Node.new(
+ :status, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |type| type.content = "incorrect" }
+ record << Niceogiri::XML::Node.new(
+ :expected, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |expected|
+ expected << Niceogiri::XML::Node.new(
+ :value, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |value| value.content = "1::2" }
+ }
+ record << Niceogiri::XML::Node.new(
+ :found, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |found|
+ found << Niceogiri::XML::Node.new(
+ :value, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |value| value.content = "0::0" }
+ }
+ }
+ }
+ }
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(domain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("domain")
+ 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(
+ "snikket.example.com",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert_equal(
+ ["snikket.example.com"],
+ iq.form.xpath(
+ "./ns:item/ns:field[@var='name']/ns:value",
+ ns: "jabber:x:data"
+ ).map(&:content)
+ )
+ assert_equal(
+ ["1::2"],
+ iq.form.xpath(
+ "./ns:item/ns:field[@var='expected']/ns:value",
+ ns: "jabber:x:data"
+ ).map(&:content)
+ )
+ 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_needs_dns
+ end
end