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, boostrap_token, customer_id, domain)
164 VALUES
165 ($1, $2, $3, $4)
166 SQL
167 end
168 end
169end