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