Feature: add chat integration reference post (#216)

* FEATURE: Add chat integration reference post

This class works similar to a post but it is not a post.

* DEV: change how `excerpt` method works

* feature: Add `send_chat_integration_message` scriptable

add `send_chat_integration_message` scriptable that uses the rules to send a message to the chat provider
add locale strings for the new scriptable
update `ChatIntegrationReferencePost` `excerpt` method
add tests for `ChatIntegrationReferencePost`

* DEV: Add `get_channel_by_name` to every provider

This makes using `trigger_notification` easier with every provider as well.

* DEV: Add `get_channel_name` to all providers

This method gets the name of the channel based on how the provider identifies it.
Updates channel_name in locales yaml
Adds migrate_tag_added_filter_to_all_providers.rb to move all existing rules to use Automation

* DEV: Add removal of old migration data

Update small action locales with strings from core

* DEV: solve review comments

* DEV: update test locale strings

* DEV: remove empty line to trigger lint

* DEV: lint applied

* DEV: Add tests for automation integration

* DEV: add rails logger for when automatio error occurs

* DEV: move migration to be SQL only

Update provider helper to use hashes instead of dot notation

* DEV: update migration with correct table names

* DEV: Update migrate_tag_added_filter_to_all_providers to use smaller SQL queries

Commented out migrate_tag_added_from_filter_to_automation.rb

* DEV: update comments in migration file

* DEV: update indentation in client.en.yml

* DEV: update with review comments

* Update spec/lib/discourse_chat_integration/chat_integration_reference_post_spec.rb

Co-authored-by: Jarek Radosz <jradosz@gmail.com>

* Update spec/lib/discourse_chat_integration/chat_integration_reference_post_spec.rb

Co-authored-by: Jarek Radosz <jradosz@gmail.com>

* Update spec/lib/discourse_chat_integration/chat_integration_reference_post_spec.rb

Co-authored-by: Jarek Radosz <jradosz@gmail.com>

* Update spec/integration/automation_spec.rb

Co-authored-by: Jarek Radosz <jradosz@gmail.com>

* Update lib/discourse_chat_integration/chat_integration_reference_post.rb

Co-authored-by: Jarek Radosz <jradosz@gmail.com>

* DEV: update specs with review comments

* DEV: update typos in tests

* DEV: inlined functions for getting channel name for provider in migration

---------

Co-authored-by: Jarek Radosz <jradosz@gmail.com>
This commit is contained in:
Gabriel Grubba 2024-09-11 10:42:52 -03:00 committed by GitHub
parent b36ddedb1a
commit 3f8b67d1c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 837 additions and 76 deletions

View File

@ -281,3 +281,12 @@ en:
label: URL label: URL
channel: channel:
label: Channel label: Channel
send_chat_integration_message:
title: Send Chat-Integration message
fields:
channel_name:
label: Channel name
description: "You can find the channel name in the Chat Integration settings"
provider:
label: Provider

View File

@ -125,6 +125,8 @@ en:
scriptables: scriptables:
send_slack_message: send_slack_message:
title: Send Slack message title: Send Slack message
send_chat_integration_message:
title: Send Chat-Integration message
chat_integration: chat_integration:
all_categories: "(all categories)" all_categories: "(all categories)"
@ -134,6 +136,12 @@ en:
group_mention_template: "mentions of: @%{name}" group_mention_template: "mentions of: @%{name}"
group_message_template: "messages to: @%{name}" group_message_template: "messages to: @%{name}"
topic_tag_changed:
added_and_removed: "Added %{added} and removed %{removed}"
added: "Added %{added}"
removed: "Removed %{removed}"
provider: provider:
####################################### #######################################

View File

@ -1,92 +1,96 @@
# frozen_string_literal: true # frozen_string_literal: true
# The next migration file is a migration that migrates the tag_added filter to an automation.
# this one uses ActiveRecord which is not recommended for migrations.
class MigrateTagAddedFromFilterToAutomation < ActiveRecord::Migration[7.1] class MigrateTagAddedFromFilterToAutomation < ActiveRecord::Migration[7.1]
def up def up
if defined?(DiscourseAutomation) && # if defined?(DiscourseAutomation) &&
DiscourseChatIntegration::Channel.with_provider("slack").exists? # DiscourseChatIntegration::Channel.with_provider("slack").exists?
begin # begin
DiscourseChatIntegration::Rule # DiscourseChatIntegration::Rule
.where("value::json->>'filter'=?", "tag_added") # .where("value::json->>'filter'=?", "tag_added")
.each do |rule| # .each do |rule|
channel_id = rule.value["channel_id"] # channel_id = rule.value["channel_id"]
channel_name = # channel_name =
DiscourseChatIntegration::Channel.find(channel_id).value["data"]["identifier"] # it _must_ have a channel_id # DiscourseChatIntegration::Channel.find(channel_id).value["data"]["identifier"] # it _must_ have a channel_id
category_id = rule.value["category_id"] # category_id = rule.value["category_id"]
tags = rule.value["tags"] # tags = rule.value["tags"]
automation = # automation =
DiscourseAutomation::Automation.new( # DiscourseAutomation::Automation.new(
script: "send_slack_message", # script: "send_slack_message",
trigger: "topic_tags_changed", # trigger: "topic_tags_changed",
name: "When tags change in topic", # name: "When tags change in topic",
enabled: true, # enabled: true,
last_updated_by_id: Discourse.system_user.id, # last_updated_by_id: Discourse.system_user.id,
) # )
automation.save! # automation.save!
# Triggers: # # Triggers:
# Watching categories # # Watching categories
metadata = (category_id ? { "value" => [category_id] } : {}) # metadata = (category_id ? { "value" => [category_id] } : {})
automation.upsert_field!( # automation.upsert_field!(
"watching_categories", # "watching_categories",
"categories", # "categories",
metadata, # metadata,
target: "trigger", # target: "trigger",
) # )
# Watching tags # # Watching tags
metadata = (tags ? { "value" => tags } : {}) # metadata = (tags ? { "value" => tags } : {})
automation.upsert_field!("watching_tags", "tags", metadata, target: "trigger") # automation.upsert_field!("watching_tags", "tags", metadata, target: "trigger")
# Script options: # # Script options:
# Message # # Message
automation.upsert_field!( # automation.upsert_field!(
"message", # "message",
"message", # "message",
{ "value" => "${ADDED_AND_REMOVED}" }, # { "value" => "${ADDED_AND_REMOVED}" },
target: "script", # target: "script",
) # )
# URL # # URL
automation.upsert_field!( # automation.upsert_field!(
"url", # "url",
"text", # "text",
{ "value" => Discourse.current_hostname }, # { "value" => Discourse.current_hostname },
target: "script", # target: "script",
) # )
# Channel # # Channel
automation.upsert_field!( # automation.upsert_field!(
"channel", # "channel",
"text", # "text",
{ "value" => channel_name }, # { "value" => channel_name },
target: "script", # target: "script",
) # )
end # end
rescue StandardError # rescue StandardError
Rails.logger.warn("Failed to migrate tag_added rules to automations") # Rails.logger.warn("Failed to migrate tag_added rules to automations")
end # end
end # end
end end
def down def down
if defined?(DiscourseAutomation) && # if defined?(DiscourseAutomation) &&
DiscourseChatIntegration::Channel.with_provider("slack").exists? # DiscourseChatIntegration::Channel.with_provider("slack").exists?
DiscourseAutomation::Automation # DiscourseAutomation::Automation
.where(script: "send_slack_message", trigger: "topic_tags_changed") # .where(script: "send_slack_message", trigger: "topic_tags_changed")
.each do |automation| # .each do |automation|
# if is the same name as created and message is the same # # if is the same name as created and message is the same
if automation.name == "When tags change in topic" && # if automation.name == "When tags change in topic" &&
automation.fields.where(name: "message").first.metadata["value"] == # automation.fields.where(name: "message").first.metadata["value"] ==
"${ADDED_AND_REMOVED}" # "${ADDED_AND_REMOVED}"
automation.destroy! # automation.destroy!
end # end
end # end
end # end
end end
end end

View File

@ -0,0 +1,153 @@
# frozen_string_literal: true
class MigrateTagAddedFilterToAllProviders < ActiveRecord::Migration[7.1]
def up
if defined?(DiscourseAutomation)
begin
slack_usage_rows = DB.query <<~SQL
SELECT plugin_store_rows.* FROM plugin_store_rows
WHERE plugin_store_rows.type_name = 'JSON'
AND plugin_store_rows.plugin_name = 'discourse-chat-integration'
AND (key LIKE 'channel:%')
AND (value::json->>'provider'='slack')
SQL
old_migration_delete = <<~SQL
DELETE FROM discourse_automation_automations
WHERE id IN (
SELECT a.id
FROM discourse_automation_automations a
JOIN discourse_automation_fields f ON f.automation_id = a.id
WHERE a.script = 'send_slack_message'
AND a.trigger = 'topic_tags_changed'
AND a.name = 'When tags change in topic'
AND f.name = 'message'
AND f.metadata->>'value' = '${ADDED_AND_REMOVED}'
)
SQL
# Trash old migration
DB.exec old_migration_delete if slack_usage_rows.count > 0
rules_with_tag_added = <<~SQL
SELECT value
FROM plugin_store_rows
WHERE plugin_name = 'discourse-chat-integration'
AND key LIKE 'rule:%'
AND value::json->>'filter' = 'tag_added'
SQL
channel_query = <<~SQL
SELECT *
FROM plugin_store_rows
WHERE type_name = 'JSON'
AND plugin_name = 'discourse-chat-integration'
AND key LIKE 'channel:%'
AND id = :channel_id
LIMIT 1
SQL
automation_creation = <<~SQL
INSERT INTO discourse_automation_automations (script, trigger, name, enabled, last_updated_by_id, created_at, updated_at)
VALUES ('send_chat_integration_message', 'topic_tags_changed', 'When tags change in topic', true, -1, NOW(), NOW())
RETURNING id
SQL
create_automation_field = <<~SQL
INSERT INTO discourse_automation_fields (automation_id, name, component, metadata, target, created_at, updated_at)
VALUES (:automation_id, :name, :component, :metadata, :target, NOW(), NOW())
SQL
provider_identifier_map = {
"groupme" => "groupme_instance_name",
"discord" => "name",
"guilded" => "name",
"mattermost" => "identifier",
"matrix" => "name",
"teams" => "name",
"zulip" => "stream",
"powerautomate" => "name",
"rocketchat" => "identifier",
"gitter" => "name",
"telegram" => "name",
"flowdock" => "flow_token",
"google" => "name",
"webex" => "name",
"slack" => "identifier",
}
DB
.query(rules_with_tag_added)
.each do |row|
rule = JSON.parse(row.value).with_indifferent_access
channel =
JSON.parse(
DB.query(channel_query, channel_id: rule[:channel_id]).first.value,
).with_indifferent_access
provider_name = channel[:provider]
channel_name = channel[:data][provider_identifier_map[provider_name]]
category_id = rule[:category_id]
tags = rule[:tags]
automation_id = DB.query(automation_creation).first.id
# Triggers:
# Watching categories
metadata = (category_id ? { "value" => [category_id] } : {}).to_json
DB.exec(
create_automation_field,
automation_id: automation_id,
name: "watching_categories",
component: "categories",
metadata: metadata,
target: "trigger",
)
# Watching tags
metadata = (tags.present? ? { "value" => tags } : {}).to_json
DB.exec(
create_automation_field,
automation_id: automation_id,
name: "watching_tags",
component: "tags",
metadata: metadata,
target: "trigger",
)
# Script options:
# Provider
DB.exec(
create_automation_field,
automation_id: automation_id,
name: "provider",
component: "choices",
metadata: { "value" => provider_name }.to_json,
target: "script",
)
# Channel name
DB.exec(
create_automation_field,
automation_id: automation_id,
name: "channel_name",
component: "text",
metadata: { "value" => channel_name }.to_json,
target: "script",
)
end
rescue StandardError
puts "Error migrating tag_added filters to all providers"
end
end
end
def down
DB.exec <<~SQL if defined?(DiscourseAutomation)
DELETE FROM discourse_automation_automations
WHERE script = 'send_chat_integration_message'
AND trigger = 'topic_tags_changed'
AND name = 'When tags change in topic'
SQL
end
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
module DiscourseChatIntegration
class ChatIntegrationReferencePost
def initialize(user:, topic:, kind:, raw: nil, context: {})
@user = user
@topic = topic
@kind = kind
@raw = raw if raw.present?
@context = context
@created_at = Time.current
end
def id
@topic.posts.empty? ? @topic.id : @topic.posts.first.id
end
def user
@user
end
def topic
@topic
end
def full_url
@topic.posts.empty? ? @topic.full_url : @topic.posts.first.full_url
end
def excerpt(maxlength = nil, options = {})
cooked = PrettyText.cook(raw, { user_id: user.id })
maxlength ||= SiteSetting.post_excerpt_maxlength
PrettyText.excerpt(cooked, maxlength, options)
end
def is_first_post?
topic.try(:highest_post_number) == 0
end
def created_at
@created_at
end
def raw
if @raw.nil? && @kind == DiscourseAutomation::Triggers::TOPIC_TAGS_CHANGED
tag_list_to_raw = ->(tag_list) do
tag_list.sort.map { |tag_name| "##{tag_name}" }.join(", ")
end
added_tags = @context["added_tags"]
removed_tags = @context["removed_tags"]
@raw =
if added_tags.present? && removed_tags.present?
I18n.t(
"chat_integration.topic_tag_changed.added_and_removed",
added: tag_list_to_raw.call(added_tags),
removed: tag_list_to_raw.call(removed_tags),
)
elsif added_tags.present?
I18n.t(
"chat_integration.topic_tag_changed.added",
added: tag_list_to_raw.call(added_tags),
)
elsif removed_tags.present?
I18n.t(
"chat_integration.topic_tag_changed.removed",
removed: tag_list_to_raw.call(removed_tags),
)
end
end
@raw
end
end
end

View File

@ -5,7 +5,7 @@ module DiscourseChatIntegration
module DiscordProvider module DiscordProvider
PROVIDER_NAME = "discord".freeze PROVIDER_NAME = "discord".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_discord_enabled PROVIDER_ENABLED_SETTING = :chat_integration_discord_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ {
@ -94,6 +94,13 @@ module DiscourseChatIntegration
) )
end end
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -3,6 +3,7 @@
module DiscourseChatIntegration::Provider::FlowdockProvider module DiscourseChatIntegration::Provider::FlowdockProvider
PROVIDER_NAME = "flowdock".freeze PROVIDER_NAME = "flowdock".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_flowdock_enabled PROVIDER_ENABLED_SETTING = :chat_integration_flowdock_enabled
CHANNEL_IDENTIFIER_KEY = "flow_token".freeze # this is really weird but is the only way to identify a channel in this provider
CHANNEL_PARAMETERS = [{ key: "flow_token", regex: '^\S+', unique: true, hidden: true }] CHANNEL_PARAMETERS = [{ key: "flow_token", regex: '^\S+', unique: true, hidden: true }]
def self.send_message(url, message) def self.send_message(url, message)
@ -60,4 +61,11 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
} }
end end
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module GitterProvider module GitterProvider
PROVIDER_NAME = "gitter".freeze PROVIDER_NAME = "gitter".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_gitter_enabled PROVIDER_ENABLED_SETTING = :chat_integration_gitter_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ {
@ -43,6 +44,13 @@ module DiscourseChatIntegration
"[__#{display_name}__ - #{topic.title} - #{category_name}](#{post.full_url})" "[__#{display_name}__ - #{topic.title} - #{category_name}](#{post.full_url})"
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module GoogleProvider module GoogleProvider
PROVIDER_NAME = "google".freeze PROVIDER_NAME = "google".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_google_enabled PROVIDER_ENABLED_SETTING = :chat_integration_google_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ {
@ -107,6 +108,13 @@ module DiscourseChatIntegration
], ],
} }
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -2,6 +2,7 @@
module DiscourseChatIntegration::Provider::GroupmeProvider module DiscourseChatIntegration::Provider::GroupmeProvider
PROVIDER_NAME = "groupme".freeze PROVIDER_NAME = "groupme".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_groupme_enabled PROVIDER_ENABLED_SETTING = :chat_integration_groupme_enabled
CHANNEL_IDENTIFIER_KEY = "groupme_instance_name".freeze
CHANNEL_PARAMETERS = [{ key: "groupme_instance_name", regex: '[\s\S]*', unique: true }] CHANNEL_PARAMETERS = [{ key: "groupme_instance_name", regex: '[\s\S]*', unique: true }]
def self.generate_groupme_message(post) def self.generate_groupme_message(post)
@ -84,4 +85,11 @@ module DiscourseChatIntegration::Provider::GroupmeProvider
data_package = generate_groupme_message(post) data_package = generate_groupme_message(post)
self.send_via_webhook(data_package, channel) self.send_via_webhook(data_package, channel)
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -5,7 +5,7 @@ module DiscourseChatIntegration
module GuildedProvider module GuildedProvider
PROVIDER_NAME = "guilded".freeze PROVIDER_NAME = "guilded".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_guilded_enabled PROVIDER_ENABLED_SETTING = :chat_integration_guilded_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ {
@ -92,6 +92,13 @@ module DiscourseChatIntegration
return url if !url.start_with?("//") return url if !url.start_with?("//")
"http:#{url}" "http:#{url}"
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module MatrixProvider module MatrixProvider
PROVIDER_NAME = "matrix".freeze PROVIDER_NAME = "matrix".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_matrix_enabled PROVIDER_ENABLED_SETTING = :chat_integration_matrix_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ key: "room_id", regex: '^\!\S+:\S+$', unique: true, hidden: true }, { key: "room_id", regex: '^\!\S+:\S+$', unique: true, hidden: true },
@ -88,6 +89,13 @@ module DiscourseChatIntegration
end end
end end
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module MattermostProvider module MattermostProvider
PROVIDER_NAME = "mattermost".freeze PROVIDER_NAME = "mattermost".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_mattermost_enabled PROVIDER_ENABLED_SETTING = :chat_integration_mattermost_enabled
CHANNEL_IDENTIFIER_KEY = "identifier".freeze
CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }] CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
def self.send_via_webhook(message) def self.send_via_webhook(message)
@ -93,6 +94,13 @@ module DiscourseChatIntegration
self.send_via_webhook(message) self.send_via_webhook(message)
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -3,6 +3,7 @@
module DiscourseChatIntegration::Provider::PowerAutomateProvider module DiscourseChatIntegration::Provider::PowerAutomateProvider
PROVIDER_NAME = "powerautomate" PROVIDER_NAME = "powerautomate"
PROVIDER_ENABLED_SETTING = :chat_integration_powerautomate_enabled PROVIDER_ENABLED_SETTING = :chat_integration_powerautomate_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url", regex: '^https:\/\/\S+$', unique: true, hidden: true }, { key: "webhook_url", regex: '^https:\/\/\S+$', unique: true, hidden: true },
@ -130,4 +131,11 @@ module DiscourseChatIntegration::Provider::PowerAutomateProvider
message message
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -4,7 +4,7 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
PROVIDER_NAME = "rocketchat".freeze PROVIDER_NAME = "rocketchat".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_rocketchat_enabled PROVIDER_ENABLED_SETTING = :chat_integration_rocketchat_enabled
CHANNEL_IDENTIFIER_KEY = "identifier".freeze
CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }] CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
def self.rocketchat_message(post, channel) def self.rocketchat_message(post, channel)
@ -82,4 +82,11 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
self.send_via_webhook(message) self.send_via_webhook(message)
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -12,7 +12,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
THREAD_LEGACY = "thread" THREAD_LEGACY = "thread"
PROVIDER_ENABLED_SETTING = :chat_integration_slack_enabled PROVIDER_ENABLED_SETTING = :chat_integration_slack_enabled
CHANNEL_IDENTIFIER_KEY = "identifier".freeze
CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]?\S*$', unique: true }] CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]?\S*$', unique: true }]
require_dependency "topic" require_dependency "topic"
@ -339,6 +339,13 @@ module DiscourseChatIntegration::Provider::SlackProvider
def self.create_tag_list(tag_list) def self.create_tag_list(tag_list)
tag_list.map { |tag_name| "<#{Tag.find_by_name(tag_name).full_url}|#{tag_name}>" }.join(", ") tag_list.map { |tag_name| "<#{Tag.find_by_name(tag_name).full_url}|#{tag_name}>" }.join(", ")
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
require_relative "slack_message_formatter" require_relative "slack_message_formatter"

