sgx_jmp.rb

  1# frozen_string_literal: true
  2
  3require "pg/em"
  4require "bigdecimal"
  5require "blather/client/dsl" # Require this first to not auto-include
  6require "blather/client"
  7require "braintree"
  8require "dhall"
  9require "em-hiredis"
 10require "em_promise"
 11
 12singleton_class.class_eval do
 13	include Blather::DSL
 14	Blather::DSL.append_features(self)
 15end
 16
 17require_relative "lib/btc_sell_prices"
 18require_relative "lib/buy_account_credit_form"
 19require_relative "lib/customer"
 20require_relative "lib/electrum"
 21require_relative "lib/em"
 22require_relative "lib/payment_methods"
 23require_relative "lib/registration"
 24require_relative "lib/transaction"
 25require_relative "lib/web_register_manager"
 26
 27CONFIG =
 28	Dhall::Coder
 29	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
 30	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
 31
 32ELECTRUM = Electrum.new(**CONFIG[:electrum])
 33
 34# Braintree is not async, so wrap in EM.defer for now
 35class AsyncBraintree
 36	def initialize(environment:, merchant_id:, public_key:, private_key:, **)
 37		@gateway = Braintree::Gateway.new(
 38			environment: environment,
 39			merchant_id: merchant_id,
 40			public_key: public_key,
 41			private_key: private_key
 42		)
 43	end
 44
 45	def respond_to_missing?(m, *)
 46		@gateway.respond_to?(m)
 47	end
 48
 49	def method_missing(m, *args)
 50		return super unless respond_to_missing?(m, *args)
 51
 52		EM.promise_defer(klass: PromiseChain) do
 53			@gateway.public_send(m, *args)
 54		end
 55	end
 56
 57	class PromiseChain < EMPromise
 58		def respond_to_missing?(*)
 59			false # We don't actually know what we respond to...
 60		end
 61
 62		def method_missing(m, *args)
 63			return super if respond_to_missing?(m, *args)
 64			self.then { |o| o.public_send(m, *args) }
 65		end
 66	end
 67end
 68
 69BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
 70
 71def panic(e)
 72	warn "Error raised during event loop: #{e.message}"
 73	warn e.backtrace if e.respond_to?(:backtrace)
 74	exit 1
 75end
 76
 77EM.error_handler(&method(:panic))
 78
 79when_ready do
 80	BLATHER = self
 81	REDIS = EM::Hiredis.connect
 82	BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
 83	DB = PG::EM::Client.new(dbname: "jmp")
 84	DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
 85	DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
 86
 87	EM.add_periodic_timer(3600) do
 88		ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG[:server][:host])
 89		ping.from = CONFIG[:component][:jid]
 90		self << ping
 91	end
 92end
 93
 94# workqueue_count MUST be 0 or else Blather uses threads!
 95setup(
 96	CONFIG[:component][:jid],
 97	CONFIG[:component][:secret],
 98	CONFIG[:server][:host],
 99	CONFIG[:server][:port],
100	nil,
101	nil,
102	workqueue_count: 0
103)
104
105message :error? do |m|
106	puts "MESSAGE ERROR: #{m.inspect}"
107end
108
109class SessionManager
110	def initialize(blather, id_msg, timeout: 5)
111		@blather = blather
112		@sessions = {}
113		@id_msg = id_msg
114		@timeout = timeout
115	end
116
117	def promise_for(stanza)
118		id = "#{stanza.to.stripped}/#{stanza.public_send(@id_msg)}"
119		@sessions.fetch(id) do
120			@sessions[id] = EMPromise.new
121			EM.add_timer(@timeout) do
122				@sessions.delete(id)&.reject(:timeout)
123			end
124			@sessions[id]
125		end
126	end
127
128	def write(stanza)
129		promise = promise_for(stanza)
130		@blather << stanza
131		promise
132	end
133
134	def fulfill(stanza)
135		id = "#{stanza.from.stripped}/#{stanza.public_send(@id_msg)}"
136		if stanza.error?
137			@sessions.delete(id)&.reject(stanza)
138		else
139			@sessions.delete(id)&.fulfill(stanza)
140		end
141	end
142end
143
144IQ_MANAGER = SessionManager.new(self, :id)
145COMMAND_MANAGER = SessionManager.new(self, :sessionid, timeout: 60 * 60)
146web_register_manager = WebRegisterManager.new
147
148disco_items node: "http://jabber.org/protocol/commands" do |iq|
149	reply = iq.reply
150	reply.items = [
151		# TODO: don't show this item if no braintree methods available
152		# TODO: don't show this item if no plan for this customer
153		Blather::Stanza::DiscoItems::Item.new(
154			iq.to,
155			"buy-credit",
156			"Buy account credit"
157		),
158		Blather::Stanza::DiscoItems::Item.new(
159			iq.to,
160			"jabber:iq:register",
161			"Register"
162		)
163	]
164	self << reply
165end
166
167command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
168	Customer.for_jid(iq.from.stripped).catch {
169		nil
170	}.then { |customer|
171		Registration.for(
172			iq,
173			customer,
174			web_register_manager
175		).then(&:write)
176	}.catch(&method(:panic))
177end
178
179def reply_with_note(iq, text, type: :info)
180	reply = iq.reply
181	reply.status = :completed
182	reply.note_type = type
183	reply.note_text = text
184
185	self << reply
186end
187
188command :execute?, node: "buy-credit", sessionid: nil do |iq|
189	reply = iq.reply
190	reply.allowed_actions = [:complete]
191
192	Customer.for_jid(iq.from.stripped).then { |customer|
193		BuyAccountCreditForm.new(customer).add_to_form(reply.form).then { customer }
194	}.then { |customer|
195		EMPromise.all([
196			customer.payment_methods,
197			customer.merchant_account,
198			COMMAND_MANAGER.write(reply)
199		])
200	}.then { |(payment_methods, merchant_account, iq2)|
201		iq = iq2 # This allows the catch to use it also
202		payment_method = payment_methods.fetch(
203			iq.form.field("payment_method")&.value.to_i
204		)
205		amount = iq.form.field("amount").value.to_s
206		Transaction.sale(merchant_account, payment_method, amount)
207	}.then { |transaction|
208		transaction.insert.then { transaction.amount }
209	}.then { |amount|
210		reply_with_note(iq, "$#{'%.2f' % amount} added to your account balance.")
211	}.catch { |e|
212		text = "Failed to buy credit, system said: #{e.message}"
213		reply_with_note(iq, text, type: :error)
214	}.catch(&method(:panic))
215end
216
217command sessionid: /./ do |iq|
218	COMMAND_MANAGER.fulfill(iq)
219end
220
221iq :result? do |iq|
222	IQ_MANAGER.fulfill(iq)
223end
224
225iq :error? do |iq|
226	IQ_MANAGER.fulfill(iq)
227end