command.rb

  1# frozen_string_literal: true
  2
  3require "sentry-ruby"
  4require "statsd-instrument"
  5
  6require_relative "customer_repo"
  7
  8class Command
  9	def self.execution
 10		Thread.current[:execution]
 11	end
 12
 13	def self.reply(stanza=nil, &blk)
 14		execution.reply(stanza, &blk)
 15	end
 16
 17	def self.finish(*args, **kwargs, &blk)
 18		execution.finish(*args, **kwargs, &blk)
 19	end
 20
 21	def self.customer
 22		execution.customer
 23	end
 24
 25	def self.log
 26		execution.log
 27	end
 28
 29	class Execution
 30		class FinalStanza
 31			attr_reader :stanza
 32
 33			def initialize(stanza)
 34				@stanza = stanza
 35			end
 36		end
 37
 38		attr_reader :customer_repo, :log, :iq
 39
 40		def initialize(customer_repo, blather, format_error, iq)
 41			@customer_repo = customer_repo
 42			@blather = blather
 43			@format_error = format_error
 44			@iq = iq
 45			@log = LOG.child(node: iq.node)
 46		end
 47
 48		def execute
 49			StatsD.increment("command", tags: ["node:#{iq.node}"])
 50			EMPromise.resolve(nil).then {
 51				Thread.current[:execution] = self
 52				sentry_hub
 53				catch_after(yield self)
 54			}.catch(&method(:panic))
 55		end
 56
 57		def reply(stanza=nil)
 58			stanza ||= iq.reply.tap do |reply|
 59				reply.status = :executing
 60			end
 61			yield stanza if block_given?
 62			COMMAND_MANAGER.write(stanza).then do |new_iq|
 63				@iq = new_iq
 64			end
 65		end
 66
 67		def finish(text=nil, type: :info, status: :completed)
 68			reply = @iq.reply
 69			reply.status = status
 70			yield reply if block_given?
 71			if text
 72				reply.note_type = type
 73				reply.note_text = text
 74			end
 75			EMPromise.reject(FinalStanza.new(reply))
 76		end
 77
 78		def sentry_hub
 79			return @sentry_hub if @sentry_hub
 80
 81			# Stored on Fiber-local in 4.3.1 and earlier
 82			# https://github.com/getsentry/sentry-ruby/issues/1495
 83			@sentry_hub = Sentry.get_current_hub
 84			raise "Sentry.init has not been called" unless @sentry_hub
 85
 86			@sentry_hub.push_scope
 87			@sentry_hub.current_scope.clear_breadcrumbs
 88			@sentry_hub.current_scope.set_transaction_name(@iq.node)
 89			@sentry_hub.current_scope.set_user(jid: @iq.from.stripped.to_s)
 90			@sentry_hub
 91		end
 92
 93		def customer
 94			@customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then do |c|
 95				sentry_hub.current_scope.set_user(
 96					id: c.customer_id,
 97					jid: @iq.from.stripped
 98				)
 99				c
100			end
101		end
102
103	protected
104
105		def catch_after(promise)
106			promise.catch_only(Blather::Stanza::Iq::Command) { |iq|
107				next EMPromise.reject(iq) unless iq.cancel?
108
109				finish(status: :canceled)
110			}.catch_only(FinalStanza) { |e|
111				@blather << e.stanza
112			}.catch do |e|
113				log_error(e)
114				send_final_error(e)
115			end
116		end
117
118		def send_final_error(e)
119			finish(@format_error.call(e), type: :error).catch_only(FinalStanza) do |s|
120				@blather << s.stanza
121			end
122		end
123
124		def log_error(e)
125			@log.error("Error raised during #{iq.node}: #{e.class}", e)
126			if e.is_a?(::Exception)
127				sentry_hub.capture_exception(e)
128			else
129				sentry_hub.capture_message(e.to_s)
130			end
131		end
132	end
133
134	attr_reader :node, :name
135
136	def initialize(
137		node,
138		name,
139		list_for: ->(tel:, **) { !!tel },
140		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
141		&blk
142	)
143		@node = node
144		@name = name
145		@list_for = list_for
146		@format_error = format_error
147		@blk = blk
148	end
149
150	def register(blather)
151		blather.command(:execute?, node: @node, sessionid: nil) do |iq|
152			customer_repo = CustomerRepo.new
153			Execution.new(customer_repo, blather, @format_error, iq).execute(&@blk)
154		end
155		self
156	end
157
158	def list_for?(**kwargs)
159		@list_for.call(**kwargs)
160	end
161end