View File

@ -3,6 +3,7 @@
module DiscourseChatIntegration::Provider::TeamsProvider module DiscourseChatIntegration::Provider::TeamsProvider
PROVIDER_NAME = "teams".freeze PROVIDER_NAME = "teams".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url", regex: '^https:\/\/\S+$', unique: true, hidden: true }, { key: "webhook_url", regex: '^https:\/\/\S+$', unique: true, hidden: true },
@ -82,4 +83,11 @@ module DiscourseChatIntegration::Provider::TeamsProvider
message message
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module TelegramProvider module TelegramProvider
PROVIDER_NAME = "telegram".freeze PROVIDER_NAME = "telegram".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ key: "chat_id", regex: '^(-?[0-9]+|@\S+)$', unique: true }, { key: "chat_id", regex: '^(-?[0-9]+|@\S+)$', unique: true },
@ -110,6 +111,13 @@ module DiscourseChatIntegration
} }
end end
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -3,6 +3,7 @@
module DiscourseChatIntegration::Provider::WebexProvider module DiscourseChatIntegration::Provider::WebexProvider
PROVIDER_NAME = "webex".freeze PROVIDER_NAME = "webex".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_webex_enabled PROVIDER_ENABLED_SETTING = :chat_integration_webex_enabled
CHANNEL_IDENTIFIER_KEY = "name".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ {
@ -71,4 +72,11 @@ module DiscourseChatIntegration::Provider::WebexProvider
{ markdown: markdown } { markdown: markdown }
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end

View File

@ -5,6 +5,7 @@ module DiscourseChatIntegration
module ZulipProvider module ZulipProvider
PROVIDER_NAME = "zulip".freeze PROVIDER_NAME = "zulip".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_zulip_enabled PROVIDER_ENABLED_SETTING = :chat_integration_zulip_enabled
CHANNEL_IDENTIFIER_KEY = "stream".freeze
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "stream", unique: true, regex: '^\S+' }, { key: "stream", unique: true, regex: '^\S+' },
{ key: "subject", unique: true, regex: '^\S+' }, { key: "subject", unique: true, regex: '^\S+' },
@ -71,6 +72,13 @@ module DiscourseChatIntegration
} }
end end
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
end end

