Add support for Basecamp, Campfire & Slack webhooks

This commit is contained in:
Stanko K.R.
2025-09-12 15:32:00 +02:00
parent 4879995013
commit 2fc9215b1d
5 changed files with 186 additions and 2 deletions
+16 -1
View File
@@ -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)
+32 -1
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
<%= event_action_sentence(@event) %>
<% if @event.eventable %>
<%= link_to "See more", @event.eventable %>
<% end %>
+83
View File
@@ -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
+51
View File
@@ -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