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