View File

@ -16,6 +16,7 @@ register_svg_icon "fa-arrow-circle-o-right" if respond_to?(:register_svg_icon)
# Site setting validators must be loaded before initialize # Site setting validators must be loaded before initialize
require_relative "lib/discourse_chat_integration/provider/slack/slack_enabled_setting_validator" require_relative "lib/discourse_chat_integration/provider/slack/slack_enabled_setting_validator"
require_relative "lib/discourse_chat_integration/chat_integration_reference_post"
after_initialize do after_initialize do
require_relative "app/initializers/discourse_chat_integration" require_relative "app/initializers/discourse_chat_integration"
@ -84,5 +85,52 @@ after_initialize do
end end
end end
end end
add_automation_scriptable("send_chat_integration_message") do
field :provider,
component: :choices,
extra: {
content:
DiscourseChatIntegration::Provider.enabled_provider_names.map do |provider|
{ id: provider, name: "chat_integration.provider.#{provider}.title" }
end,
},
required: true
field :channel_name, component: :text, required: true
version 1
triggerables %i[topic_tags_changed]
script do |context, fields, automation|
provider = fields.dig("provider", "value")
channel_name = fields.dig("channel_name", "value")
post =
DiscourseChatIntegration::ChatIntegrationReferencePost.new(
user: context["user"],
topic: context["topic"],
kind: context["kind"],
context: {
"added_tags" => context["added_tags"],
"removed_tags" => context["removed_tags"],
},
)
provider = DiscourseChatIntegration::Provider.get_by_name(provider)
channel = provider.get_channel_by_name(channel_name) # user must have created a channel in /admin/plugins/chat-integration/<provider> page
if channel.nil?
Rails.logger.warn "[discourse-automation] Channel not found. Automation ID: #{automation.id}"
next
end
begin
provider.trigger_notification(post, channel, nil)
rescue StandardError => _
Rails.logger.warn "[discourse-automation] Error while sending chat integration message. Automation ID: #{automation.id}"
end
end
end
end end
end end

