M Gemfile => Gemfile +2 -2
@@ 10,12 10,12 @@ gem "em-hiredis"
gem "em-http-request"
gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
gem "em-synchrony"
-gem "em_promise.rb", "~> 0.0.2"
+gem "em_promise.rb", "~> 0.0.3"
gem "eventmachine"
gem "money-open-exchange-rates"
gem "ougai"
gem "ruby-bandwidth-iris"
-gem "sentry-ruby"
+gem "sentry-ruby", "<= 4.3.1"
gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"
gem "value_semantics", git: "https://github.com/singpolyma/value_semantics"
A lib/command.rb => lib/command.rb +144 -0
@@ 0,0 1,144 @@
+# frozen_string_literal: true
+
+require_relative "customer_repo"
+
+class Command
+ def self.exection
+ Thread.current[:execution]
+ end
+
+ def self.reply(stanza)
+ execution.reply(stanza)
+ end
+
+ def self.finish(*args, **kwargs, &blk)
+ execution.finish(*args, **kwargs, &blk)
+ end
+
+ def self.customer
+ execution.customer
+ end
+
+ def self.log
+ execution.log
+ end
+
+ class Execution
+ attr_reader :customer_repo, :log, :iq
+
+ def initialize(customer_repo, blather, format_error, iq)
+ @customer_repo = customer_repo
+ @blather = blather
+ @format_error = format_error
+ @iq = iq
+ @log = LOG.child(node: iq.node)
+ end
+
+ def execute
+ StatsD.increment("command", tags: ["node:#{iq.node}"])
+ EMPromise.resolve(nil).then do
+ Thread.current[:execution] = self
+ setup_sentry_hub!
+ catch_after(@blk.call(self))
+ end
+ end
+
+ def reply(stanza=nil)
+ stanza ||= iq.reply.tap do |reply|
+ reply.status = :executing
+ end
+ stanza = yield stanza if block_given?
+ COMMAND_MANAGER.write(stanza).then do |new_iq|
+ @iq = new_iq
+ end
+ end
+
+ def finish(text=nil, type: :info, status: :completed)
+ reply = @iq.reply
+ reply.status = status
+ yield reply if block_given?
+ if text
+ reply.note_type = type
+ reply.note_text = text
+ end
+ @blather << reply
+ @iq = nil
+ end
+
+ def sentry_hub
+ return @sentry_hub if @sentry_hub
+
+ # Stored on Fiber-local in 4.3.1 and earlier
+ # https://github.com/getsentry/sentry-ruby/issues/1495
+ @sentry_hub = Sentry.get_current_hub
+ raise "Sentry.init has not been called" unless @sentry_hub
+
+ @sentry_hub.push_scope
+ @sentry_hub.current_scope.clear_breadcrumbs
+ @sentry_hub.current_scope.set_transaction_name(@iq.node)
+ @sentry_hub.current_scope.set_user(jid: @iq.from.stripped.to_s)
+ @sentry_hub
+ end
+
+ def customer
+ @customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then do |c|
+ sentry_hub.current_scope.set_user(
+ id: c.customer_id,
+ jid: @iq.from.stripped
+ )
+ c
+ end
+ end
+
+ protected
+
+ def catch_after(promise)
+ promise.catch_only(ErrorToSend) { |e|
+ @blather << e.stanza
+ }.catch { |e|
+ log_error(e)
+ finish(format_error(e), type: :error)
+ }.catch { |e| panic(e) }
+ end
+
+ def log_error(e)
+ @log.error(
+ "Error raised during #{iq.node}: #{e.class}",
+ e
+ )
+ if e.is_a?(::Exception)
+ sentry_hub.capture_exception(e)
+ else
+ sentry_hub.capture_message(e.to_s)
+ end
+ end
+ end
+
+ attr_reader :node, :name
+
+ def initialize(
+ node,
+ name,
+ list_for: ->(tel:, **) { !!tel },
+ format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
+ &blk
+ )
+ @node = node
+ @name = name
+ @list_for = list_for
+ @format_error = format_error
+ @blk = blk
+ @customer_repo = CustomerRepo.new
+ end
+
+ def register(blather, command_list)
+ command_list.register(self)
+ blather.command(:execute?, node: @node, sessionid: nil) do |iq|
+ Execution.new(@customer_repo, blather, @format_error, iq).execute
+ end
+ end
+
+ def list_for?(**kwargs)
+ @list_for.call(**kwargs)
+ end
+end
M lib/command_list.rb => lib/command_list.rb +22 -51
@@ 3,65 3,36 @@
class CommandList
include Enumerable
- def self.for(customer)
- EMPromise.resolve(customer&.registered?).catch { nil }.then do |reg|
- next Registered.for(customer, reg.phone) if reg
- CommandList.new
- end
+ def self.register(command)
+ @commands ||= []
+ @commands << command
end
- def each
- yield node: "jabber:iq:register", name: "Register"
- end
-
- class Registered < CommandList
- def self.for(customer, tel)
- EMPromise.all([
- REDIS.get("catapult_fwd-#{tel}"),
- customer.plan_name ? customer.payment_methods : []
- ]).then do |(fwd, payment_methods)|
- Registered.new(*[
- (HAS_CREDIT_CARD unless payment_methods.empty?),
- (HAS_CURRENCY if customer.currency),
- (HAS_FORWARDING if fwd)
- ].compact)
+ def self.for(customer)
+ EMPromise.resolve(customer&.registered?).catch { nil }.then do |reg|
+ args_for(customer, reg).then do |kwargs|
+ new(@commands.select { |c| c.list_for?(**kwargs) })
end
end
+ end
- def initialize(*args)
- @extra = args
- end
-
- ALWAYS = [
- { node: "number-display", name: "Display JMP Number" },
- { node: "configure-calls", name: "Configure Calls" },
- { node: "usage", name: "Show Monthly Usage" },
- { node: "reset sip account", name: "Create or Reset SIP Account" },
- {
- node: "credit cards",
- name: "Credit Card Settings and Management"
- }
- ].freeze
+ def self.args_for(customer, reg)
+ args = { customer: customer, tel: reg ? reg.phone : nil }
+ return EMPromise.resolve(args) unless phone
- def each
- super
- ([ALWAYS] + @extra).each do |commands|
- commands.each { |x| yield x }
- end
+ EMPromise.all([
+ REDIS.get("catapult_fwd-#{tel}"),
+ customer.plan_name ? customer.payment_methods : []
+ ]).then do |(fwd, payment_methods)|
+ args.merge(fwd: fwd, payment_methods: payment_methods)
end
end
- HAS_CURRENCY = [
- node: "alt top up",
- name: "Buy Account Credit by Bitcoin, Mail, or Interac eTransfer"
- ].freeze
-
- HAS_FORWARDING = [
- node: "record-voicemail-greeting",
- name: "Record Voicemail Greeting"
- ].freeze
+ def initialize(commands)
+ @commands = commands
+ end
- HAS_CREDIT_CARD = [
- node: "top up", name: "Buy Account Credit by Credit Card"
- ].freeze
+ def each(&blk)
+ commands.map { |c| { node: c.node, name: c.name } }.each(&blk)
+ end
end
M sgx_jmp.rb => sgx_jmp.rb +90 -131
@@ 42,6 42,7 @@ require_relative "lib/backend_sgx"
require_relative "lib/bandwidth_tn_order"
require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
+require_relative "lib/command"
require_relative "lib/command_list"
require_relative "lib/customer"
require_relative "lib/customer_repo"
@@ 113,7 114,10 @@ end
BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
def panic(e, hub=nil)
- LOG.fatal "Error raised during event loop: #{e.class}", e
+ (Thread.current[:log] || LOG).fatal(
+ "Error raised during event loop: #{e.class}",
+ e
+ )
if e.is_a?(::Exception)
(hub || Sentry).capture_exception(e, hint: { background: false })
else
@@ 370,44 374,26 @@ iq "/iq/ns:services", ns: "urn:xmpp:extdisco:2" do |iq|
self << reply
end
-command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
-
- sentry_hub = new_sentry_hub(iq, name: iq.node)
- EMPromise.resolve(nil).then {
- CustomerRepo.new.find_by_jid(iq.from.stripped)
- }.catch {
- sentry_hub.add_breadcrumb(Sentry::Breadcrumb.new(
- message: "Customer.create"
- ))
- CustomerRepo.new.create(iq.from.stripped)
+Command.new(
+ "jabber:iq:register",
+ "Register",
+ list_for: ->(*) { true }
+) {
+ Command.customer.catch {
+ Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Customer.create"))
+ Command.execution.customer_repo.create(Command.execution.iq.from.stripped)
}.then { |customer|
- sentry_hub.current_scope.set_user(
- id: customer.customer_id,
- jid: iq.from.stripped.to_s
- )
- sentry_hub.add_breadcrumb(Sentry::Breadcrumb.new(
- message: "Registration.for"
- ))
+ Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Registration.for"))
+ # TODO: Have to change all of lib/registration to use Command.reply and Command.finish
+ # Can also use Command.log and Sentry.* wherever you like
Registration.for(
- iq,
customer,
web_register_manager
- ).then(&:write).then { StatsD.increment("registration.completed") }
- }.catch_only(ErrorToSend) { |e|
- self << e.stanza
- }.catch { |e| panic(e, sentry_hub) }
-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
+ ).then(&:write)
+ }.then { StatsD.increment("registration.completed") }
+}.register(self, CommandList)
+# TODO: one Command each or a allow Command to have multiple nodes or just leave it?
# Commands that just pass through to the SGX
command node: [
"number-display",
@@ 427,117 413,90 @@ command node: [
}.catch { |e| panic(e, sentry_hub) }
end
-command :execute?, node: "credit cards", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
-
- sentry_hub = new_sentry_hub(iq, name: iq.node)
- reply = iq.reply
- reply.status = :completed
-
- CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
- oob = OOB.find_or_create(reply.command)
- oob.url = CONFIG[:credit_card_url].call(
+Command.new(
+ "credit cards",
+ "Credit Card Settings and Management"
+) {
+ Command.customer.then do |customer|
+ url = CONFIG[:credit_card_url].call(
reply.to.stripped.to_s.gsub("\\", "%5C"),
customer.customer_id
)
- oob.desc = "Manage credits cards and settings"
-
- reply.note_type = :info
- reply.note_text = "#{oob.desc}: #{oob.url}"
-
- self << reply
- }.catch { |e| panic(e, sentry_hub) }
-end
-
-command :execute?, node: "top up", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
-
- sentry_hub = new_sentry_hub(iq, name: iq.node)
- reply = iq.reply
- reply.allowed_actions = [:complete]
-
- CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
+ desc = "Manage credits cards and settings"
+ Command.finish("#{desc}: #{url}") do |reply|
+ oob = OOB.find_or_create(reply.command)
+ oob.url = url
+ oob.desc = desc
+ end
+ end
+}.register(self, CommandList)
+
+Command.new(
+ "top up",
+ "Buy Account Credit by Credit Card",
+ list_for: ->(payment_methods:, **) { !payment_methods.empty? },
+ format_error: ->(e) { "Failed to buy credit, system said: #{e.message}" }
+) {
+ Command.customer.then { |customer|
BuyAccountCreditForm.for(customer).then do |credit_form|
- credit_form.add_to_form(reply.form)
- COMMAND_MANAGER.write(reply).then { |iq2| [customer, credit_form, iq2] }
+ Command.reply { |reply|
+ reply.allowed_actions = [:complete]
+ credit_form.add_to_form(reply.form)
+ }.then do |iq|
+ Transaction.sale(customer, **credit_form.parse(iq.form))
+ end
end
- }.then { |(customer, credit_form, iq2)|
- iq = iq2 # This allows the catch to use it also
- Transaction.sale(customer, **credit_form.parse(iq2.form))
}.then { |transaction|
transaction.insert.then do
- reply_with_note(iq, "#{transaction} added to your account balance.")
+ Command.finish("#{transaction} added to your account balance.")
end
- }.catch_only(BuyAccountCreditForm::AmountValidationError) { |e|
- reply_with_note(iq, e.message, type: :error)
- }.catch { |e|
- sentry_hub.capture_exception(e)
- text = "Failed to buy credit, system said: #{e.message}"
- reply_with_note(iq, text, type: :error)
- }.catch { |e| panic(e, sentry_hub) }
-end
-
-command :execute?, node: "alt top up", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
-
- sentry_hub = new_sentry_hub(iq, name: iq.node)
- reply = iq.reply
- reply.status = :executing
- reply.allowed_actions = [:complete]
-
- CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
- sentry_hub.current_scope.set_user(
- id: customer.customer_id,
- jid: iq.from.stripped.to_s
- )
-
+ }.catch_only(BuyAccountCreditForm::AmountValidationError) do |e|
+ Command.finish(e.message, type: :error)
+ end
+}.register(self, CommandList)
+
+Command.new(
+ "alt top up",
+ "Buy Account Credit by Bitcoin, Mail, or Interac eTransfer",
+ list_for: ->(customer:, **) { !!customer.currency }
+) {
+ Command.customer.then { |customer|
EMPromise.all([AltTopUpForm.for(customer), customer])
- }.then { |(alt_form, customer)|
- reply.command << alt_form.form
-
- COMMAND_MANAGER.write(reply).then do |iq2|
- AddBitcoinAddress.for(iq2, alt_form, customer).write
+ }.then do |(alt_form, customer)|
+ Command.reply { |reply|
+ reply.allowed_actions = [:complete]
+ reply.command << alt_form.form
+ }.then do |iq|
+ AddBitcoinAddress.for(iq, alt_form, customer).write
end
- }.catch { |e| panic(e, sentry_hub) }
-end
-
-command :execute?, node: "reset sip account", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
-
- sentry_hub = new_sentry_hub(iq, name: iq.node)
- CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
- sentry_hub.current_scope.set_user(
- id: customer.customer_id,
- jid: iq.from.stripped.to_s
- )
- customer.reset_sip_account
- }.then { |sip_account|
- reply = iq.reply
- reply.command << sip_account.form
- BLATHER << reply
- }.catch { |e| panic(e, sentry_hub) }
-end
-
-command :execute?, node: "usage", sessionid: nil do |iq|
- StatsD.increment("command", tags: ["node:#{iq.node}"])
+ end
+}.register(self, CommandList)
+
+Command.new(
+ "reset sip account",
+ "Create or Reset SIP Account"
+) {
+ Command.customer.then(&:reset_sip_account).then do |sip_account|
+ Command.finish do |reply|
+ reply.command << sip_account.form
+ end
+ end
+}.register(self, CommandList)
- sentry_hub = new_sentry_hub(iq, name: iq.node)
+Command.new(
+ "usage",
+ "Show Monthly Usage"
+) {
report_for = (Date.today..(Date.today << 1))
- CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
- sentry_hub.current_scope.set_user(
- id: customer.customer_id,
- jid: iq.from.stripped.to_s
- )
-
+ Command.customer.then { |customer|
customer.usage_report(report_for)
- }.then { |usage_report|
- reply = iq.reply
- reply.status = :completed
- reply.command << usage_report.form
- BLATHER << reply
- }.catch { |e| panic(e, sentry_hub) }
-end
+ }.then do |usage_report|
+ Command.finish do |reply|
+ reply.command << usage_report.form
+ end
+ end
+}.register(self, CommandList)
command :execute?, node: "web-register", sessionid: nil do |iq|
StatsD.increment("command", tags: ["node:#{iq.node}"])