snikket.rb

  1# frozen_string_literal: true
  2
  3require "blather"
  4require "em-http"
  5require "em_promise"
  6require "em-synchrony/em-http" # For apost vs post
  7require "link-header-parser"
  8
  9module Snikket
 10	class Launch < Blather::Stanza::Iq
 11		register nil, "launch", "xmpp:snikket.org/hosting/v1"
 12
 13		def self.new(type=nil, to=nil, id=nil, domain: nil)
 14			stanza = super(type || :set, to, id)
 15			node = Nokogiri::XML::Node.new("launch", stanza.document)
 16			node.default_namespace = registered_ns
 17			stanza << node
 18			stanza.domain = domain if domain
 19			stanza
 20		end
 21
 22		def domain=(domain)
 23			query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
 24			node = Nokogiri::XML::Node.new("domain", document)
 25			node.default_namespace = self.class.registered_ns
 26			node.content = domain
 27			query << node
 28		end
 29
 30		def query
 31			at_xpath("./ns:launch", ns: self.class.registered_ns)
 32		end
 33	end
 34
 35	class Launched < Blather::Stanza::Iq
 36		register :snikket_launched, "launched", "xmpp:snikket.org/hosting/v1"
 37
 38		def instance_id
 39			query
 40				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
 41				&.content
 42		end
 43
 44		def bootstrap_token
 45			query
 46				.at_xpath("./ns:bootstrap/ns:token", ns: self.class.registered_ns)
 47				&.content
 48		end
 49
 50		def query
 51			at_xpath("./ns:launched", ns: self.class.registered_ns)
 52		end
 53	end
 54
 55	class Instance < Blather::Stanza::Iq
 56		register :snikket_instance, "instance", "xmpp:snikket.org/hosting/v1"
 57
 58		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
 59			stanza = super(type || :get, to, id)
 60			node = Nokogiri::XML::Node.new("instance", stanza.document)
 61			node.default_namespace = registered_ns
 62			stanza << node
 63			stanza.instance_id = instance_id if instance_id
 64			stanza
 65		end
 66
 67		def inherit(*)
 68			query.remove
 69			super
 70		end
 71
 72		def instance_id=(instance_id)
 73			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
 74			node = Nokogiri::XML::Node.new("instance-id", document)
 75			node.default_namespace = self.class.registered_ns
 76			node.content = instance_id
 77			query << node
 78		end
 79
 80		def instance_id
 81			query
 82				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
 83				&.content
 84		end
 85
 86		def update_needed?
 87			!!query.at_xpath("./ns:update-needed", ns: self.class.registered_ns)
 88		end
 89
 90		def operation
 91			query.at_xpath("./ns:operation", ns: self.class.registered_ns)
 92		end
 93
 94		def status
 95			query
 96				.at_xpath("./ns:status", ns: self.class.registered_ns)
 97				&.content&.to_sym
 98		end
 99
100		def query
101			at_xpath("./ns:instance", ns: self.class.registered_ns)
102		end
103	end
104
105	class CustomerInstance
106		def self.for(customer, domain, launched)
107			new(
108				instance_id: launched.instance_id,
109				bootstrap_token: launched.bootstrap_token,
110				customer_id: customer.customer_id,
111				domain: domain
112			)
113		end
114
115		value_semantics do
116			instance_id     String
117			bootstrap_token String
118			customer_id     String
119			domain          String
120		end
121
122		def bootstrap_uri
123			"https://#{domain}/invites_bootstrap?token=#{bootstrap_token}"
124		end
125
126		def fetch_invite
127			url = bootstrap_uri
128			EM::HttpRequest.new(
129				url, tls: { verify_peer: true }
130			).ahead(redirects: 5).then { |res|
131				LinkHeaderParser.parse(
132					Array(res.response_header["LINK"]), base: url
133				).group_by_relation_type[:alternate]&.find do |header|
134					URI.parse(header.target_uri).scheme == "xmpp"
135				end&.target_uri
136			}.catch { nil }
137		end
138	end
139
140	class Repo
141		def initialize(db: LazyObject.new { DB })
142			@db = db
143		end
144
145		def find_by_customer(customer)
146			promise = @db.query_defer(<<~SQL, [customer.customer_id])
147				SELECT instance_id, bootstrap_token, customer_id, domain
148				FROM snikket_instances
149				WHERE customer_id=$1
150			SQL
151			promise.then do |rows|
152				rows.map { |row| CustomerInstance.new(**row.transform_keys(&:to_sym)) }
153			end
154		end
155
156		def put(instance)
157			params = [
158				instance.instance_id, instance.bootstrap_token,
159				instance.customer_id, instance.domain
160			]
161			@db.exec_defer(<<~SQL, params)
162				INSERT INTO snikket_instances
163					(instance_id, bootstrap_token, customer_id, domain)
164				VALUES
165					($1, $2, $3, $4)
166			SQL
167		end
168	end
169end