View File

@ -40,6 +40,7 @@ RSpec.shared_context "with validated dummy provider" do
module ::DiscourseChatIntegration::Provider::Dummy2Provider module ::DiscourseChatIntegration::Provider::Dummy2Provider
PROVIDER_NAME = "dummy2".freeze PROVIDER_NAME = "dummy2".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_enabled # Tie to main plugin enabled setting PROVIDER_ENABLED_SETTING = :chat_integration_enabled # Tie to main plugin enabled setting
CHANNEL_IDENTIFIER_KEY = "val".freeze
CHANNEL_PARAMETERS = [{ key: "val", regex: '^\S+$', unique: true }] CHANNEL_PARAMETERS = [{ key: "val", regex: '^\S+$', unique: true }]
@@sent_messages = [] @@sent_messages = []
@ -51,8 +52,17 @@ RSpec.shared_context "with validated dummy provider" do
def self.sent_messages def self.sent_messages
@@sent_messages @@sent_messages
end end
def self.get_channel_by_name(name)
DiscourseChatIntegration::Channel
.with_provider(PROVIDER_NAME)
.with_data_value(CHANNEL_IDENTIFIER_KEY, name)
.first
end
end end
end end
after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider) } after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider) }
let(:validated_provider) { ::DiscourseChatIntegration::Provider::Dummy2Provider }
end end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require_relative "../dummy_provider"
RSpec.describe "Triggering notifications" do
include_context "with validated dummy provider"
context "with automation installed", if: defined?(DiscourseAutomation) do
fab!(:admin)
fab!(:category)
fab!(:tag)
fab!(:automation) do
Fabricate(
:automation,
script: "send_chat_integration_message",
trigger: "topic_tags_changed",
enabled: true,
)
end
let(:channel1) do
DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "channel" })
end
before do
SiteSetting.chat_integration_enabled = true
SiteSetting.discourse_automation_enabled = true
SiteSetting.tagging_enabled = true
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:everyone]
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:everyone]
automation.upsert_field!(
"watching_categories",
"categories",
{ "value" => [category.id] },
target: "trigger",
)
automation.upsert_field!(
"watching_tags",
"tags",
{ "value" => [tag.name] },
target: "trigger",
)
automation.upsert_field!(
"provider",
"choices",
{ "value" => channel1.provider },
target: "script",
)
automation.upsert_field!("channel_name", "text", { "value" => "channel" }, target: "script")
end
it "triggers a notification" do
topic = Fabricate(:topic, user: admin, tags: [], category: category)
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), [tag.name])
expect(validated_provider.sent_messages.length).to eq(1)
expect(validated_provider.sent_messages.first[:post]).to eq(topic.id)
expect(validated_provider.sent_messages.first[:channel]).to eq(channel1)
end
it "only triggers for the correct tag" do
topic = Fabricate(:topic, user: admin, tags: [], category: category)
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), ["other_tag"])
expect(validated_provider.sent_messages.length).to eq(0)
end
it "only triggers for the correct category" do
topic = Fabricate(:topic, user: admin, tags: [], category: Fabricate(:category))
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), [tag.name])
expect(validated_provider.sent_messages.length).to eq(0)
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
RSpec.describe DiscourseChatIntegration::ChatIntegrationReferencePost do
fab!(:topic)
fab!(:first_post) { Fabricate(:post, topic: topic) }
let!(:context) do
{
"user" => Fabricate(:user),
"topic" => topic,
# every rule will add a kind and their context params
}
end
describe "when creating when topic tags change" do
before do
context["kind"] = DiscourseAutomation::Triggers::TOPIC_TAGS_CHANGED
context["added_tags"] = %w[tag1 tag2]
context["removed_tags"] = %w[tag3 tag4]
end
it "creates a post with the correct raw" do
post =
described_class.new(
user: context["user"],
topic: context["topic"],
kind: context["kind"],
context: {
"added_tags" => context["added_tags"],
"removed_tags" => context["removed_tags"],
},
)
expect(post.raw).to eq("Added #tag1, #tag2 and removed #tag3, #tag4")
end
it "has a working excerpt" do
post =
described_class.new(
user: context["user"],
topic: context["topic"],
kind: context["kind"],
context: {
"added_tags" => context["added_tags"],
"removed_tags" => context["removed_tags"],
},
)
expect(post.excerpt).to eq("Added #tag1, #tag2 and removed #tag3, #tag4")
end
end
end

