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(
 14			type=nil, to=nil, id=nil,
 15			domain: nil, region: nil, test_instance: false, av_region: "na"
 16		)
 17			stanza = super(type || :set, to, id)
 18			node = Nokogiri::XML::Node.new("launch", stanza.document)
 19			node.default_namespace = registered_ns
 20			stanza << node
 21			stanza.domain = domain if domain
 22			stanza.region = region if region
 23			stanza.av_region = av_region if av_region
 24			stanza.test_instance = test_instance
 25			stanza
 26		end
 27
 28		def domain=(domain)
 29			query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
 30			node = Nokogiri::XML::Node.new("domain", document)
 31			node.default_namespace = self.class.registered_ns
 32			node.content = domain
 33			query << node
 34		end
 35
 36		def region=(domain)
 37			query.at_xpath("./ns:region", ns: self.class.registered_ns)&.remove
 38			node = Nokogiri::XML::Node.new("region", document)
 39			node.default_namespace = self.class.registered_ns
 40			node.content = domain
 41			query << node
 42		end
 43
 44		def av_region=(domain)
 45			query.at_xpath("./ns:av-region", ns: self.class.registered_ns)&.remove
 46			node = Nokogiri::XML::Node.new("av-region", document)
 47			node.default_namespace = self.class.registered_ns
 48			node.content = domain
 49			query << node
 50		end
 51
 52		def query
 53			at_xpath("./ns:launch", ns: self.class.registered_ns)
 54		end
 55
 56		def test_instance=(enabled)
 57			query.at_xpath("./ns:test-instance", ns: self.class.registered_ns)&.remove
 58			return unless enabled
 59
 60			node = Nokogiri::XML::Node.new("test-instance", document)
 61			node.default_namespace = self.class.registered_ns
 62			query << node
 63		end
 64	end
 65
 66	class Launched < Blather::Stanza::Iq
 67		register :snikket_launched, "launched", "xmpp:snikket.org/hosting/v1"
 68
 69		def instance_id
 70			query
 71				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
 72				&.content
 73		end
 74
 75		def bootstrap_token
 76			query
 77				.at_xpath("./ns:bootstrap/ns:token", ns: self.class.registered_ns)
 78				&.content
 79		end
 80
 81		def status
 82			query
 83				.at_xpath("./ns:status", ns: self.class.registered_ns)
 84				&.content&.to_sym
 85		end
 86
 87		def records
 88			query
 89				.xpath("./ns:records/ns:record", ns: self.class.registered_ns)
 90				&.map(&Record.method(:new))
 91		end
 92
 93		def query
 94			at_xpath("./ns:launched", ns: self.class.registered_ns)
 95		end
 96	end
 97
 98	class Record
 99		NS = "xmpp:snikket.org/hosting/v1"
