2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
require "excon"
|
|
|
|
|
|
|
|
module Jobs
|
2019-10-02 00:01:53 -04:00
|
|
|
class EmitWebHookEvent < ::Jobs::Base
|
2021-08-13 01:11:02 -04:00
|
|
|
sidekiq_options queue: "low"
|
|
|
|
|
2020-04-30 02:48:34 -04:00
|
|
|
PING_EVENT = "ping"
|
|
|
|
MAX_RETRY_COUNT = 4
|
2018-05-30 07:27:40 -04:00
|
|
|
RETRY_BACKOFF = 5
|
2018-05-21 04:23:09 -04:00
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
def execute(args)
|
2019-04-19 21:39:25 -04:00
|
|
|
@arguments = args
|
|
|
|
@retry_count = args[:retry_count] || 0
|
|
|
|
@web_hook = WebHook.find_by(id: @arguments[:web_hook_id])
|
2019-04-17 05:03:23 -04:00
|
|
|
validate_arguments!
|
2016-11-07 22:43:54 -05:00
|
|
|
|
2020-04-16 16:24:09 -04:00
|
|
|
return if @web_hook.blank? # Web Hook was deleted
|
|
|
|
|
2019-04-19 21:39:25 -04:00
|
|
|
unless ping_event?(@arguments[:event_type])
|
2019-04-17 05:03:23 -04:00
|
|
|
validate_argument!(:payload)
|
2016-12-22 11:08:35 -05:00
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
return if webhook_inactive?
|
|
|
|
return if group_webhook_invalid?
|
|
|
|
return if category_webhook_invalid?
|
|
|
|
return if tag_webhook_invalid?
|
2016-11-07 22:43:54 -05:00
|
|
|
end
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
send_webhook!
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
def validate_arguments!
|
|
|
|
validate_argument!(:web_hook_id)
|
|
|
|
validate_argument!(:event_type)
|
2016-12-22 11:08:35 -05:00
|
|
|
end
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
def validate_argument!(key)
|
2024-05-27 06:27:13 -04:00
|
|
|
raise Discourse::InvalidParameters.new(key) if @arguments[key].blank?
|
2016-12-22 11:08:35 -05:00
|
|
|
end
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
def send_webhook!
|
|
|
|
web_hook_body = build_webhook_body
|
|
|
|
web_hook_event = create_webhook_event(web_hook_body)
|
2022-11-01 12:33:17 -04:00
|
|
|
uri = URI(@web_hook.payload_url.strip)
|
2024-05-17 10:33:39 -04:00
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
web_hook_headers = build_webhook_headers(uri, web_hook_body, web_hook_event)
|
2022-11-01 12:33:17 -04:00
|
|
|
|
|
|
|
emitter = WebHookEmitter.new(@web_hook, web_hook_event)
|
|
|
|
web_hook_response = emitter.emit!(headers: web_hook_headers, body: web_hook_body)
|
2018-05-30 07:27:40 -04:00
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
publish_webhook_event(web_hook_event)
|
2019-04-18 07:36:37 -04:00
|
|
|
process_webhook_response(web_hook_response)
|
|
|
|
end
|
|
|
|
|
|
|
|
def process_webhook_response(web_hook_response)
|
|
|
|
return if web_hook_response&.status.blank?
|
|
|
|
|
|
|
|
case web_hook_response.status
|
|
|
|
when 200..299
|
|
|
|
when 404, 410
|
|
|
|
if @retry_count >= MAX_RETRY_COUNT
|
2019-04-19 21:39:25 -04:00
|
|
|
@web_hook.update!(active: false)
|
|
|
|
|
|
|
|
StaffActionLogger.new(Discourse.system_user).log_web_hook_deactivate(
|
|
|
|
@web_hook,
|
|
|
|
web_hook_response.status,
|
|
|
|
)
|
2019-04-18 07:36:37 -04:00
|
|
|
end
|
|
|
|
else
|
|
|
|
retry_web_hook
|
|
|
|
end
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def retry_web_hook
|
|
|
|
if SiteSetting.retry_web_hook_events?
|
|
|
|
@retry_count += 1
|
|
|
|
return if @retry_count > MAX_RETRY_COUNT
|
|
|
|
delay = RETRY_BACKOFF**(@retry_count - 1)
|
2020-02-21 09:59:00 -05:00
|
|
|
@arguments[:retry_count] = @retry_count
|
2019-10-21 13:25:35 -04:00
|
|
|
::Jobs.enqueue_in(delay.minutes, :emit_web_hook_event, @arguments)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def publish_webhook_event(web_hook_event)
|
2019-04-19 21:39:25 -04:00
|
|
|
MessageBus.publish(
|
|
|
|
"/web_hook_events/#{@web_hook.id}",
|
2019-03-21 09:04:54 -04:00
|
|
|
{ web_hook_event_id: web_hook_event.id, event_type: @arguments[:event_type] },
|
2020-04-26 23:50:21 -04:00
|
|
|
group_ids: [Group::AUTO_GROUPS[:staff]],
|
|
|
|
)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
2019-03-21 09:04:54 -04:00
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
def ping_event?(event_type)
|
|
|
|
PING_EVENT == event_type
|
2018-05-30 07:27:40 -04:00
|
|
|
end
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
def webhook_inactive?
|
2019-04-19 21:39:25 -04:00
|
|
|
!@web_hook.active?
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def group_webhook_invalid?
|
2021-04-30 20:08:38 -04:00
|
|
|
@web_hook.group_ids.present? &&
|
|
|
|
(@arguments[:group_ids].blank? || (@web_hook.group_ids & @arguments[:group_ids]).blank?)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def category_webhook_invalid?
|
2019-04-19 21:39:25 -04:00
|
|
|
@web_hook.category_ids.present? &&
|
|
|
|
(
|
|
|
|
!@arguments[:category_id].present? ||
|
|
|
|
!@web_hook.category_ids.include?(@arguments[:category_id])
|
2023-01-09 07:20:10 -05:00
|
|
|
)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def tag_webhook_invalid?
|
2019-04-19 21:39:25 -04:00
|
|
|
@web_hook.tag_ids.present? &&
|
|
|
|
(@arguments[:tag_ids].blank? || (@web_hook.tag_ids & @arguments[:tag_ids]).blank?)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def build_webhook_headers(uri, web_hook_body, web_hook_event)
|
|
|
|
content_type =
|
2019-04-19 21:39:25 -04:00
|
|
|
case @web_hook.content_type
|
2019-04-17 05:03:23 -04:00
|
|
|
when WebHook.content_types["application/x-www-form-urlencoded"]
|
|
|
|
"application/x-www-form-urlencoded"
|
|
|
|
else
|
|
|
|
"application/json"
|
|
|
|
end
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
"Accept" => "*/*",
|
|
|
|
"Connection" => "close",
|
2022-11-01 12:33:17 -04:00
|
|
|
"Content-Length" => web_hook_body.bytesize.to_s,
|
2019-04-17 05:03:23 -04:00
|
|
|
"Content-Type" => content_type,
|
|
|
|
"Host" => uri.host,
|
|
|
|
"User-Agent" => "Discourse/#{Discourse::VERSION::STRING}",
|
|
|
|
"X-Discourse-Instance" => Discourse.base_url,
|
2022-11-01 12:33:17 -04:00
|
|
|
"X-Discourse-Event-Id" => web_hook_event.id.to_s,
|
2019-04-19 21:39:25 -04:00
|
|
|
"X-Discourse-Event-Type" => @arguments[:event_type],
|
2019-04-17 05:03:23 -04:00
|
|
|
}
|
|
|
|
|
2019-04-19 21:39:25 -04:00
|
|
|
headers["X-Discourse-Event"] = @arguments[:event_name] if @arguments[:event_name].present?
|
2019-04-17 05:03:23 -04:00
|
|
|
|
2019-04-19 21:39:25 -04:00
|
|
|
if @web_hook.secret.present?
|
|
|
|
headers[
|
|
|
|
"X-Discourse-Event-Signature"
|
|
|
|
] = "sha256=#{OpenSSL::HMAC.hexdigest("sha256", @web_hook.secret, web_hook_body)}"
|
2018-05-30 07:27:40 -04:00
|
|
|
end
|
2019-04-17 05:03:23 -04:00
|
|
|
|
|
|
|
headers
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
2019-04-17 05:03:23 -04:00
|
|
|
|
|
|
|
def build_webhook_body
|
|
|
|
body = {}
|
|
|
|
|
2019-04-19 21:39:25 -04:00
|
|
|
if ping_event?(@arguments[:event_type])
|
2019-04-17 05:03:23 -04:00
|
|
|
body["ping"] = "OK"
|
|
|
|
else
|
2019-04-19 21:39:25 -04:00
|
|
|
body[@arguments[:event_type]] = JSON.parse(@arguments[:payload])
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
new_body = Plugin::Filter.apply(:after_build_web_hook_body, self, body)
|
|
|
|
MultiJson.dump(new_body)
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_webhook_event(web_hook_body)
|
2019-04-19 21:39:25 -04:00
|
|
|
WebHookEvent.create!(web_hook: @web_hook, payload: web_hook_body)
|
2019-04-17 05:03:23 -04:00
|
|
|
end
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
end
|