View File

@ -49,4 +49,18 @@ RSpec.describe DiscourseChatIntegration::Provider::DiscordProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "discord",
data: {
name: "Awesome Channel",
webhook_url: "https://discord.com/api/webhooks/1234/abcd",
},
)
expect(described_class.get_channel_by_name("Awesome Channel")).to eq(expected)
end
end
end end

View File

@ -36,4 +36,19 @@ RSpec.describe DiscourseChatIntegration::Provider::FlowdockProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "flowdock",
data: {
flow_token: "5d1fe04cf66e078d6a2b579ddb8a465b",
},
)
expect(described_class.get_channel_by_name("5d1fe04cf66e078d6a2b579ddb8a465b")).to eq(
expected,
)
end
end
end end

View File

@ -37,4 +37,18 @@ RSpec.describe DiscourseChatIntegration::Provider::GitterProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "gitter",
data: {
name: "gitterHQ/services",
webhook_url: "https://webhooks.gitter.im/e/a1e2i3o4u5",
},
)
expect(described_class.get_channel_by_name("gitterHQ/services")).to eq(expected)
end
end
end end

View File

@ -33,4 +33,18 @@ RSpec.describe DiscourseChatIntegration::Provider::GoogleProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "google",
data: {
name: "discourse",
webhook_url: "https://chat.googleapis.com/v1/abcdefg",
},
)
expect(described_class.get_channel_by_name("discourse")).to eq(expected)
end
end
end end

