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