# frozen_string_literal: true
require "pg/em"
require "bigdecimal"
require "blather/client"
require "braintree"
require "dhall"
require "em-hiredis"
require "em_promise"
require_relative "lib/buy_account_credit_form"
require_relative "lib/customer"
require_relative "lib/em"
require_relative "lib/payment_methods"
require_relative "lib/transaction"
CONFIG =
Dhall::Coder
.new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
# Braintree is not async, so wrap in EM.defer for now
class AsyncBraintree
def initialize(environment:, merchant_id:, public_key:, private_key:, **)
@gateway = Braintree::Gateway.new(
environment: environment,
merchant_id: merchant_id,
public_key: public_key,
private_key: private_key
)
end
def respond_to_missing?(m, *)
@gateway.respond_to?(m)
end
def method_missing(m, *args)
return super unless respond_to_missing?(m, *args)
EM.promise_defer(klass: PromiseChain) do
@gateway.public_send(m, *args)
end
end
class PromiseChain < EMPromise
def respond_to_missing?(*)
false # We don't actually know what we respond to...
end
def method_missing(m, *args)
return super if respond_to_missing?(m, *args)
self.then { |o| o.public_send(m, *args) }
end
end
end
BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
Blather::DSL.append_features(self.class)
def panic(e)
warn "Error raised during event loop: #{e.message}"
exit 1
end
EM.error_handler(&method(:panic))
when_ready do
REDIS = EM::Hiredis.connect
DB = PG::EM::Client.new(dbname: "jmp")
DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
EM.add_periodic_timer(3600) do
ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG[:server][:host])
ping.from = CONFIG[:component][:jid]
self << ping
end
end
# workqueue_count MUST be 0 or else Blather uses threads!
setup(
CONFIG[:component][:jid],
CONFIG[:component][:secret],
CONFIG[:server][:host],
CONFIG[:server][:port],
nil,
nil,
workqueue_count: 0
)
message :error? do |m|
puts "MESSAGE ERROR: #{m.inspect}"
end
class SessionManager
def initialize(blather, id_msg, timeout: 5)
@blather = blather
@sessions = {}
@id_msg = id_msg
@timeout = timeout
end
def promise_for(stanza)
id = "#{stanza.to.stripped}/#{stanza.public_send(@id_msg)}"
@sessions.fetch(id) do
@sessions[id] = EMPromise.new
EM.add_timer(@timeout) do
@sessions.delete(id)&.reject(:timeout)
end
@sessions[id]
end
end
def write(stanza)
promise = promise_for(stanza)
@blather << stanza
promise
end
def fulfill(stanza)
id = "#{stanza.from.stripped}/#{stanza.public_send(@id_msg)}"
@sessions.delete(id)&.fulfill(stanza)
end
end
IQ_MANAGER = SessionManager.new(self, :id)
COMMAND_MANAGER = SessionManager.new(self, :sessionid, timeout: 60 * 60)
disco_items node: "http://jabber.org/protocol/commands" do |iq|
reply = iq.reply
reply.items = [
# TODO: don't show this item if no braintree methods available
# TODO: don't show this item if no plan for this customer
Blather::Stanza::DiscoItems::Item.new(
iq.to,
"buy-credit",
"Buy account credit"
)
]
self << reply
end
def reply_with_note(iq, text, type: :info)
reply = iq.reply
reply.status = :completed
reply.note_type = type
reply.note_text = text
self << reply
end
command :execute?, node: "buy-credit", sessionid: nil do |iq|
reply = iq.reply
reply.allowed_actions = [:complete]
Customer.for_jid(iq.from.stripped).then { |customer|
BuyAccountCreditForm.new(customer).add_to_form(reply.form).then { customer }
}.then { |customer|
EMPromise.all([
customer.payment_methods,
customer.merchant_account,
COMMAND_MANAGER.write(reply)
])
}.then { |(payment_methods, merchant_account, iq2)|
iq = iq2 # This allows the catch to use it also
payment_method = payment_methods.fetch(
iq.form.field("payment_method")&.value.to_i
)
amount = iq.form.field("amount").value.to_s
Transaction.sale(merchant_account, payment_method, amount)
}.then { |transaction|
transaction.insert.then { transaction.amount }
}.then { |amount|
reply_with_note(iq, "$#{'%.2f' % amount} added to your account balance.")
}.catch { |e|
text = "Failed to buy credit, system said: #{e.message}"
reply_with_note(iq, text, type: :error)
}.catch(&method(:panic))
end
command sessionid: /./ do |iq|
COMMAND_MANAGER.fulfill(iq)
end
iq :result? do |iq|
IQ_MANAGER.fulfill(iq)
end
iq :error? do |iq|
IQ_MANAGER.fulfill(iq)
end