View File

@ -39,4 +39,17 @@ RSpec.describe DiscourseChatIntegration::Provider::GroupmeProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "groupme",
data: {
groupme_instance_name: "my instance",
},
)
expect(described_class.get_channel_by_name("my instance")).to eq(expected)
end
end
end end

View File

@ -35,4 +35,18 @@ RSpec.describe DiscourseChatIntegration::Provider::GuildedProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "guilded",
data: {
name: "Awesome Channel",
webhook_url: "https://media.guilded.gg/webhooks/1234/abcd",
},
)
expect(described_class.get_channel_by_name("Awesome Channel")).to eq(expected)
end
end
end end

View File

@ -44,4 +44,18 @@ RSpec.describe DiscourseChatIntegration::Provider::MatrixProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "matrix",
data: {
name: "Awesome Channel",
room_id: "!blah:matrix.org",
},
)
expect(described_class.get_channel_by_name("Awesome Channel")).to eq(expected)
end
end
end end

View File

@ -57,4 +57,17 @@ RSpec.describe DiscourseChatIntegration::Provider::MattermostProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "mattermost",
data: {
identifier: "#awesomechannel",
},
)
expect(described_class.get_channel_by_name("#awesomechannel")).to eq(expected)
end
end
end end

View File

@ -34,4 +34,19 @@ RSpec.describe DiscourseChatIntegration::Provider::PowerAutomateProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "powerautomate",
data: {
name: "discourse",
webhook_url:
"https://prod-189.westus.logic.azure.com:443/workflows/c94b462906e64fe8a7299043706be96e/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=-cmkg1oG-88dP3Yqdh62yTG1LUtJFcB91rQisorfw_w",
},
)
expect(described_class.get_channel_by_name("discourse")).to eq(expected)
end
end
end end

