diff --git a/config-schema.dhall b/config-schema.dhall
index e34ee2e19c4df5c62ae988e3095a697662a6cdd5..36f07d4118cc7261be71c281c3b1d800a9489386 100644
--- a/config-schema.dhall
+++ b/config-schema.dhall
@@ -41,6 +41,7 @@
, sgx : Text
, 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 >
diff --git a/config.dhall.sample b/config.dhall.sample
index b16a3c4430d02f008c1ecd0402fafe08d4a79dad..59d7f081955a309ec4c1c4645e240ed5cc9b5f42 100644
--- a/config.dhall.sample
+++ b/config.dhall.sample
@@ -79,6 +79,7 @@ in
unbilled_targets = ["+14169938000"],
keep_area_codes = ["555"],
keep_area_codes_in = { account = "", site_id = "", sip_peer_id = "" },
+ snikket_hosting_api = "",
upstream_domain = "example.net",
approved_domains = toMap { `example.com` = Some "customer_id" }
}
diff --git a/forms/snikket_launch.rb b/forms/snikket_launch.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6989cccb5957e555339522946614d308f6b9f79a
--- /dev/null
+++ b/forms/snikket_launch.rb
@@ -0,0 +1,9 @@
+form!
+
+title "Launch Snikket Instance"
+
+field(
+ var: "domain",
+ label: "Domain for Instance",
+ type: "text-single"
+)
diff --git a/forms/snikket_launched.rb b/forms/snikket_launched.rb
new file mode 100644
index 0000000000000000000000000000000000000000..749ed11b8bfb1c87ee550d6e68dbdc8d45d4589f
--- /dev/null
+++ b/forms/snikket_launched.rb
@@ -0,0 +1,19 @@
+result!
+
+title "Snikket Instance Lauching"
+
+instructions "Snikket instance is launching now. Please wait a few minutes."
+
+field(
+ type: "text-single",
+ var: "instance-id",
+ label: "Instance ID",
+ value: @launched.instance_id
+)
+
+field(
+ type: "text-single",
+ var: "bootstrap-uri",
+ label: "Admin Invite",
+ value: @launched.bootstrap_uri(@domain)
+)
diff --git a/lib/snikket.rb b/lib/snikket.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3403d1c6335b5b2075da209cc5cef09036bee366
--- /dev/null
+++ b/lib/snikket.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require "blather"
+
+module Snikket
+ class Launch < Blather::Stanza::Iq
+ register nil, "launch", "xmpp:snikket.org/hosting/v1"
+
+ def self.new(type=nil, to=nil, id=nil, domain: nil)
+ stanza = super(type || :set, to, id)
+ node = Nokogiri::XML::Node.new("launch", stanza.document)
+ node.default_namespace = registered_ns
+ stanza << node
+ stanza.domain = domain if domain
+ stanza
+ end
+
+ def domain=(domain)
+ query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
+ node = Nokogiri::XML::Node.new("domain", document)
+ node.default_namespace = self.class.registered_ns
+ node.content = domain
+ query << node
+ end
+
+ def query
+ at_xpath("./ns:launch", ns: self.class.registered_ns)
+ end
+ end
+
+ class Launched < Blather::Stanza::Iq
+ register :snikket_launched, "launched", "xmpp:snikket.org/hosting/v1"
+
+ def instance_id
+ query
+ .at_xpath("./ns:instance-id", ns: self.class.registered_ns)
+ &.content
+ end
+
+ def bootstrap_token
+ query
+ .at_xpath("./ns:bootstrap/ns:token", ns: self.class.registered_ns)
+ &.content
+ end
+
+ def bootstrap_uri(instance_domain)
+ "https://#{instance_domain}/invites_bootstrap?token=#{bootstrap_token}"
+ end
+
+ def query
+ at_xpath("./ns:launched", ns: self.class.registered_ns)
+ end
+ end
+
+ class Instance < Blather::Stanza::Iq
+ register :snikket_instance, "instance", "xmpp:snikket.org/hosting/v1"
+
+ def self.new(type=nil, to=nil, id=nil, instance_id: nil)
+ stanza = super(type || :get, to, id)
+ node = Nokogiri::XML::Node.new("instance", stanza.document)
+ node.default_namespace = registered_ns
+ stanza << node
+ stanza.instance_id = instance_id if instance_id
+ stanza
+ end
+
+ def inherit(*)
+ query.remove
+ super
+ end
+
+ def instance_id=(instance_id)
+ query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
+ node = Nokogiri::XML::Node.new("instance-id", document)
+ node.default_namespace = self.class.registered_ns
+ node.content = instance_id
+ query << node
+ end
+
+ def instance_id
+ query
+ .at_xpath("./ns:instance-id", ns: self.class.registered_ns)
+ &.content
+ end
+
+ def update_needed?
+ !!query.at_xpath("./ns:update-needed", ns: self.class.registered_ns)
+ end
+
+ def operation
+ query.at_xpath("./ns:operation", ns: self.class.registered_ns)
+ end
+
+ def status
+ query
+ .at_xpath("./ns:status", ns: self.class.registered_ns)
+ &.content&.to_sym
+ end
+
+ def query
+ at_xpath("./ns:instance", ns: self.class.registered_ns)
+ end
+ end
+end
diff --git a/sgx_jmp.rb b/sgx_jmp.rb
index 80a9c92cabbd66e21e060e7325cc1a23ff44bdbd..d347337f5e420c056b61901fec0a0101735a2b0d 100644
--- a/sgx_jmp.rb
+++ b/sgx_jmp.rb
@@ -97,6 +97,7 @@ require_relative "lib/registration"
require_relative "lib/transaction"
require_relative "lib/tel_selections"
require_relative "lib/session_manager"
+require_relative "lib/snikket"
require_relative "lib/statsd"
require_relative "web"
@@ -772,6 +773,37 @@ Command.new(
end
}.register(self).then(&CommandList.method(:register))
+Command.new(
+ "snikket",
+ "Launch Snikket Instance",
+ list_for: ->(customer: nil, **) { customer&.admin? }
+) {
+ Command.customer.then do |customer|
+ raise AuthError, "You are not an admin" unless customer&.admin?
+
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << FormTemplate.render("snikket_launch")
+ }.then { |response|
+ domain = response.form.field("domain").value.to_s
+ IQ_MANAGER.write(Snikket::Launch.new(
+ nil, CONFIG[:snikket_hosting_api],
+ domain: domain
+ )).then do |launched|
+ [domain, launched]
+ end
+ }.then { |(domain, launched)|
+ Command.finish do |reply|
+ reply.command << FormTemplate.render(
+ "snikket_launched",
+ launched: launched,
+ domain: domain
+ )
+ end
+ }
+ end
+}.register(self).then(&CommandList.method(:register))
+
def reply_with_note(iq, text, type: :info)
reply = iq.reply
reply.status = :completed
diff --git a/test/test_snikket.rb b/test/test_snikket.rb
new file mode 100644
index 0000000000000000000000000000000000000000..26e89f258a7bcf51bf379146c7b947a7e6d2bf5a
--- /dev/null
+++ b/test/test_snikket.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require "snikket"
+
+class TestSnikket < Minitest::Test
+ NS = "xmpp:snikket.org/hosting/v1"
+
+ def test_launch
+ launch = Snikket::Launch.new(domain: "example.com")
+ assert_equal :set, launch.type
+ assert_equal NS, launch.query.namespace.href
+ assert_equal "launch", launch.query.node_name
+ assert_equal(
+ "example.com",
+ launch.query.at_xpath("./ns:domain", ns: NS).content
+ )
+ end
+
+ def test_launched
+ launched = Blather::XMPPNode.parse(<<~XML)
+
+
+
+ fZLy6iTh
+
+ si-12345
+
+
+ XML
+ assert_equal :result, launched.type
+ assert_equal NS, launched.query.namespace.href
+ assert_equal "launched", launched.query.node_name
+ assert_equal(
+ "https://example.com/invites_bootstrap?token=fZLy6iTh",
+ launched.bootstrap_uri("example.com")
+ )
+ assert_equal "si-12345", launched.instance_id
+ end
+
+ def test_instance_get
+ instance = Snikket::Instance.new(instance_id: "si-1234")
+ assert_equal :get, instance.type
+ assert_equal NS, instance.query.namespace.href
+ assert_equal "instance", instance.query.node_name
+ assert_equal(
+ "si-1234",
+ instance.query.at_xpath("./ns:instance-id", ns: NS).content
+ )
+ end
+
+ def test_instance_result
+ instance = Blather::XMPPNode.parse(<<~XML)
+
+
+
+ si-1234
+
+
+ running
+
+ up
+
+
+ XML
+ assert_equal :result, instance.type
+ assert_equal NS, instance.query.namespace.href
+ assert_equal "instance", instance.query.node_name
+ assert_equal "si-1234", instance.instance_id
+ assert instance.update_needed?
+ assert_kind_of Nokogiri::XML::Element, instance.operation
+ assert_equal :up, instance.status
+ end
+end