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		attr_reader :customer_repo, :log, :iq
 31
 32		def initialize(customer_repo, blather, format_error, iq)
 33			@customer_repo = customer_repo
 34			@blather = blather
 35			@format_error = format_error
 36			@iq = iq
 37			@log = LOG.child(node: iq.node)
 38		end
 39
 40		def execute
 41			StatsD.increment("command", tags: ["node:#{iq.node}"])
 42			EMPromise.resolve(nil).then {
 43				Thread.current[:execution] = self
 44				sentry_hub
 45				catch_after(yield self)
 46			}.catch(&method(:panic))
 47		end
 48
 49		def reply(stanza=nil)
 50			stanza ||= iq.reply.tap do |reply|
 51				reply.status = :executing
 52			end
 53			yield stanza if block_given?
 54			COMMAND_MANAGER.write(stanza).then do |new_iq|
 55				@iq = new_iq
 56			end
 57		end
 58
 59		def finish(text=nil, type: :info, status: :completed)
 60			reply = @iq.reply
 61			reply.status = status
 62			yield reply if block_given?
 63			if text
 64				reply.note_type = type
 65				reply.note_text = text
 66			end
 67			raise ErrorToSend, reply
 68		end
 69
 70		def sentry_hub
 71			return @sentry_hub if @sentry_hub
 72
 73			# Stored on Fiber-local in 4.3.1 and earlier
 74			# https://github.com/getsentry/sentry-ruby/issues/1495
 75			@sentry_hub = Sentry.get_current_hub
 76			raise "Sentry.init has not been called" unless @sentry_hub
 77
 78			@sentry_hub.push_scope
 79			@sentry_hub.current_scope.clear_breadcrumbs
 80			@sentry_hub.current_scope.set_transaction_name(@iq.node)
 81			@sentry_hub.current_scope.set_user(jid: @iq.from.stripped.to_s)
 82			@sentry_hub
 83		end
 84
 85		def customer
 86			@customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then do |c|
 87				sentry_hub.current_scope.set_user(
 88					id: c.customer_id,
 89					jid: @iq.from.stripped
 90				)
 91				c
 92			end
 93		end
 94
 95	protected
 96
 97		def catch_after(promise)
 98			promise.catch_only(ErrorToSend) { |e|
 99				@blather << e.stanza
100			}.catch do |e|
101				log_error(e)
102				finish(@format_error.call(e), type: :error)
103			end
104		end
105
106		def log_error(e)
107			@log.error(
108				"Error raised during #{iq.node}: #{e.class}",
109				e
110			)
111			if e.is_a?(::Exception)
112				sentry_hub.capture_exception(e)
113			else
114				sentry_hub.capture_message(e.to_s)
115			end
116		end
117	end
118
119	attr_reader :node, :name
120
121	def initialize(
122		node,
123		name,
124		list_for: ->(tel:, **) { !!tel },
125		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
126		&blk
127	)
128		@node = node
129		@name = name
130		@list_for = list_for
131		@format_error = format_error
132		@blk = blk
133	end
134
135	def register(blather)
136		blather.command(:execute?, node: @node, sessionid: nil) do |iq|
137			customer_repo = CustomerRepo.new
138			Execution.new(customer_repo, blather, @format_error, iq).execute(&@blk)
139		end
140		self
141	end
142
143	def list_for?(**kwargs)
144		@list_for.call(**kwargs)
145	end
146end