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(FinalStanza) { |e|
107				@blather << e.stanza
108			}.catch do |e|
109				log_error(e)
110				finish(
111					@format_error.call(e), type: :error
112				).catch_only(FinalStanza) do |to_send|
113					@blather << to_send.stanza
114				end
115			end
116		end
117
118		def log_error(e)
119			@log.error(
120				"Error raised during #{iq.node}: #{e.class}",
121				e
122			)
123			if e.is_a?(::Exception)
124				sentry_hub.capture_exception(e)
125			else
126				sentry_hub.capture_message(e.to_s)
127			end
128		end
129	end
130
131	attr_reader :node, :name
132
133	def initialize(
134		node,
135		name,
136		list_for: ->(tel:, **) { !!tel },
137		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
138		&blk
139	)
140		@node = node
141		@name = name
142		@list_for = list_for
143		@format_error = format_error
144		@blk = blk
145	end
146
147	def register(blather)
148		blather.command(:execute?, node: @node, sessionid: nil) do |iq|
149			customer_repo = CustomerRepo.new
150			Execution.new(customer_repo, blather, @format_error, iq).execute(&@blk)
151		end
152		self
153	end
154
155	def list_for?(**kwargs)
156		@list_for.call(**kwargs)
157	end
158end