diff --git a/app/models/webhook.rb b/app/models/webhook.rb index fc905d1c2..647c14600 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,10 +1,13 @@ class Webhook < ApplicationRecord include Triggerable + SLACK_WEBHOOK_URL_REGEX = %r{//hooks\.slack\.com/services/T[^\/]+/B[^\/]+/[^\/]+\Z}i + CAMPFIRE_WEBHOOK_URL_REGEX = %r{/rooms/\d+/\d+-[^\/]+/messages\Z}i + BASECAMP_CAMPFIRE_WEBHOOK_URL_REGEX = %r{/\d+/integrations/[^\/]+/buckets/\d+/chats/\d+/lines\Z}i + PERMITTED_SCHEMES = %w[ http https ].freeze PERMITTED_ACTIONS = %w[ card_assigned - card_boosted card_closed card_collection_changed card_created @@ -48,6 +51,18 @@ class Webhook < ApplicationRecord @renderer ||= ApplicationController.renderer.new end + def for_basecamp? + url.match? BASECAMP_CAMPFIRE_WEBHOOK_URL_REGEX + end + + def for_campfire? + url.match? CAMPFIRE_WEBHOOK_URL_REGEX + end + + def for_slack? + url.match? SLACK_WEBHOOK_URL_REGEX + end + private def validate_url uri = URI.parse(url.presence) diff --git a/app/models/webhook/delivery.rb b/app/models/webhook/delivery.rb index 4cd926107..db37e20f2 100644 --- a/app/models/webhook/delivery.rb +++ b/app/models/webhook/delivery.rb @@ -106,6 +106,37 @@ class Webhook::Delivery < ApplicationRecord end def payload - webhook.renderer.render(template: "webhooks/event", assigns: { event: event }, format: :json) + @payload ||= if webhook.for_basecamp? + { line: { content: render_payload(formats: :html) } }.to_json + elsif webhook.for_campfire? + render_payload(formats: :html) + elsif webhook.for_slack? + html = render_payload(formats: :html) + { text: convert_html_to_mrkdwn(html) }.to_json + else + render_payload(formats: :json) + end + end + + def render_payload(**options) + webhook.renderer.render(layout: false, template: "webhooks/event", assigns: { event: event }, **options).strip + end + + def convert_html_to_mrkdwn(html) + document = Nokogiri::HTML5(html) + + document.css("a").each do |a| + a.replace("<#{a["href"].strip}|#{a.text}>") if a["href"].present? + end + + document.css("b").each do |b| + b.replace("*#{b.text}*") + end + + document.css("i").each do |i| + i.replace("_#{i.text}_") + end + + document.text end end diff --git a/app/views/webhooks/event.html.erb b/app/views/webhooks/event.html.erb new file mode 100644 index 000000000..038c5971d --- /dev/null +++ b/app/views/webhooks/event.html.erb @@ -0,0 +1,4 @@ +<%= event_action_sentence(@event) %> +<% if @event.eventable %> + <%= link_to "See more", @event.eventable %> +<% end %> diff --git a/test/models/webhook/delivery_test.rb b/test/models/webhook/delivery_test.rb index d0fdaeae9..a45c89059 100644 --- a/test/models/webhook/delivery_test.rb +++ b/test/models/webhook/delivery_test.rb @@ -108,4 +108,87 @@ class Webhook::DeliveryTest < ActiveSupport::TestCase assert_equal "errored", delivery.state end + + test "deliver with basecamp webhook format" do + webhook = Webhook.create!( + name: "Basecamp", + url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/789/lines" + ) + event = events(:layout_commented) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + request_stub = stub_request(:post, webhook.url) + .with do |request| + body = JSON.parse(request.body) + body.key?("line") && body["line"].key?("content") && body["line"]["content"].present? + end + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + assert delivery.succeeded? + end + + test "deliver with campfire webhook format" do + webhook = Webhook.create!( + name: "Campfire", + url: "https://example.com/rooms/123/456-room-name/messages" + ) + event = events(:layout_commented) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + request_stub = stub_request(:post, webhook.url) + .with do |request| + request.body.is_a?(String) && !request.body.start_with?("{") && request.body.present? + end + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + assert delivery.succeeded? + end + + test "deliver with slack webhook format" do + webhook = Webhook.create!( + name: "Slack", + url: "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" + ) + event = events(:layout_commented) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + request_stub = stub_request(:post, webhook.url) + .with do |request| + body = JSON.parse(request.body) + body.key?("text") && body["text"].present? + end + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + assert delivery.succeeded? + end + + test "deliver with generic webhook format" do + webhook = Webhook.create!( + name: "Generic", + url: "https://example.com/webhook" + ) + event = events(:layout_commented) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + request_stub = stub_request(:post, webhook.url) + .with do |request| + body = JSON.parse(request.body) + body.present? && !body.key?("line") && !body.key?("text") + end + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + assert delivery.succeeded? + end end diff --git a/test/models/webhook_test.rb b/test/models/webhook_test.rb index 096b2223a..ac2ddc748 100644 --- a/test/models/webhook_test.rb +++ b/test/models/webhook_test.rb @@ -52,4 +52,55 @@ class WebhookTest < ActiveSupport::TestCase webhook.activate end end + + test "for_slack?" do + webhook = Webhook.new name: "Test", url: "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" + assert webhook.for_slack? + + webhook = Webhook.new name: "Test", url: "https://hooks.slack.com/services/T12345678/B12345678" + assert_not webhook.for_slack? + + webhook = Webhook.new name: "Test", url: "https://hooks.slack.com/services/T12345678" + assert_not webhook.for_slack? + + webhook = Webhook.new name: "Test", url: "https://hooks.slack.com/services/" + assert_not webhook.for_slack? + + webhook = Webhook.new name: "Test", url: "https://example.com/webhook" + assert_not webhook.for_slack? + end + + test "for_campfire?" do + webhook = Webhook.new name: "Test", url: "https://example.com/rooms/123/456-room-name/messages" + assert webhook.for_campfire? + + webhook = Webhook.new name: "Test", url: "https://campfire.example.com/rooms/999/123-test-room/messages" + assert webhook.for_campfire? + + webhook = Webhook.new name: "Test", url: "https://campfire.example.com/rooms/999/123/messages" + assert_not webhook.for_campfire?, "The bot key is missing a token" + + webhook = Webhook.new name: "Test", url: "https://example.com/webhook" + assert_not webhook.for_campfire? + + webhook = Webhook.new name: "Test", url: "https://example.com/rooms/123/messages" + assert_not webhook.for_campfire? + + webhook = Webhook.new name: "Test", url: "https://example.com/rooms/123/456-room-name/" + assert_not webhook.for_campfire? + end + + test "for_basecamp?" do + webhook = Webhook.new name: "Test", url: "https://basecamp.com/999/integrations/some-token/buckets/111/chats/222/lines" + assert webhook.for_basecamp? + + webhook = Webhook.new name: "Test", url: "https://example.com/webhook" + assert_not webhook.for_basecamp? + + webhook = Webhook.new name: "Test", url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/" + assert_not webhook.for_basecamp? + + webhook = Webhook.new name: "Test", url: "https://3.basecamp.com/integrations/webhook/buckets/456/chats/789/lines" + assert_not webhook.for_basecamp? + end end