FEATURE: add outgoing web hooks for Chat messages
This commit is contained in:
parent
6c20d8cc8c
commit
e03dd76dc6
|
@ -16,6 +16,7 @@ class WebHookEventType < ActiveRecord::Base
|
||||||
LIKE = 15
|
LIKE = 15
|
||||||
USER_PROMOTED = 16
|
USER_PROMOTED = 16
|
||||||
TOPIC_VOTING = 17
|
TOPIC_VOTING = 17
|
||||||
|
CHAT_MESSAGE = 18
|
||||||
|
|
||||||
has_and_belongs_to_many :web_hooks
|
has_and_belongs_to_many :web_hooks
|
||||||
|
|
||||||
|
@ -34,6 +35,9 @@ class WebHookEventType < ActiveRecord::Base
|
||||||
unless defined?(SiteSetting.voting_enabled) && SiteSetting.voting_enabled
|
unless defined?(SiteSetting.voting_enabled) && SiteSetting.voting_enabled
|
||||||
ids_to_exclude << TOPIC_VOTING
|
ids_to_exclude << TOPIC_VOTING
|
||||||
end
|
end
|
||||||
|
unless defined?(SiteSetting.chat_enabled) && SiteSetting.chat_enabled
|
||||||
|
ids_to_exclude << CHAT_MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
self.where.not(id: ids_to_exclude)
|
self.where.not(id: ids_to_exclude)
|
||||||
end
|
end
|
||||||
|
|
|
@ -74,3 +74,8 @@ WebHookEventType.seed do |b|
|
||||||
b.id = WebHookEventType::TOPIC_VOTING
|
b.id = WebHookEventType::TOPIC_VOTING
|
||||||
b.name = "topic_voting"
|
b.name = "topic_voting"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
WebHookEventType.seed do |b|
|
||||||
|
b.id = WebHookEventType::CHAT_MESSAGE
|
||||||
|
b.name = "chat_message"
|
||||||
|
end
|
||||||
|
|
|
@ -19,6 +19,10 @@ en:
|
||||||
descriptions:
|
descriptions:
|
||||||
chat:
|
chat:
|
||||||
create_message: "Create a chat message in a specified channel."
|
create_message: "Create a chat message in a specified channel."
|
||||||
|
web_hooks:
|
||||||
|
chat_message_event:
|
||||||
|
name: "Chat message event"
|
||||||
|
details: "When a chat message is created, edited, trashed or restored."
|
||||||
about:
|
about:
|
||||||
chat_messages_count: "Chat Messages"
|
chat_messages_count: "Chat Messages"
|
||||||
chat_channels_count: "Chat Channels"
|
chat_channels_count: "Chat Channels"
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
module OutgoingWebHookExtension
|
||||||
|
def self.prepended(base)
|
||||||
|
def base.enqueue_chat_message_hooks(event, payload, opts = {})
|
||||||
|
if active_web_hooks("chat_message").exists?
|
||||||
|
WebHook.enqueue_hooks(:chat_message, event, payload: payload, **opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -67,6 +67,7 @@ after_initialize do
|
||||||
Jobs::UserEmail.prepend Chat::UserEmailExtension
|
Jobs::UserEmail.prepend Chat::UserEmailExtension
|
||||||
Plugin::Instance.prepend Chat::PluginInstanceExtension
|
Plugin::Instance.prepend Chat::PluginInstanceExtension
|
||||||
Jobs::ExportCsvFile.class_eval { prepend Chat::MessagesExporter }
|
Jobs::ExportCsvFile.class_eval { prepend Chat::MessagesExporter }
|
||||||
|
WebHook.prepend Chat::OutgoingWebHookExtension
|
||||||
end
|
end
|
||||||
|
|
||||||
if Oneboxer.respond_to?(:register_local_handler)
|
if Oneboxer.respond_to?(:register_local_handler)
|
||||||
|
@ -381,6 +382,35 @@ after_initialize do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# outgoing webhook events
|
||||||
|
%i[
|
||||||
|
chat_message_created
|
||||||
|
chat_message_edited
|
||||||
|
chat_message_trashed
|
||||||
|
chat_message_restored
|
||||||
|
].each do |chat_message_event|
|
||||||
|
on(chat_message_event) do |message, channel, user|
|
||||||
|
guardian = Guardian.new(user)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
message: Chat::MessageSerializer.new(message, { scope: guardian, root: false }).as_json,
|
||||||
|
channel:
|
||||||
|
Chat::ChannelSerializer.new(
|
||||||
|
channel,
|
||||||
|
{ scope: guardian, membership: channel.membership_for(user), root: false },
|
||||||
|
).as_json,
|
||||||
|
}
|
||||||
|
|
||||||
|
category_id = channel.chatable_type == "Category" ? channel.chatable_id : nil
|
||||||
|
|
||||||
|
WebHook.enqueue_chat_message_hooks(
|
||||||
|
chat_message_event,
|
||||||
|
payload.to_json,
|
||||||
|
category_id: category_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
mount ::Chat::Engine, at: "/chat"
|
mount ::Chat::Engine, at: "/chat"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:outgoing_chat_message_web_hook, from: :web_hook) do
|
||||||
|
transient chat_message_hook: WebHookEventType.find_by(name: "chat_message")
|
||||||
|
|
||||||
|
after_build do |web_hook, transients|
|
||||||
|
web_hook.web_hook_event_types = [transients[:chat_message_hook]]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,245 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Outgoing chat webhooks" do
|
||||||
|
before do
|
||||||
|
SiteSetting.chat_enabled = true
|
||||||
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "chat messages" do
|
||||||
|
fab!(:web_hook) { Fabricate(:outgoing_chat_message_web_hook) }
|
||||||
|
fab!(:user1) { Fabricate(:user) }
|
||||||
|
fab!(:user2) { Fabricate(:user) }
|
||||||
|
let(:message_content) { "This is a test message" }
|
||||||
|
let(:new_message_content) { "This is the edited message" }
|
||||||
|
let(:job_args) do
|
||||||
|
Jobs::EmitWebHookEvent
|
||||||
|
.jobs
|
||||||
|
.map { |job| job["args"].first }
|
||||||
|
.find { |args| args["event_type"] == "chat_message" }
|
||||||
|
end
|
||||||
|
let(:event_name) { job_args["event_name"] }
|
||||||
|
let(:event_category_id) { job_args["category_id"] }
|
||||||
|
let(:payload) { JSON.parse(job_args["payload"]) }
|
||||||
|
|
||||||
|
def expect_response_to_be_successful
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_web_hook_event_name_to_be(name)
|
||||||
|
expect(event_name).to eq(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_web_hook_event_category_to_be(category)
|
||||||
|
expect(event_category_id).to eq(category.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_web_hook_payload_message_to_match(message:, user:, &block)
|
||||||
|
payload_message = payload["message"]
|
||||||
|
|
||||||
|
expect(payload_message["id"]).to eq(message.id)
|
||||||
|
expect(payload_message["message"]).to eq(message.message)
|
||||||
|
expect(payload_message["cooked"]).to eq(message.cooked)
|
||||||
|
expect(payload_message["created_at"]).to eq(message.created_at.iso8601)
|
||||||
|
expect(payload_message["excerpt"]).to eq(message.excerpt)
|
||||||
|
expect(payload_message["chat_channel_id"]).to eq(message.chat_channel_id)
|
||||||
|
expect(payload_message["mentioned_users"]).to be_empty
|
||||||
|
expect(payload_message["available_flags"]).to be_empty
|
||||||
|
expect(payload_message["user"]["id"]).to eq(user.id)
|
||||||
|
expect(payload_message["user"]["username"]).to eq(user.username)
|
||||||
|
expect(payload_message["user"]["avatar_template"]).to eq(user.avatar_template)
|
||||||
|
expect(payload_message["user"]["admin"]).to eq(user.admin?)
|
||||||
|
expect(payload_message["user"]["staff"]).to eq(user.staff?)
|
||||||
|
expect(payload_message["user"]["moderator"]).to eq(user.moderator?)
|
||||||
|
expect(payload_message["user"]["new_user"]).to eq(user.trust_level == TrustLevel[0])
|
||||||
|
expect(payload_message["user"]["primary_group_name"]).to eq(user.primary_group&.name)
|
||||||
|
expect(payload_message["uploads"]).to be_empty
|
||||||
|
|
||||||
|
yield(payload_message) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_web_hook_payload_channel_to_match_category(channel:, category:, &block)
|
||||||
|
payload_channel = payload["channel"]
|
||||||
|
|
||||||
|
expect(payload_channel["id"]).to eq(channel.id)
|
||||||
|
expect(payload_channel["allow_channel_wide_mentions"]).to eq(
|
||||||
|
channel.allow_channel_wide_mentions,
|
||||||
|
)
|
||||||
|
expect(payload_channel["chatable_id"]).to eq(category.id)
|
||||||
|
expect(payload_channel["chatable_type"]).to eq("Category")
|
||||||
|
expect(payload_channel["chatable_url"]).to eq(category.url)
|
||||||
|
expect(payload_channel["title"]).to eq(channel.title)
|
||||||
|
expect(payload_channel["slug"]).to eq(channel.slug)
|
||||||
|
|
||||||
|
yield(payload_channel) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_web_hook_payload_channel_to_match_direct_message(channel:, direct_message:, &block)
|
||||||
|
payload_channel = payload["channel"]
|
||||||
|
|
||||||
|
expect(payload_channel["id"]).to eq(channel.id)
|
||||||
|
expect(payload_channel["allow_channel_wide_mentions"]).to eq(
|
||||||
|
channel.allow_channel_wide_mentions,
|
||||||
|
)
|
||||||
|
expect(payload_channel["chatable_id"]).to eq(direct_message.id)
|
||||||
|
expect(payload_channel["chatable_type"]).to eq("DirectMessage")
|
||||||
|
expect(payload_channel["chatable_url"]).to be_nil
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["id"]).to eq(user2.id)
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["username"]).to eq(user2.username)
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["name"]).to eq(user2.name)
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["avatar_template"]).to eq(
|
||||||
|
user2.avatar_template,
|
||||||
|
)
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["can_chat"]).to eq(true)
|
||||||
|
expect(payload_channel["chatable"]["users"][0]["has_chat_enabled"]).to eq(true)
|
||||||
|
expect(payload_channel["title"]).to eq(channel.title(user1))
|
||||||
|
expect(payload_channel["slug"]).to be_nil
|
||||||
|
|
||||||
|
yield(payload_channel) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for a category channel" do
|
||||||
|
fab!(:category) { Fabricate(:category) }
|
||||||
|
fab!(:chat_channel) { Fabricate(:category_channel, chatable: category) }
|
||||||
|
fab!(:chat_message) { Fabricate(:chat_message, chat_channel: chat_channel, user: user1) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
[user1, user2].each do |user|
|
||||||
|
Chat::UserChatChannelMembership.create(
|
||||||
|
user: user,
|
||||||
|
chat_channel: chat_channel,
|
||||||
|
following: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
sign_in(user1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is created" do
|
||||||
|
post "/chat/#{chat_channel.id}.json", params: { message: message_content }
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_created")
|
||||||
|
expect_web_hook_event_category_to_be(category)
|
||||||
|
expect_web_hook_payload_message_to_match(
|
||||||
|
message: Chat::Message.last,
|
||||||
|
user: user1,
|
||||||
|
) { |payload_message| expect(payload_message["message"]).to eq(message_content) }
|
||||||
|
expect_web_hook_payload_channel_to_match_category(channel: chat_channel, category: category)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is edited" do
|
||||||
|
put "/chat/#{chat_channel.id}/edit/#{chat_message.id}.json",
|
||||||
|
params: {
|
||||||
|
new_message: new_message_content,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_edited")
|
||||||
|
expect_web_hook_event_category_to_be(category)
|
||||||
|
expect_web_hook_payload_message_to_match(
|
||||||
|
message: Chat::Message.last,
|
||||||
|
user: user1,
|
||||||
|
) { |payload_message| expect(payload_message["message"]).to eq(new_message_content) }
|
||||||
|
expect_web_hook_payload_channel_to_match_category(channel: chat_channel, category: category)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is trashed" do
|
||||||
|
delete "/chat/api/channels/#{chat_message.chat_channel_id}/messages/#{chat_message.id}.json"
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect(chat_message.reload.trashed?).to eq(true)
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_trashed")
|
||||||
|
expect_web_hook_event_category_to_be(category)
|
||||||
|
expect_web_hook_payload_message_to_match(message: chat_message, user: user1)
|
||||||
|
expect_web_hook_payload_channel_to_match_category(channel: chat_channel, category: category)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a trashed chat message is restored" do
|
||||||
|
chat_message.trash!(user1)
|
||||||
|
expect(chat_message.reload.trashed?).to eq(true)
|
||||||
|
|
||||||
|
put "/chat/api/channels/#{chat_channel.id}/messages/#{chat_message.id}/restore.json"
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect(chat_message.reload.trashed?).to eq(false)
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_restored")
|
||||||
|
expect_web_hook_event_category_to_be(category)
|
||||||
|
expect_web_hook_payload_message_to_match(message: chat_message, user: user1)
|
||||||
|
expect_web_hook_payload_channel_to_match_category(channel: chat_channel, category: category)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for a direct message channel" do
|
||||||
|
fab!(:direct_message) { Fabricate(:direct_message, users: [user1, user2]) }
|
||||||
|
fab!(:direct_message_channel) { Fabricate(:direct_message_channel, chatable: direct_message) }
|
||||||
|
fab!(:chat_message) do
|
||||||
|
Fabricate(:chat_message, chat_channel: direct_message_channel, user: user1)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { sign_in(user1) }
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is created" do
|
||||||
|
post "/chat/#{direct_message_channel.id}.json", params: { message: message_content }
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_created")
|
||||||
|
expect_web_hook_payload_message_to_match(
|
||||||
|
message: Chat::Message.last,
|
||||||
|
user: user1,
|
||||||
|
) { |payload_message| expect(payload_message["message"]).to eq(message_content) }
|
||||||
|
expect_web_hook_payload_channel_to_match_direct_message(
|
||||||
|
channel: direct_message_channel,
|
||||||
|
direct_message: direct_message,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is edited" do
|
||||||
|
put "/chat/#{direct_message_channel.id}/edit/#{chat_message.id}.json",
|
||||||
|
params: {
|
||||||
|
new_message: new_message_content,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_edited")
|
||||||
|
expect_web_hook_payload_message_to_match(
|
||||||
|
message: Chat::Message.last,
|
||||||
|
user: user1,
|
||||||
|
) { |payload_message| expect(payload_message["message"]).to eq(new_message_content) }
|
||||||
|
expect_web_hook_payload_channel_to_match_direct_message(
|
||||||
|
channel: direct_message_channel,
|
||||||
|
direct_message: direct_message,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a chat message is trashed" do
|
||||||
|
delete "/chat/api/channels/#{chat_message.chat_channel_id}/messages/#{chat_message.id}.json"
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect(chat_message.reload.trashed?).to eq(true)
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_trashed")
|
||||||
|
expect_web_hook_payload_message_to_match(message: chat_message, user: user1)
|
||||||
|
expect_web_hook_payload_channel_to_match_direct_message(
|
||||||
|
channel: direct_message_channel,
|
||||||
|
direct_message: direct_message,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a webhook when a trashed chat message is restored" do
|
||||||
|
chat_message.trash!(user1)
|
||||||
|
expect(chat_message.reload.trashed?).to eq(true)
|
||||||
|
|
||||||
|
put "/chat/api/channels/#{direct_message_channel.id}/messages/#{chat_message.id}/restore.json"
|
||||||
|
|
||||||
|
expect_response_to_be_successful
|
||||||
|
expect(chat_message.reload.trashed?).to eq(false)
|
||||||
|
expect_web_hook_event_name_to_be("chat_message_restored")
|
||||||
|
expect_web_hook_payload_message_to_match(message: chat_message, user: user1)
|
||||||
|
expect_web_hook_payload_channel_to_match_direct_message(
|
||||||
|
channel: direct_message_channel,
|
||||||
|
direct_message: direct_message,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -67,8 +67,11 @@ RSpec.describe Chat::RestoreMessage do
|
||||||
freeze_time
|
freeze_time
|
||||||
messages = nil
|
messages = nil
|
||||||
event =
|
event =
|
||||||
DiscourseEvent.track_events { messages = MessageBus.track_publish { result } }.first
|
DiscourseEvent
|
||||||
expect(event[:event_name]).to eq(:chat_message_restored)
|
.track_events { messages = MessageBus.track_publish { result } }
|
||||||
|
.find { |e| e[:event_name] == :chat_message_restored }
|
||||||
|
|
||||||
|
expect(event).to be_present
|
||||||
expect(event[:params]).to eq([message, message.chat_channel, current_user])
|
expect(event[:params]).to eq([message, message.chat_channel, current_user])
|
||||||
expect(
|
expect(
|
||||||
messages.find { |m| m.channel == "/chat/#{message.chat_channel_id}" }.data,
|
messages.find { |m| m.channel == "/chat/#{message.chat_channel_id}" }.data,
|
||||||
|
|
|
@ -62,8 +62,11 @@ RSpec.describe Chat::TrashMessage do
|
||||||
freeze_time
|
freeze_time
|
||||||
messages = nil
|
messages = nil
|
||||||
event =
|
event =
|
||||||
DiscourseEvent.track_events { messages = MessageBus.track_publish { result } }.first
|
DiscourseEvent
|
||||||
expect(event[:event_name]).to eq(:chat_message_trashed)
|
.track_events { messages = MessageBus.track_publish { result } }
|
||||||
|
.find { |e| e[:event_name] == :chat_message_trashed }
|
||||||
|
|
||||||
|
expect(event).to be_present
|
||||||
expect(event[:params]).to eq([message, message.chat_channel, current_user])
|
expect(event[:params]).to eq([message, message.chat_channel, current_user])
|
||||||
expect(messages.find { |m| m.channel == "/chat/#{message.chat_channel_id}" }.data).to eq(
|
expect(messages.find { |m| m.channel == "/chat/#{message.chat_channel_id}" }.data).to eq(
|
||||||
{
|
{
|
||||||
|
|
|
@ -64,6 +64,10 @@ RSpec.describe WebHook do
|
||||||
SiteSetting.stubs(:voting_enabled).returns(true)
|
SiteSetting.stubs(:voting_enabled).returns(true)
|
||||||
voting_event_types = WebHookEventType.active.where(name: "topic_voting")
|
voting_event_types = WebHookEventType.active.where(name: "topic_voting")
|
||||||
expect(voting_event_types.count).to eq(1)
|
expect(voting_event_types.count).to eq(1)
|
||||||
|
|
||||||
|
SiteSetting.stubs(:chat_enabled).returns(true)
|
||||||
|
chat_enabled_types = WebHookEventType.active.where("name LIKE 'chat_%'")
|
||||||
|
expect(chat_enabled_types.count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#active_web_hooks" do
|
describe "#active_web_hooks" do
|
||||||
|
|
Loading…
Reference in New Issue