100
101		def initialize(element)
102			@element = element
103		end
104
105		def name
106			@element
107				.at_xpath("./ns:name", ns: NS)
108				&.content
109		end
110
111		def type
112			@element
113				.at_xpath("./ns:type", ns: NS)
114				&.content
115		end
116
117		def status
118			@element
119				.at_xpath("./ns:status", ns: NS)
120				&.content&.to_sym
121		end
122
123		def expected
124			@element
125				.xpath("./ns:expected/ns:value", ns: NS)
126				&.map(&:content)
127		end
128
129		def found
130			@element
131				.xpath("./ns:found/ns:value", ns: NS)
132				&.map(&:content)
133		end
134	end
135
136	class Instance < Blather::Stanza::Iq
137		register :snikket_instance, "instance", "xmpp:snikket.org/hosting/v1"
138
139		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
140			stanza = super(type || :get, to, id)
141			node = Nokogiri::XML::Node.new("instance", stanza.document)
142			node.default_namespace = registered_ns
143			stanza << node
144			stanza.instance_id = instance_id if instance_id
145			stanza
146		end
147
148		def inherit(*)
149			query.remove
150			super
151		end
152
153		def instance_id=(instance_id)
154			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
155			node = Nokogiri::XML::Node.new("instance-id", document)
156			node.default_namespace = self.class.registered_ns
157			node.content = instance_id
158			query << node
159		end
160
161		def instance_id
162			query
163				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
164				&.content
165		end
166
167		def update_needed?
168			!!query.at_xpath("./ns:update-needed", ns: self.class.registered_ns)
169		end
170
171		def operation
172			query.at_xpath("./ns:operation", ns: self.class.registered_ns)
173		end
174
175		def status
176			query
177				.at_xpath("./ns:status", ns: self.class.registered_ns)
178				&.content&.to_sym
179		end
180
181		def query
182			at_xpath("./ns:instance", ns: self.class.registered_ns)
183		end
184	end
185
186	class Stop < Blather::Stanza::Iq
187		register nil, "stop", "xmpp:snikket.org/hosting/v1"
188
189		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
190			stanza = super(type || :set, to, id)
191			node = Nokogiri::XML::Node.new("stop", stanza.document)
192			node.default_namespace = registered_ns
193			stanza << node
194			stanza.instance_id = instance_id if instance_id
195			stanza
196		end
197
198		def instance_id=(instance_id)
199			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
200			node = Nokogiri::XML::Node.new("instance-id", document)
201			node.default_namespace = self.class.registered_ns
202			node.content = instance_id
203			query << node
204		end
205
206		def query
207			at_xpath("./ns:stop", ns: self.class.registered_ns)
208		end
209	end
210
211	class Delete < Blather::Stanza::Iq
212		register nil, "delete", "xmpp:snikket.org/hosting/v1"
213
214		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
215			stanza = super(type || :set, to, id)
216			node = Nokogiri::XML::Node.new("delete", stanza.document)
217			node.default_namespace = registered_ns
218			stanza << node
219			stanza.instance_id = instance_id if instance_id
220			stanza
221		end
222
223		def instance_id=(instance_id)
224			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
225			node = Nokogiri::XML::Node.new("instance-id", document)
226			node.default_namespace = self.class.registered_ns
227			node.content = instance_id
228			query << node
229		end
230
231		def query
232			at_xpath("./ns:delete", ns: self.class.registered_ns)
233		end
234	end
235
236	class DomainInfo < Blather::Stanza::Iq
237		register nil, "domain-info", "xmpp:snikket.org/hosting/v1"
238
239		def self.new(type=nil, to=nil, id=nil, domain: nil)
240			stanza = super(type || :get, to, id)
241			if domain
242				node = Nokogiri::XML::Node.new("domain-info", stanza.document)
243				node.default_namespace = registered_ns
244				stanza << node
245			end
246			stanza.domain = domain if domain
247			stanza
248		end
249
250		def domain=(domain)
251			query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
252			node = Nokogiri::XML::Node.new("domain", document)
253			node.default_namespace = self.class.registered_ns
254			node.content = domain
255			query << node
256		end
257
258		def instance_id
259			query
260				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
261				&.content
262		end
263
264		def available?
265			!!query.at_xpath("./ns:available", ns: self.class.registered_ns)
266		end
267
268		def custom?
269			!!query.at_xpath("./ns:custom", ns: self.class.registered_ns)
270		end
271
272		def query
273			at_xpath("./ns:domain-info", ns: self.class.registered_ns)
274		end
275	end
276
277	class CustomerInstance
278		def self.for(customer, domain, launched)
279			new(
280				instance_id: launched.instance_id,
281				bootstrap_token: launched.bootstrap_token || "",
282				customer_id: customer.customer_id,
283				domain: domain
284			)
285		end
286
287		value_semantics do
288			instance_id     String
289			bootstrap_token String
290			customer_id     String
291			domain          String
292		end
293
294		def bootstrap_uri
295			"https://#{domain}/invites_bootstrap?token=#{bootstrap_token}"
296		end
297
298		def fetch_invite
299			url = bootstrap_uri
300			EM::HttpRequest.new(
301				url, tls: { verify_peer: true }
302			).ahead(redirects: 5).then { |res|
303				LinkHeaderParser.parse(
304					Array(res.response_header["LINK"]), base: url
305				).group_by_relation_type[:alternate]&.find do |header|
306					URI.parse(header.target_uri).scheme == "xmpp"
307				end&.target_uri
308			}.catch { nil }
309		end
310	end
311
312	class Repo
313		def initialize(db: LazyObject.new { DB })
314			@db = db
315		end
316
317		def find_by_customer(customer)
318			promise = @db.query_defer(<<~SQL, [customer.customer_id])
319				SELECT instance_id, bootstrap_token, customer_id, domain
320				FROM snikket_instances
321				WHERE customer_id=$1
322			SQL
323			promise.then do |rows|
324				rows.map { |row| CustomerInstance.new(**row.transform_keys(&:to_sym)) }
325			end
326		end
327
328		def del(instance)
329			return EMPromise.resolve(nil) unless instance
330
331			params = [instance.instance_id, instance.customer_id, instance.domain]
332
333			IQ_MANAGER.write(Delete.new(
334				nil, CONFIG[:snikket_hosting_api], instance_id: instance.instance_id
335			)).then do
336				@db.exec_defer(<<~SQL, params)
337					DELETE FROM snikket_instances
338					WHERE instance_id=$1 AND customer_id=$2 AND domain=$3
339				SQL
340			end
341		end
342
343		def put(instance)
344			params = [
345				instance.instance_id, instance.bootstrap_token,
346				instance.customer_id, instance.domain
347			]
348			@db.exec_defer(<<~SQL, params)
349				INSERT INTO snikket_instances
350					(instance_id, bootstrap_token, customer_id, domain)
351				VALUES
352					($1, $2, $3, $4)
353				ON CONFLICT (instance_id)
354				DO UPDATE SET bootstrap_token=$2
355			SQL
356		end
357	end
358end