View File

@ -35,4 +35,17 @@ RSpec.describe DiscourseChatIntegration::Provider::RocketchatProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "rocketchat",
data: {
identifier: "#general",
},
)
expect(described_class.get_channel_by_name("#general")).to eq(expected)
end
end
end end

View File

@ -346,4 +346,17 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
}.to raise_error(StandardError) }.to raise_error(StandardError)
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "slack",
data: {
identifier: "#general",
},
)
expect(described_class.get_channel_by_name("#general")).to eq(expected)
end
end
end end

View File

@ -44,4 +44,19 @@ RSpec.describe DiscourseChatIntegration::Provider::TeamsProvider do
end end
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "teams",
data: {
name: "discourse",
webhook_url:
"https://outlook.office.com/webhook/677980e4-e03b-4a5e-ad29-dc1ee0c32a80@9e9b5238-5ab2-496a-8e6a-e9cf05c7eb5c/IncomingWebhook/e7a1006ded44478992769d0c4f391e34/e028ca8a-e9c8-4c6c-a4d8-578f881a3cff",
},
)
expect(described_class.get_channel_by_name("discourse")).to eq(expected)
end
end
end end

View File

@ -48,4 +48,18 @@ RSpec.describe DiscourseChatIntegration::Provider::TelegramProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "telegram",
data: {
name: "Awesome Channel",
chat_id: "123",
},
)
expect(described_class.get_channel_by_name("Awesome Channel")).to eq(expected)
end
end
end end

View File

@ -34,4 +34,19 @@ RSpec.describe DiscourseChatIntegration::Provider::WebexProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
expected =
DiscourseChatIntegration::Channel.create!(
provider: "webex",
data: {
name: "discourse",
webhook_url:
"https://webexapis.com/v1/webhooks/incoming/jAHJjVVQ1cgEwb4ikQQawIrGdUtlocKA9fSNvIyADQoYo0mI70pztWUDOu22gDRPJOEJtCsc688zi1RMa",
},
)
expect(described_class.get_channel_by_name("discourse")).to eq(expected)
end
end
end end

View File

@ -42,4 +42,19 @@ RSpec.describe DiscourseChatIntegration::Provider::ZulipProvider do
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
describe ".get_channel_by_name" do
it "returns the right channel" do
created =
DiscourseChatIntegration::Channel.create!(
provider: "zulip",
data: {
stream: "foo",
subject: "Discourse Notifications",
},
)
channel = described_class.get_channel_by_name("foo")
expect(channel).to eq(created)
end
end
end end