DEV: Introduce syntax_tree for ruby formatting (#149)

This commit is contained in:
David Taylor 2022-12-29 12:31:05 +00:00 committed by GitHub
parent f6dde41cba
commit 49956bf829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 3078 additions and 2236 deletions

View File

@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
cache: yarn cache: yarn
- name: Yarn install - name: Yarn install
@ -33,15 +33,15 @@ jobs:
bundler-cache: true bundler-cache: true
- name: ESLint - name: ESLint
if: ${{ always() }} if: ${{ !cancelled() }}
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets,admin/assets}/javascripts
- name: Prettier - name: Prettier
if: ${{ always() }} if: ${{ !cancelled() }}
shell: bash shell: bash
run: | run: |
yarn prettier -v yarn prettier -v
if [ 0 -lt $(find assets -type f \( -name "*.scss" -or -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find assets admin/assets -type f \( -name "*.scss" -or -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
yarn prettier --list-different "assets/**/*.{scss,js,es6}" yarn prettier --list-different "assets/**/*.{scss,js,es6}"
fi fi
if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
@ -49,9 +49,18 @@ jobs:
fi fi
- name: Ember template lint - name: Ember template lint
if: ${{ always() }} if: ${{ !cancelled() }}
run: yarn ember-template-lint --no-error-on-unmatched-pattern assets/javascripts run: yarn ember-template-lint --no-error-on-unmatched-pattern assets/javascripts admin/assets/javascripts
- name: Rubocop - name: Rubocop
if: ${{ always() }} if: ${{ !cancelled() }}
run: bundle exec rubocop . run: bundle exec rubocop .
- name: Syntax Tree
if: ${{ !cancelled() }}
run: |
if test -f .streerc; then
bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')
else
echo "Stree config not detected for this repository. Skipping."
fi

View File

@ -80,7 +80,7 @@ jobs:
- name: Get yarn cache directory - name: Get yarn cache directory
id: yarn-cache-dir id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache - name: Yarn cache
uses: actions/cache@v3 uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin RSpec - name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin QUnit - name: Plugin QUnit

View File

@ -1,2 +1,2 @@
inherit_gem: inherit_gem:
rubocop-discourse: default.yml rubocop-discourse: stree-compat.yml

2
.streerc Normal file
View File

@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source "https://rubygems.org"
group :development do group :development do
gem 'translations-manager', git: 'https://github.com/discourse/translations-manager.git' gem "translations-manager", git: "https://github.com/discourse/translations-manager.git"
gem 'rubocop-discourse' gem "rubocop-discourse"
gem "syntax_tree"
end end

View File

@ -12,6 +12,7 @@ GEM
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.1) parser (3.1.2.1)
ast (~> 2.4.1) ast (~> 2.4.1)
prettier_print (1.2.0)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.6.0) regexp_parser (2.6.0)
rexml (3.2.5) rexml (3.2.5)
@ -33,6 +34,8 @@ GEM
rubocop-rspec (2.13.2) rubocop-rspec (2.13.2)
rubocop (~> 1.33) rubocop (~> 1.33)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
syntax_tree (5.1.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.3.0) unicode-display_width (2.3.0)
PLATFORMS PLATFORMS
@ -40,6 +43,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
rubocop-discourse rubocop-discourse
syntax_tree
translations-manager! translations-manager!
BUNDLED WITH BUNDLED WITH

View File

@ -8,15 +8,16 @@ class DiscourseChatIntegration::ChatController < ApplicationController
end end
def list_providers def list_providers
providers = ::DiscourseChatIntegration::Provider.enabled_providers.map do |x| providers =
{ ::DiscourseChatIntegration::Provider.enabled_providers.map do |x|
name: x::PROVIDER_NAME, {
id: x::PROVIDER_NAME, name: x::PROVIDER_NAME,
channel_parameters: (defined? x::CHANNEL_PARAMETERS) ? x::CHANNEL_PARAMETERS : [] id: x::PROVIDER_NAME,
} channel_parameters: (defined?(x::CHANNEL_PARAMETERS)) ? x::CHANNEL_PARAMETERS : [],
end }
end
render json: providers, root: 'providers' render json: providers, root: "providers"
end end
def test def test
@ -28,9 +29,7 @@ class DiscourseChatIntegration::ChatController < ApplicationController
provider = ::DiscourseChatIntegration::Provider.get_by_name(channel.provider) provider = ::DiscourseChatIntegration::Provider.get_by_name(channel.provider)
if !DiscourseChatIntegration::Provider.is_enabled(provider) raise Discourse::NotFound if !DiscourseChatIntegration::Provider.is_enabled(provider)
raise Discourse::NotFound
end
post = Topic.find(topic_id.to_i).posts.first post = Topic.find(topic_id.to_i).posts.first
@ -56,34 +55,36 @@ class DiscourseChatIntegration::ChatController < ApplicationController
raise Discourse::InvalidParameters if !providers.include?(requested_provider) raise Discourse::InvalidParameters if !providers.include?(requested_provider)
channels = DiscourseChatIntegration::Channel.with_provider(requested_provider) channels = DiscourseChatIntegration::Channel.with_provider(requested_provider)
render_serialized channels, DiscourseChatIntegration::ChannelSerializer, root: 'channels' render_serialized channels, DiscourseChatIntegration::ChannelSerializer, root: "channels"
end end
def create_channel def create_channel
begin begin
providers = ::DiscourseChatIntegration::Provider.enabled_providers.map { |x| x::PROVIDER_NAME } providers =
::DiscourseChatIntegration::Provider.enabled_providers.map { |x| x::PROVIDER_NAME }
if !defined?(params[:channel]) && defined?(params[:channel][:provider]) if !defined?(params[:channel]) && defined?(params[:channel][:provider])
raise Discourse::InvalidParameters, 'Provider is not valid' raise Discourse::InvalidParameters, "Provider is not valid"
end end
requested_provider = params[:channel][:provider] requested_provider = params[:channel][:provider]
if !providers.include?(requested_provider) if !providers.include?(requested_provider)
raise Discourse::InvalidParameters, 'Provider is not valid' raise Discourse::InvalidParameters, "Provider is not valid"
end end
allowed_keys = DiscourseChatIntegration::Provider.get_by_name(requested_provider)::CHANNEL_PARAMETERS.map { |p| p[:key].to_sym } allowed_keys =
DiscourseChatIntegration::Provider.get_by_name(
requested_provider,
)::CHANNEL_PARAMETERS.map { |p| p[:key].to_sym }
hash = params.require(:channel).permit(:provider, data: allowed_keys) hash = params.require(:channel).permit(:provider, data: allowed_keys)
channel = DiscourseChatIntegration::Channel.new(hash) channel = DiscourseChatIntegration::Channel.new(hash)
if !channel.save raise Discourse::InvalidParameters, "Channel is not valid" if !channel.save
raise Discourse::InvalidParameters, 'Channel is not valid'
end
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: 'channel' render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: "channel"
rescue Discourse::InvalidParameters => e rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422 render json: { errors: [e.message] }, status: 422
end end
@ -94,15 +95,16 @@ class DiscourseChatIntegration::ChatController < ApplicationController
channel = DiscourseChatIntegration::Channel.find(params[:id].to_i) channel = DiscourseChatIntegration::Channel.find(params[:id].to_i)
channel.error_key = nil # Reset any error on the rule channel.error_key = nil # Reset any error on the rule
allowed_keys = DiscourseChatIntegration::Provider.get_by_name(channel.provider)::CHANNEL_PARAMETERS.map { |p| p[:key].to_sym } allowed_keys =
DiscourseChatIntegration::Provider.get_by_name(
channel.provider,
)::CHANNEL_PARAMETERS.map { |p| p[:key].to_sym }
hash = params.require(:channel).permit(data: allowed_keys) hash = params.require(:channel).permit(data: allowed_keys)
if !channel.update(hash) raise Discourse::InvalidParameters, "Channel is not valid" if !channel.update(hash)
raise Discourse::InvalidParameters, 'Channel is not valid'
end
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: 'channel' render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: "channel"
rescue Discourse::InvalidParameters => e rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422 render json: { errors: [e.message] }, status: 422
end end
@ -118,14 +120,13 @@ class DiscourseChatIntegration::ChatController < ApplicationController
def create_rule def create_rule
begin begin
hash = params.require(:rule).permit(:channel_id, :type, :filter, :group_id, :category_id, tags: []) hash =
params.require(:rule).permit(:channel_id, :type, :filter, :group_id, :category_id, tags: [])
rule = DiscourseChatIntegration::Rule.new(hash) rule = DiscourseChatIntegration::Rule.new(hash)
if !rule.save raise Discourse::InvalidParameters, "Rule is not valid" if !rule.save
raise Discourse::InvalidParameters, 'Rule is not valid'
end
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: 'rule' render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: "rule"
rescue Discourse::InvalidParameters => e rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422 render json: { errors: [e.message] }, status: 422
end end
@ -136,11 +137,9 @@ class DiscourseChatIntegration::ChatController < ApplicationController
rule = DiscourseChatIntegration::Rule.find(params[:id].to_i) rule = DiscourseChatIntegration::Rule.find(params[:id].to_i)
hash = params.require(:rule).permit(:type, :filter, :group_id, :category_id, tags: []) hash = params.require(:rule).permit(:type, :filter, :group_id, :category_id, tags: [])
if !rule.update(hash) raise Discourse::InvalidParameters, "Rule is not valid" if !rule.update(hash)
raise Discourse::InvalidParameters, 'Rule is not valid'
end
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: 'rule' render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: "rule"
rescue Discourse::InvalidParameters => e rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422 render json: { errors: [e.message] }, status: 422
end end

View File

@ -2,7 +2,6 @@
module DiscourseChatIntegration module DiscourseChatIntegration
module Helper module Helper
def self.process_command(channel, tokens) def self.process_command(channel, tokens)
guardian = DiscourseChatIntegration::Manager.guardian guardian = DiscourseChatIntegration::Manager.guardian
@ -16,13 +15,19 @@ module DiscourseChatIntegration
when "thread", "watch", "follow", "mute" when "thread", "watch", "follow", "mute"
return error_text if tokens.empty? return error_text if tokens.empty?
# If the first token in the command is a tag, this rule applies to all categories # If the first token in the command is a tag, this rule applies to all categories
category_name = tokens[0].start_with?('tag:') ? nil : tokens.shift category_name = tokens[0].start_with?("tag:") ? nil : tokens.shift
if category_name if category_name
category = Category.find_by(slug: category_name) category = Category.find_by(slug: category_name)
unless category unless category
cat_list = (CategoryList.new(guardian).categories.map(&:slug)).join(', ') cat_list = (CategoryList.new(guardian).categories.map(&:slug)).join(", ")
return I18n.t("chat_integration.provider.#{provider}.not_found.category", name: category_name, list: cat_list) return(
I18n.t(
"chat_integration.provider.#{provider}.not_found.category",
name: category_name,
list: cat_list,
)
)
end end
else else
category = nil # All categories category = nil # All categories
@ -32,8 +37,8 @@ module DiscourseChatIntegration
# Every remaining token must be a tag. If not, abort and send help text # Every remaining token must be a tag. If not, abort and send help text
while tokens.size > 0 while tokens.size > 0
token = tokens.shift token = tokens.shift
if token.start_with?('tag:') if token.start_with?("tag:")
tag_name = token.sub(/^tag:/, '') tag_name = token.sub(/^tag:/, "")
else else
return error_text return error_text
end end
@ -47,7 +52,12 @@ module DiscourseChatIntegration
end end
category_id = category.nil? ? nil : category.id category_id = category.nil? ? nil : category.id
case DiscourseChatIntegration::Helper.smart_create_rule(channel: channel, filter: cmd, category_id: category_id, tags: tags) case DiscourseChatIntegration::Helper.smart_create_rule(
channel: channel,
filter: cmd,
category_id: category_id,
tags: tags,
)
when :created when :created
I18n.t("chat_integration.provider.#{provider}.create.created") I18n.t("chat_integration.provider.#{provider}.create.created")
when :updated when :updated
@ -107,23 +117,25 @@ module DiscourseChatIntegration
end end
end end
text << I18n.t("chat_integration.provider.#{provider}.status.rule_string", text << I18n.t(
"chat_integration.provider.#{provider}.status.rule_string",
index: i, index: i,
filter: rule.filter, filter: rule.filter,
category: category_name category: category_name,
) )
if SiteSetting.tagging_enabled && (!rule.tags.nil?) if SiteSetting.tagging_enabled && (!rule.tags.nil?)
text << I18n.t("chat_integration.provider.#{provider}.status.rule_string_tags_suffix", tags: rule.tags.join(', ')) text << I18n.t(
"chat_integration.provider.#{provider}.status.rule_string_tags_suffix",
tags: rule.tags.join(", "),
)
end end
text << "\n" text << "\n"
i += 1 i += 1
end end
if rules.size == 0 text << I18n.t("chat_integration.provider.#{provider}.status.no_rules") if rules.size == 0
text << I18n.t("chat_integration.provider.#{provider}.status.no_rules")
end
text text
end end
@ -144,12 +156,15 @@ module DiscourseChatIntegration
# :created if a new rule has been created # :created if a new rule has been created
# false if there was an error # false if there was an error
def self.smart_create_rule(channel:, filter:, category_id: nil, tags: nil) def self.smart_create_rule(channel:, filter:, category_id: nil, tags: nil)
existing_rules = DiscourseChatIntegration::Rule.with_channel(channel).with_type('normal') existing_rules = DiscourseChatIntegration::Rule.with_channel(channel).with_type("normal")
# Select the ones that have the same category # Select the ones that have the same category
same_category = existing_rules.select { |rule| rule.category_id == category_id } same_category = existing_rules.select { |rule| rule.category_id == category_id }
same_category_and_tags = same_category.select { |rule| (rule.tags.nil? ? [] : rule.tags.sort) == (tags.nil? ? [] : tags.sort) } same_category_and_tags =
same_category.select do |rule|
(rule.tags.nil? ? [] : rule.tags.sort) == (tags.nil? ? [] : tags.sort)
end
if same_category_and_tags.size > 0 if same_category_and_tags.size > 0
# These rules have exactly the same criteria as what we're trying to create # These rules have exactly the same criteria as what we're trying to create
@ -185,7 +200,9 @@ module DiscourseChatIntegration
end end
# This rule is unique! Create a new one: # This rule is unique! Create a new one:
return :created if Rule.new(channel: channel, filter: filter, category_id: category_id, tags: tags).save if Rule.new(channel: channel, filter: filter, category_id: category_id, tags: tags).save
return :created
end
false false
end end
@ -197,13 +214,13 @@ module DiscourseChatIntegration
end end
def self.formatted_display_name(user) def self.formatted_display_name(user)
if !SiteSetting.enable_names || user.name.blank? return "@#{user.username}" if !SiteSetting.enable_names || user.name.blank?
return "@#{user.username}"
end
full_name = user.name full_name = user.name
full_name_normalized = User.normalize_username(full_name.strip) full_name_normalized = User.normalize_username(full_name.strip)
similar = full_name_normalized.gsub(' ', '_') == user.username_lower || full_name_normalized.gsub(' ', '') == user.username_lower similar =
full_name_normalized.gsub(" ", "_") == user.username_lower ||
full_name_normalized.gsub(" ", "") == user.username_lower
if similar && SiteSetting.prioritize_username_in_ux? if similar && SiteSetting.prioritize_username_in_ux?
"@#{user.username}" "@#{user.username}"
elsif similar elsif similar

View File

@ -3,9 +3,7 @@
module Jobs module Jobs
class DiscourseChatAddTypeField < ::Jobs::Onceoff class DiscourseChatAddTypeField < ::Jobs::Onceoff
def execute_onceoff(args) def execute_onceoff(args)
DiscourseChatIntegration::Rule.find_each do |rule| DiscourseChatIntegration::Rule.find_each { |rule| rule.save(validate: false) }
rule.save(validate: false)
end
end end
end end
end end

View File

@ -3,26 +3,26 @@
module Jobs module Jobs
class DiscourseChatMigrateFromSlackOfficial < ::Jobs::Onceoff class DiscourseChatMigrateFromSlackOfficial < ::Jobs::Onceoff
def execute_onceoff(args) def execute_onceoff(args)
slack_installed = PluginStoreRow.where(plugin_name: 'discourse-slack-official').exists? slack_installed = PluginStoreRow.where(plugin_name: "discourse-slack-official").exists?
if slack_installed if slack_installed
already_setup_rules = DiscourseChatIntegration::Channel.with_provider('slack').exists? already_setup_rules = DiscourseChatIntegration::Channel.with_provider("slack").exists?
already_setup_sitesettings = already_setup_sitesettings =
SiteSetting.chat_integration_slack_enabled || SiteSetting.chat_integration_slack_enabled ||
SiteSetting.chat_integration_slack_access_token.present? || SiteSetting.chat_integration_slack_access_token.present? ||
SiteSetting.chat_integration_slack_incoming_webhook_token.present? || SiteSetting.chat_integration_slack_incoming_webhook_token.present? ||
SiteSetting.chat_integration_slack_outbound_webhook_url.present? SiteSetting.chat_integration_slack_outbound_webhook_url.present?
if !already_setup_rules && !already_setup_sitesettings if !already_setup_rules && !already_setup_sitesettings
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
migrate_settings migrate_settings
migrate_data migrate_data
is_slack_enabled = site_settings_value('slack_enabled') is_slack_enabled = site_settings_value("slack_enabled")
if is_slack_enabled if is_slack_enabled
slack_enabled = SiteSetting.find_by(name: 'slack_enabled') slack_enabled = SiteSetting.find_by(name: "slack_enabled")
slack_enabled.update!(value: 'f') slack_enabled.update!(value: "f")
SiteSetting.chat_integration_slack_enabled = true SiteSetting.chat_integration_slack_enabled = true
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
@ -30,44 +30,55 @@ module Jobs
end end
end end
end end
end end
def migrate_data def migrate_data
rows = [] rows = []
PluginStoreRow.where(plugin_name: 'discourse-slack-official') PluginStoreRow
.where(plugin_name: "discourse-slack-official")
.where("key ~* :pat", pat: "^category_.*") .where("key ~* :pat", pat: "^category_.*")
.each do |row| .each do |row|
PluginStore
.cast_value(row.type_name, row.value)
.each do |rule|
category_id =
if row.key == "category_*"
nil
else
row.key.gsub!("category_", "")
row.key.to_i
end
PluginStore.cast_value(row.type_name, row.value).each do |rule| next if !category_id.nil? && !Category.exists?(id: category_id)
category_id =
if row.key == 'category_*' valid_tags = []
nil valid_tags = Tag.where(name: rule[:tags]).pluck(:name) if rule[:tags]
else
row.key.gsub!('category_', '') rows << {
row.key.to_i category_id: category_id,
channel: rule[:channel],
filter: rule[:filter],
tags: valid_tags,
}
end end
next if !category_id.nil? && !Category.exists?(id: category_id)
valid_tags = []
valid_tags = Tag.where(name: rule[:tags]).pluck(:name) if rule[:tags]
rows << {
category_id: category_id,
channel: rule[:channel],
filter: rule[:filter],
tags: valid_tags
}
end end
end
rows.each do |row| rows.each do |row|
# Load an existing channel with this identifier. If none, create it # Load an existing channel with this identifier. If none, create it
row[:channel] = "##{row[:channel]}" unless row[:channel].start_with?("#") row[:channel] = "##{row[:channel]}" unless row[:channel].start_with?("#")
channel = DiscourseChatIntegration::Channel.with_provider('slack').with_data_value('identifier', row[:channel]).first channel =
DiscourseChatIntegration::Channel
.with_provider("slack")
.with_data_value("identifier", row[:channel])
.first
if !channel if !channel
channel = DiscourseChatIntegration::Channel.create(provider: 'slack', data: { identifier: row[:channel] }) channel =
DiscourseChatIntegration::Channel.create(
provider: "slack",
data: {
identifier: row[:channel],
},
)
if !channel.id if !channel.id
Rails.logger.warn("Error creating channel for #{row}") Rails.logger.warn("Error creating channel for #{row}")
next next
@ -75,50 +86,58 @@ module Jobs
end end
# Create the rule, with clever logic for avoiding duplicates # Create the rule, with clever logic for avoiding duplicates
success = DiscourseChatIntegration::Helper.smart_create_rule(channel: channel, filter: row[:filter], category_id: row[:category_id], tags: row[:tags]) success =
DiscourseChatIntegration::Helper.smart_create_rule(
channel: channel,
filter: row[:filter],
category_id: row[:category_id],
tags: row[:tags],
)
end end
end end
private private
def migrate_settings def migrate_settings
if !(slack_access_token = site_settings_value('slack_access_token')).nil? if !(slack_access_token = site_settings_value("slack_access_token")).nil?
SiteSetting.chat_integration_slack_access_token = slack_access_token SiteSetting.chat_integration_slack_access_token = slack_access_token
end end
if !(slack_incoming_webhook_token = site_settings_value('slack_incoming_webhook_token')).nil? if !(slack_incoming_webhook_token = site_settings_value("slack_incoming_webhook_token")).nil?
SiteSetting.chat_integration_slack_incoming_webhook_token = slack_incoming_webhook_token SiteSetting.chat_integration_slack_incoming_webhook_token = slack_incoming_webhook_token
end end
if !(slack_discourse_excerpt_length = site_settings_value('slack_discourse_excerpt_length')).nil? if !(
slack_discourse_excerpt_length = site_settings_value("slack_discourse_excerpt_length")
).nil?
SiteSetting.chat_integration_slack_excerpt_length = slack_discourse_excerpt_length SiteSetting.chat_integration_slack_excerpt_length = slack_discourse_excerpt_length
end end
if !(slack_outbound_webhook_url = site_settings_value('slack_outbound_webhook_url')).nil? if !(slack_outbound_webhook_url = site_settings_value("slack_outbound_webhook_url")).nil?
SiteSetting.chat_integration_slack_outbound_webhook_url = slack_outbound_webhook_url SiteSetting.chat_integration_slack_outbound_webhook_url = slack_outbound_webhook_url
end end
if !(slack_icon_url = site_settings_value('slack_icon_url')).nil? if !(slack_icon_url = site_settings_value("slack_icon_url")).nil?
SiteSetting.chat_integration_slack_icon_url = slack_icon_url SiteSetting.chat_integration_slack_icon_url = slack_icon_url
end end
if !(post_to_slack_window_secs = site_settings_value('post_to_slack_window_secs')).nil? if !(post_to_slack_window_secs = site_settings_value("post_to_slack_window_secs")).nil?
SiteSetting.chat_integration_delay_seconds = post_to_slack_window_secs SiteSetting.chat_integration_delay_seconds = post_to_slack_window_secs
end end
if !(slack_discourse_username = site_settings_value('slack_discourse_username')).nil? if !(slack_discourse_username = site_settings_value("slack_discourse_username")).nil?
username = User.find_by(username: slack_discourse_username.downcase)&.username username = User.find_by(username: slack_discourse_username.downcase)&.username
SiteSetting.chat_integration_discourse_username = (username || Discourse.system_user.username) SiteSetting.chat_integration_discourse_username =
(username || Discourse.system_user.username)
end end
end end
def site_settings_value(name) def site_settings_value(name)
value = SiteSetting.find_by(name: name)&.value value = SiteSetting.find_by(name: name)&.value
if value == 't' if value == "t"
value = true value = true
elsif value == 'f' elsif value == "f"
value = false value = false
end end

View File

@ -2,10 +2,11 @@
class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
# Setup ActiveRecord::Store to use the JSON field to read/write these values # Setup ActiveRecord::Store to use the JSON field to read/write these values
store :value, accessors: [ :provider, :error_key, :error_info, :data ], coder: JSON store :value, accessors: %i[provider error_key error_info data], coder: JSON
scope :with_provider, ->(provider) { where("value::json->>'provider'=?", provider) } scope :with_provider, ->(provider) { where("value::json->>'provider'=?", provider) }
scope :with_data_value, ->(key, value) { where("(value::json->>'data')::json->>?=?", key.to_s, value.to_s) } scope :with_data_value,
->(key, value) { where("(value::json->>'data')::json->>?=?", key.to_s, value.to_s) }
after_initialize :init_data after_initialize :init_data
after_destroy :destroy_rules after_destroy :destroy_rules
@ -13,7 +14,7 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
validate :provider_valid?, :data_valid? validate :provider_valid?, :data_valid?
def self.key_prefix def self.key_prefix
'channel:'.freeze "channel:".freeze
end end
def rules def rules
@ -52,9 +53,7 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
data.each do |key, value| data.each do |key, value|
regex_string = params.find { |p| p[:key] == key }[:regex] regex_string = params.find { |p| p[:key] == key }[:regex]
if !Regexp.new(regex_string).match(value) errors.add(:data, "data.#{key} is invalid") if !Regexp.new(regex_string).match(value)
errors.add(:data, "data.#{key} is invalid")
end
unique = params.find { |p| p[:key] == key }[:unique] unique = params.find { |p| p[:key] == key }[:unique]
if unique if unique
@ -63,8 +62,6 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
end end
end end
if check_unique && matching_channels.exists? errors.add(:data, "matches an existing channel") if check_unique && matching_channels.exists?
errors.add(:data, "matches an existing channel")
end
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class DiscourseChatIntegration::PluginModel < PluginStoreRow class DiscourseChatIntegration::PluginModel < PluginStoreRow
PLUGIN_NAME = 'discourse-chat-integration' PLUGIN_NAME = "discourse-chat-integration"
default_scope { self.default_scope } default_scope { self.default_scope }
@ -9,13 +9,14 @@ class DiscourseChatIntegration::PluginModel < PluginStoreRow
before_save :set_key before_save :set_key
def self.default_scope def self.default_scope
where(type_name: 'JSON') where(type_name: "JSON").where(plugin_name: self::PLUGIN_NAME).where(
.where(plugin_name: self::PLUGIN_NAME) "key LIKE ?",
.where("key LIKE ?", "#{self.key_prefix}%") "#{self.key_prefix}%",
)
end end
def self.key_prefix def self.key_prefix
raise 'Not implemented' raise "Not implemented"
end end
private private
@ -25,7 +26,7 @@ class DiscourseChatIntegration::PluginModel < PluginStoreRow
end end
def init_plugin_model def init_plugin_model
self.type_name ||= 'JSON' self.type_name ||= "JSON"
self.plugin_name ||= PLUGIN_NAME self.plugin_name ||= PLUGIN_NAME
end end
@ -37,5 +38,4 @@ class DiscourseChatIntegration::PluginModel < PluginStoreRow
"#{self.key_prefix}#{max_id}" "#{self.key_prefix}#{max_id}"
end end
end end
end end

View File

@ -2,54 +2,65 @@
class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
# Setup ActiveRecord::Store to use the JSON field to read/write these values # Setup ActiveRecord::Store to use the JSON field to read/write these values
store :value, accessors: [ :channel_id, :type, :group_id, :category_id, :tags, :filter ], coder: JSON store :value, accessors: %i[channel_id type group_id category_id tags filter], coder: JSON
scope :with_type, ->(type) { where("value::json->>'type'=?", type.to_s) } scope :with_type, ->(type) { where("value::json->>'type'=?", type.to_s) }
scope :with_channel, ->(channel) { with_channel_id(channel.id) } scope :with_channel, ->(channel) { with_channel_id(channel.id) }
scope :with_channel_id, ->(channel_id) { where("value::json->>'channel_id'=?", channel_id.to_s) } scope :with_channel_id, ->(channel_id) { where("value::json->>'channel_id'=?", channel_id.to_s) }
scope :with_category_id, ->(category_id) do scope :with_category_id,
if category_id.nil? ->(category_id) {
where("(value::json->'category_id') IS NULL OR json_typeof(value::json->'category_id')='null'") if category_id.nil?
else where(
where("value::json->>'category_id'=?", category_id.to_s) "(value::json->'category_id') IS NULL OR json_typeof(value::json->'category_id')='null'",
end )
end else
where("value::json->>'category_id'=?", category_id.to_s)
end
}
scope :with_group_ids, ->(group_id) do scope :with_group_ids,
where("value::json->>'group_id' IN (?)", group_id.map!(&:to_s)) ->(group_id) { where("value::json->>'group_id' IN (?)", group_id.map!(&:to_s)) }
end
scope :order_by_precedence, -> { scope :order_by_precedence,
order(" -> {
order(
"
CASE CASE
WHEN value::json->>'type' = 'group_mention' THEN 1 WHEN value::json->>'type' = 'group_mention' THEN 1
WHEN value::json->>'type' = 'group_message' THEN 2 WHEN value::json->>'type' = 'group_message' THEN 2
ELSE 3 ELSE 3
END END
", ",
" "
CASE CASE
WHEN value::json->>'filter' = 'mute' THEN 1 WHEN value::json->>'filter' = 'mute' THEN 1
WHEN value::json->>'filter' = 'thread' THEN 2 WHEN value::json->>'filter' = 'thread' THEN 2
WHEN value::json->>'filter' = 'watch' THEN 3 WHEN value::json->>'filter' = 'watch' THEN 3
WHEN value::json->>'filter' = 'follow' THEN 4 WHEN value::json->>'filter' = 'follow' THEN 4
END END
") ",
} )
}
after_initialize :init_filter after_initialize :init_filter
validates :filter, inclusion: { in: %w(thread watch follow mute), validates :filter,
message: "%{value} is not a valid filter" } inclusion: {
in: %w[thread watch follow mute],
message: "%{value} is not a valid filter",
}
validates :type, inclusion: { in: %w(normal group_message group_mention), validates :type,
message: "%{value} is not a valid filter" } inclusion: {
in: %w[normal group_message group_mention],
message: "%{value} is not a valid filter",
}
validate :channel_valid?, :category_valid?, :group_valid?, :tags_valid? validate :channel_valid?, :category_valid?, :group_valid?, :tags_valid?
def self.key_prefix def self.key_prefix
'rule:'.freeze "rule:".freeze
end end
# We never want an empty array, set it to nil instead # We never want an empty array, set it to nil instead
@ -62,7 +73,7 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end end
# These are only allowed to be integers # These are only allowed to be integers
%w(channel_id category_id group_id).each do |name| %w[channel_id category_id group_id].each do |name|
define_method "#{name}=" do |val| define_method "#{name}=" do |val|
if val.nil? || val.blank? if val.nil? || val.blank?
super(nil) super(nil)
@ -91,11 +102,11 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end end
def category_valid? def category_valid?
if type != 'normal' && !category_id.nil? if type != "normal" && !category_id.nil?
errors.add(:category_id, "cannot be specified for that type of rule") errors.add(:category_id, "cannot be specified for that type of rule")
end end
return unless type == 'normal' return unless type == "normal"
if !(category_id.nil? || Category.where(id: category_id).exists?) if !(category_id.nil? || Category.where(id: category_id).exists?)
errors.add(:category_id, "#{category_id} is not a valid category id") errors.add(:category_id, "#{category_id} is not a valid category id")
@ -103,11 +114,11 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end end
def group_valid? def group_valid?
if type == 'normal' && !group_id.nil? if type == "normal" && !group_id.nil?
errors.add(:group_id, "cannot be specified for that type of rule") errors.add(:group_id, "cannot be specified for that type of rule")
end end
return if type == 'normal' return if type == "normal"
if !Group.where(id: group_id).exists? if !Group.where(id: group_id).exists?
errors.add(:group_id, "#{group_id} is not a valid group id") errors.add(:group_id, "#{group_id} is not a valid group id")
@ -118,14 +129,12 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
return if tags.nil? return if tags.nil?
tags.each do |tag| tags.each do |tag|
if !Tag.where(name: tag).exists? errors.add(:tags, "#{tag} is not a valid tag") if !Tag.where(name: tag).exists?
errors.add(:tags, "#{tag} is not a valid tag")
end
end end
end end
def init_filter def init_filter
self.filter ||= 'watch' self.filter ||= "watch"
self.type ||= 'normal' self.type ||= "normal"
end end
end end

View File

@ -1,10 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
Discourse::Application.routes.append do Discourse::Application.routes.append do
mount ::DiscourseChatIntegration::AdminEngine, at: '/admin/plugins/chat-integration', constraints: AdminConstraint.new mount ::DiscourseChatIntegration::AdminEngine,
mount ::DiscourseChatIntegration::PublicEngine, at: '/chat-transcript/', as: 'chat-transcript' at: "/admin/plugins/chat-integration",
mount ::DiscourseChatIntegration::Provider::HookEngine, at: '/chat-integration/' constraints: AdminConstraint.new
mount ::DiscourseChatIntegration::PublicEngine, at: "/chat-transcript/", as: "chat-transcript"
mount ::DiscourseChatIntegration::Provider::HookEngine, at: "/chat-integration/"
# For backwards compatibility with Slack plugin # For backwards compatibility with Slack plugin
post "/slack/command" => "discourse_chat_integration/provider/slack_provider/slack_command#command" post "/slack/command" =>
"discourse_chat_integration/provider/slack_provider/slack_command#command"
end end

View File

@ -1,26 +1,24 @@
# frozen_string_literal: true # frozen_string_literal: true
require_dependency 'admin_constraint' require_dependency "admin_constraint"
module ::DiscourseChatIntegration module ::DiscourseChatIntegration
AdminEngine.routes.draw do AdminEngine.routes.draw do
get "" => "chat#respond" get "" => "chat#respond"
get '/providers' => "chat#list_providers" get "/providers" => "chat#list_providers"
post '/test' => "chat#test" post "/test" => "chat#test"
get '/channels' => "chat#list_channels" get "/channels" => "chat#list_channels"
post '/channels' => "chat#create_channel" post "/channels" => "chat#create_channel"
put '/channels/:id' => "chat#update_channel" put "/channels/:id" => "chat#update_channel"
delete '/channels/:id' => "chat#destroy_channel" delete "/channels/:id" => "chat#destroy_channel"
post '/rules' => "chat#create_rule" post "/rules" => "chat#create_rule"
put '/rules/:id' => "chat#update_rule" put "/rules/:id" => "chat#update_rule"
delete '/rules/:id' => "chat#destroy_rule" delete "/rules/:id" => "chat#destroy_rule"
get "/:provider" => "chat#respond" get "/:provider" => "chat#respond"
end end
PublicEngine.routes.draw do PublicEngine.routes.draw { get "/:secret" => "public#post_transcript" }
get '/:secret' => "public#post_transcript"
end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative './rule_serializer' require_relative "./rule_serializer"
class DiscourseChatIntegration::ChannelSerializer < ApplicationSerializer class DiscourseChatIntegration::ChannelSerializer < ApplicationSerializer
attributes :id, :provider, :error_key, :error_info, :data, :rules attributes :id, :provider, :error_key, :error_info, :data, :rules

View File

@ -2,7 +2,6 @@
module DiscourseChatIntegration module DiscourseChatIntegration
module Manager module Manager
def self.guardian def self.guardian
Guardian.new(User.find_by(username: SiteSetting.chat_integration_discourse_username)) Guardian.new(User.find_by(username: SiteSetting.chat_integration_discourse_username))
end end
@ -23,19 +22,26 @@ module DiscourseChatIntegration
if topic.archetype == Archetype.private_message if topic.archetype == Archetype.private_message
group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id) group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id)
return if group_ids_with_access.empty? return if group_ids_with_access.empty?
matching_rules = DiscourseChatIntegration::Rule.with_type('group_message').with_group_ids(group_ids_with_access) matching_rules =
DiscourseChatIntegration::Rule.with_type("group_message").with_group_ids(
group_ids_with_access,
)
else else
matching_rules = DiscourseChatIntegration::Rule.with_type('normal').with_category_id(topic.category_id) matching_rules =
DiscourseChatIntegration::Rule.with_type("normal").with_category_id(topic.category_id)
if topic.category # Also load the rules for the wildcard category if topic.category # Also load the rules for the wildcard category
matching_rules += DiscourseChatIntegration::Rule.with_type('normal').with_category_id(nil) matching_rules += DiscourseChatIntegration::Rule.with_type("normal").with_category_id(nil)
end end
# If groups are mentioned, check for any matching rules and append them # If groups are mentioned, check for any matching rules and append them
mentions = post.raw_mentions mentions = post.raw_mentions
if mentions && mentions.length > 0 if mentions && mentions.length > 0
groups = Group.where('LOWER(name) IN (?)', mentions) groups = Group.where("LOWER(name) IN (?)", mentions)
if groups.exists? if groups.exists?
matching_rules += DiscourseChatIntegration::Rule.with_type('group_mention').with_group_ids(groups.map(&:id)) matching_rules +=
DiscourseChatIntegration::Rule.with_type("group_mention").with_group_ids(
groups.map(&:id),
)
end end
end end
end end
@ -43,17 +49,19 @@ module DiscourseChatIntegration
# If tagging is enabled, thow away rules that don't apply to this topic # If tagging is enabled, thow away rules that don't apply to this topic
if SiteSetting.tagging_enabled if SiteSetting.tagging_enabled
topic_tags = topic.tags.present? ? topic.tags.pluck(:name) : [] topic_tags = topic.tags.present? ? topic.tags.pluck(:name) : []
matching_rules = matching_rules.select do |rule| matching_rules =
next true if rule.tags.nil? || rule.tags.empty? # Filter has no tags specified matching_rules.select do |rule|
any_tags_match = !((rule.tags & topic_tags).empty?) next true if rule.tags.nil? || rule.tags.empty? # Filter has no tags specified
next any_tags_match # If any tags match, keep this filter, otherwise throw away any_tags_match = !((rule.tags & topic_tags).empty?)
end next any_tags_match # If any tags match, keep this filter, otherwise throw away
end
end end
# Sort by order of precedence # Sort by order of precedence
t_prec = { 'group_message' => 0, 'group_mention' => 1, 'normal' => 2 } # Group things win t_prec = { "group_message" => 0, "group_mention" => 1, "normal" => 2 } # Group things win
f_prec = { 'mute' => 0, 'thread' => 1, 'watch' => 2, 'follow' => 3 } #(mute always wins; thread beats watch beats follow) f_prec = { "mute" => 0, "thread" => 1, "watch" => 2, "follow" => 3 } #(mute always wins; thread beats watch beats follow)
sort_func = proc { |a, b| [t_prec[a.type], f_prec[a.filter]] <=> [t_prec[b.type], f_prec[b.filter]] } sort_func =
proc { |a, b| [t_prec[a.type], f_prec[a.filter]] <=> [t_prec[b.type], f_prec[b.filter]] }
matching_rules = matching_rules.sort(&sort_func) matching_rules = matching_rules.sort(&sort_func)
# Take the first rule for each channel # Take the first rule for each channel
@ -81,14 +89,15 @@ module DiscourseChatIntegration
begin begin
provider.trigger_notification(post, channel, rule) provider.trigger_notification(post, channel, rule)
channel.update_attribute('error_key', nil) if channel.error_key channel.update_attribute("error_key", nil) if channel.error_key
rescue => e rescue => e
if e.class == (DiscourseChatIntegration::ProviderError) && e.info.key?(:error_key) && !e.info[:error_key].nil? if e.class == (DiscourseChatIntegration::ProviderError) && e.info.key?(:error_key) &&
channel.update_attribute('error_key', e.info[:error_key]) !e.info[:error_key].nil?
channel.update_attribute("error_key", e.info[:error_key])
else else
channel.update_attribute('error_key', 'chat_integration.channel_exception') channel.update_attribute("error_key", "chat_integration.channel_exception")
end end
channel.update_attribute('error_info', JSON.pretty_generate(e.try(:info))) channel.update_attribute("error_info", JSON.pretty_generate(e.try(:info)))
# Log the error # Log the error
# Discourse.handle_job_exception(e, # Discourse.handle_job_exception(e,
@ -99,10 +108,7 @@ module DiscourseChatIntegration
# error_info: e.class == DiscourseChatIntegration::ProviderError ? e.info : nil } # error_info: e.class == DiscourseChatIntegration::ProviderError ? e.info : nil }
# ) # )
end end
end end
end end
end end
end end

View File

@ -2,9 +2,10 @@
class AddUniqueIndexToSlackThreadTs < ActiveRecord::Migration[6.1] class AddUniqueIndexToSlackThreadTs < ActiveRecord::Migration[6.1]
def up def up
add_index :topic_custom_fields, [:topic_id, :name], add_index :topic_custom_fields,
unique: true, %i[topic_id name],
where: "(name LIKE 'slack_thread_id_%')", unique: true,
name: "index_topic_custom_fields_on_topic_id_and_slack_thread_id" where: "(name LIKE 'slack_thread_id_%')",
name: "index_topic_custom_fields_on_topic_id_and_slack_thread_id"
end end
end end

View File

@ -12,15 +12,11 @@ module DiscourseChatIntegration
module Provider module Provider
def self.providers def self.providers
constants.select do |constant| constants.select { |constant| constant.to_s =~ /Provider$/ }.map(&method(:const_get))
constant.to_s =~ /Provider$/
end.map(&method(:const_get))
end end
def self.enabled_providers def self.enabled_providers
self.providers.select do |provider| self.providers.select { |provider| self.is_enabled(provider) }
self.is_enabled(provider)
end
end end
def self.provider_names def self.provider_names
@ -36,7 +32,7 @@ module DiscourseChatIntegration
end end
def self.is_enabled(provider) def self.is_enabled(provider)
if defined? provider::PROVIDER_ENABLED_SETTING if defined?(provider::PROVIDER_ENABLED_SETTING)
SiteSetting.public_send(provider::PROVIDER_ENABLED_SETTING) SiteSetting.public_send(provider::PROVIDER_ENABLED_SETTING)
else else
false false
@ -51,9 +47,10 @@ module DiscourseChatIntegration
class HookController < ::ApplicationController class HookController < ::ApplicationController
requires_plugin DiscourseChatIntegration::PLUGIN_NAME requires_plugin DiscourseChatIntegration::PLUGIN_NAME
class ProviderDisabled < StandardError; end class ProviderDisabled < StandardError
end
rescue_from ProviderDisabled do rescue_from ProviderDisabled do
rescue_discourse_actions(:not_found, 404) rescue_discourse_actions(:not_found, 404)
end end
@ -72,22 +69,20 @@ module DiscourseChatIntegration
def self.mount_engines def self.mount_engines
engines = [] engines = []
DiscourseChatIntegration::Provider.providers.each do |provider| DiscourseChatIntegration::Provider.providers.each do |provider|
engine = provider.constants.select do |constant| engine =
constant.to_s =~ (/Engine$/) && (constant.to_s != "HookEngine") provider
end.map(&provider.method(:const_get)).first .constants
.select { |constant| constant.to_s =~ (/Engine$/) && (constant.to_s != "HookEngine") }
.map(&provider.method(:const_get))
.first
if engine engines.push(engine: engine, name: provider::PROVIDER_NAME) if engine
engines.push(engine: engine, name: provider::PROVIDER_NAME)
end
end end
DiscourseChatIntegration::Provider::HookEngine.routes.draw do DiscourseChatIntegration::Provider::HookEngine.routes.draw do
engines.each do |engine| engines.each { |engine| mount engine[:engine], at: engine[:name] }
mount engine[:engine], at: engine[:name]
end
end end
end end
end end
end end

View File

@ -8,7 +8,12 @@ module DiscourseChatIntegration
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ key: "webhook_url", regex: '^https:\/\/discord\.com\/api\/webhooks\/', unique: true, hidden: true } {
key: "webhook_url",
regex: '^https:\/\/discord\.com\/api\/webhooks\/',
unique: true,
hidden: true,
},
].freeze ].freeze
def self.send_message(url, message) def self.send_message(url, message)
@ -17,7 +22,7 @@ module DiscourseChatIntegration
uri = URI(url) uri = URI(url)
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
@ -25,7 +30,7 @@ module DiscourseChatIntegration
end end
def self.ensure_protocol(url) def self.ensure_protocol(url)
return url if !url.start_with?('//') return url if !url.start_with?("//")
"http:#{url}" "http:#{url}"
end end
@ -34,24 +39,40 @@ module DiscourseChatIntegration
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category if topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
message = { message = {
content: SiteSetting.chat_integration_discord_message_content, content: SiteSetting.chat_integration_discord_message_content,
embeds: [{ embeds: [
title: "#{topic.title} #{(category == '[uncategorized]') ? '' : category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}", {
color: topic.category ? topic.category.color.to_i(16) : nil, title:
description: post.excerpt(SiteSetting.chat_integration_discord_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), "#{topic.title} #{(category == "[uncategorized]") ? "" : category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
url: post.full_url, color: topic.category ? topic.category.color.to_i(16) : nil,
author: { description:
name: display_name, post.excerpt(
url: Discourse.base_url + "/u/" + post.user.username, SiteSetting.chat_integration_discord_excerpt_length,
icon_url: ensure_protocol(post.user.small_avatar_url) text_entities: true,
} strip_links: true,
}] remap_emoji: true,
),
url: post.full_url,
author: {
name: display_name,
url: Discourse.base_url + "/u/" + post.user.username,
icon_url: ensure_protocol(post.user.small_avatar_url),
},
},
],
} }
message message
@ -59,17 +80,20 @@ module DiscourseChatIntegration
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
# Adding ?wait=true means that we actually get a success/failure response, rather than returning asynchronously # Adding ?wait=true means that we actually get a success/failure response, rather than returning asynchronously
webhook_url = "#{channel.data['webhook_url']}?wait=true" webhook_url = "#{channel.data["webhook_url"]}?wait=true"
message = generate_discord_message(post) message = generate_discord_message(post)
response = send_message(webhook_url, message) response = send_message(webhook_url, message)
if !response.kind_of?(Net::HTTPSuccess) if !response.kind_of?(Net::HTTPSuccess)
raise ::DiscourseChatIntegration::ProviderError.new(info: { raise ::DiscourseChatIntegration::ProviderError.new(
error_key: nil, message: message, response_body: response.body info: {
}) error_key: nil,
message: message,
response_body: response.body,
},
)
end end
end end
end end
end end
end end

View File

@ -1,12 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
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_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "flow_token", regex: '^\S+', unique: true, hidden: true }]
{ key: "flow_token", regex: '^\S+', unique: true, hidden: true },
]
def self.send_message(url, message) def self.send_message(url, message)
uri = URI(url) uri = URI(url)
@ -14,7 +11,7 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = true http.use_ssl = true
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
@ -29,15 +26,21 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
event: "discussion", event: "discussion",
author: { author: {
name: display_name, name: display_name,
avatar: post.user.small_avatar_url avatar: post.user.small_avatar_url,
}, },
title: I18n.t("chat_integration.provider.flowdock.message_title"), title: I18n.t("chat_integration.provider.flowdock.message_title"),
external_thread_id: post.topic.id, external_thread_id: post.topic.id,
body: post.excerpt(SiteSetting.chat_integration_flowdock_excerpt_length, text_entities: true, strip_links: false, remap_emoji: true), body:
post.excerpt(
SiteSetting.chat_integration_flowdock_excerpt_length,
text_entities: true,
strip_links: false,
remap_emoji: true,
),
thread: { thread: {
title: post.topic.title, title: post.topic.title,
external_url: post.full_url external_url: post.full_url,
} },
} }
message message
@ -50,7 +53,11 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
unless response.kind_of?(Net::HTTPSuccess) unless response.kind_of?(Net::HTTPSuccess)
error_key = nil error_key = nil
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, message: message, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
message: message,
response_body: response.body,
}
end end
end end
end end

View File

@ -3,19 +3,28 @@
module DiscourseChatIntegration module DiscourseChatIntegration
module Provider module Provider
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_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url", regex: '^https://webhooks\.gitter\.im/e/\S+$', unique: true, hidden: true } {
key: "webhook_url",
regex: '^https://webhooks\.gitter\.im/e/\S+$',
unique: true,
hidden: true,
},
] ]
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
message = gitter_message(post) message = gitter_message(post)
response = Net::HTTP.post_form(URI(channel.data['webhook_url']), message: message) response = Net::HTTP.post_form(URI(channel.data["webhook_url"]), message: message)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
error_key = nil error_key = nil
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, message: message, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
message: message,
response_body: response.body,
}
end end
end end
@ -23,7 +32,14 @@ module DiscourseChatIntegration
display_name = post.user.username display_name = post.user.username
topic = post.topic topic = post.topic
parent_category = topic.category.try :parent_category parent_category = topic.category.try :parent_category
category_name = parent_category ? "[#{parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category_name =
(
if parent_category
"[#{parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
"[__#{display_name}__ - #{topic.title} - #{category_name}](#{post.full_url})" "[__#{display_name}__ - #{topic.title} - #{category_name}](#{post.full_url})"
end end

View File

@ -3,26 +3,35 @@
module DiscourseChatIntegration module DiscourseChatIntegration
module Provider module Provider
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_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url", regex: '^https:\/\/chat.googleapis.com\/v1\/\S+$', unique: true, hidden: true } {
key: "webhook_url",
regex: '^https:\/\/chat.googleapis.com\/v1\/\S+$',
unique: true,
hidden: true,
},
] ]
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
message = get_message(post) message = get_message(post)
uri = URI(channel.data['webhook_url']) uri = URI(channel.data["webhook_url"])
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
raise ::DiscourseChatIntegration::ProviderError.new info: { request: req.body, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
end end
@ -35,49 +44,67 @@ module DiscourseChatIntegration
widgets: [ widgets: [
{ {
keyValue: { keyValue: {
"topLabel": I18n.t("chat_integration.provider.google.new_#{post.is_first_post? ? "topic" : "post"}", site_title: SiteSetting.title), topLabel:
"content": post.topic.title, I18n.t(
"contentMultiline": "false", "chat_integration.provider.google.new_#{post.is_first_post? ? "topic" : "post"}",
"bottomLabel": I18n.t("chat_integration.provider.google.author", username: post.user.username), site_title: SiteSetting.title,
"onClick": { ),
"openLink": { content: post.topic.title,
"url": post.full_url contentMultiline: "false",
} bottomLabel:
} I18n.t(
} "chat_integration.provider.google.author",
username: post.user.username,
),
onClick: {
openLink: {
url: post.full_url,
},
},
},
}, },
] ],
}, },
{ {
widgets: [ widgets: [
{ {
textParagraph: { textParagraph: {
text: post.excerpt(SiteSetting.chat_integration_google_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true) text:
} post.excerpt(
SiteSetting.chat_integration_google_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
},
}, },
] ],
}, },
{ {
widgets: [ widgets: [
{ {
buttons: [ buttons: [
{ {
"textButton": { textButton: {
"text": I18n.t("chat_integration.provider.google.link", site_title: SiteSetting.title), text:
"onClick": { I18n.t(
"openLink": { "chat_integration.provider.google.link",
"url": post.full_url site_title: SiteSetting.title,
} ),
} onClick: {
} openLink: {
url: post.full_url,
},
},
},
}, },
] ],
} },
] ],
}, },
], ],
} },
] ],
} }
end end
end end

View File

@ -2,27 +2,32 @@
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_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "groupme_instance_name", regex: '[\s\S]*', unique: true }]
{ key: "groupme_instance_name", regex: '[\s\S]*', unique: true }
]
def self.generate_groupme_message(post) def self.generate_groupme_message(post)
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user) display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "#{I18n.t('uncategorized_category_name')}" category = "#{I18n.t("uncategorized_category_name")}"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? "#{topic.category.parent_category.name}/#{topic.category.name}" : "#{topic.category.name}" category =
(
if (topic.category.parent_category)
"#{topic.category.parent_category.name}/#{topic.category.name}"
else
"#{topic.category.name}"
end
)
end end
pre_post_text = "#{display_name} Posted to #{SiteSetting.title}\n\nTopic: #{topic.title} [#{category}]" pre_post_text =
"#{display_name} Posted to #{SiteSetting.title}\n\nTopic: #{topic.title} [#{category}]"
read_more = "(Read More: #{post.full_url})" read_more = "(Read More: #{post.full_url})"
post_excerpt = "#{post.excerpt(SiteSetting.chat_integration_groupme_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true)}" post_excerpt =
data = { "#{post.excerpt(SiteSetting.chat_integration_groupme_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true)}"
text: "#{pre_post_text}\n\n#{post_excerpt}\n#{read_more}" data = { text: "#{pre_post_text}\n\n#{post_excerpt}\n#{read_more}" }
}
data data
end end
@ -35,33 +40,39 @@ module DiscourseChatIntegration::Provider::GroupmeProvider
instance_names = SiteSetting.chat_integration_groupme_instance_names.split(/\s*,\s*/) instance_names = SiteSetting.chat_integration_groupme_instance_names.split(/\s*,\s*/)
unless instance_names.length() == bot_ids.length() unless instance_names.length() == bot_ids.length()
instance_names = [I18n.t('chat_integration.provider.groupme.errors.instance_names_issue')] * bot_ids.length() instance_names =
[I18n.t("chat_integration.provider.groupme.errors.instance_names_issue")] * bot_ids.length()
end end
name_to_id = Hash[instance_names.zip(bot_ids)] name_to_id = Hash[instance_names.zip(bot_ids)]
user_input_channel = channel.data['groupme_instance_name'].strip user_input_channel = channel.data["groupme_instance_name"].strip
unless user_input_channel.eql? 'all' instance_names = [user_input_channel] unless user_input_channel.eql? "all"
instance_names = [user_input_channel] instance_names.each do |instance_name|
end
instance_names.each { |instance_name|
bot_id = name_to_id["#{instance_name}"] bot_id = name_to_id["#{instance_name}"]
uri = URI("https://api.groupme.com/v3/bots/post") uri = URI("https://api.groupme.com/v3/bots/post")
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
message[:bot_id] = bot_id message[:bot_id] = bot_id
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
num_errors += 1 num_errors += 1
if response.code.to_s == '404' if response.code.to_s == "404"
error_key = 'chat_integration.provider.groupme.errors.not_found' error_key = "chat_integration.provider.groupme.errors.not_found"
else else
error_key = nil error_key = nil
end end
last_error_raised = { error_key: error_key, groupme_name: instance_name, bot_id: bot_id, request: req.body, response_code: response.code, response_body: response.body } last_error_raised = {
error_key: error_key,
groupme_name: instance_name,
bot_id: bot_id,
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
} end
if last_error_raised if last_error_raised
successfully_sent = instance_names.length() - num_errors successfully_sent = instance_names.length() - num_errors
last_error_raised[:success_rate] = "#{successfully_sent}/#{instance_names.length()}" last_error_raised[:success_rate] = "#{successfully_sent}/#{instance_names.length()}"

View File

@ -7,27 +7,43 @@ module DiscourseChatIntegration
PROVIDER_ENABLED_SETTING = :chat_integration_guilded_enabled PROVIDER_ENABLED_SETTING = :chat_integration_guilded_enabled
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+' }, { key: "name", regex: '^\S+' },
{ key: "webhook_url", regex: '^https:\/\/media\.guilded\.gg\/webhooks\/', unique: true, hidden: true } {
key: "webhook_url",
regex: '^https:\/\/media\.guilded\.gg\/webhooks\/',
unique: true,
hidden: true,
},
].freeze ].freeze
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
webhook_url = channel.data['webhook_url'] webhook_url = channel.data["webhook_url"]
message = generate_guilded_message(post) message = generate_guilded_message(post)
response = send_message(webhook_url, message) response = send_message(webhook_url, message)
if !response.kind_of?(Net::HTTPSuccess) if !response.kind_of?(Net::HTTPSuccess)
raise ::DiscourseChatIntegration::ProviderError.new(info: { raise ::DiscourseChatIntegration::ProviderError.new(
error_key: nil, message: message, response_body: response.body info: {
}) error_key: nil,
message: message,
response_body: response.body,
},
)
end end
end end
def self.generate_guilded_message(post) def self.generate_guilded_message(post)
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category if topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user) display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
@ -37,15 +53,24 @@ module DiscourseChatIntegration
end end
message = { message = {
embeds: [{ embeds: [
title: "#{topic.title} #{(category == '[uncategorized]') ? '' : category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}", {
url: post.full_url, title:
description: post.excerpt(SiteSetting.chat_integration_guilded_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), "#{topic.title} #{(category == "[uncategorized]") ? "" : category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
footer: { url: post.full_url,
icon_url: ensure_protocol(post.user.small_avatar_url), description:
text: "#{display_name} | #{post.created_at}" post.excerpt(
} SiteSetting.chat_integration_guilded_excerpt_length,
}] text_entities: true,
strip_links: true,
remap_emoji: true,
),
footer: {
icon_url: ensure_protocol(post.user.small_avatar_url),
text: "#{display_name} | #{post.created_at}",
},
},
],
} }
message message
@ -54,9 +79,9 @@ module DiscourseChatIntegration
def self.send_message(url, message) def self.send_message(url, message)
uri = URI(url) uri = URI(url)
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
@ -64,10 +89,9 @@ module DiscourseChatIntegration
end end
def self.ensure_protocol(url) def self.ensure_protocol(url)
return url if !url.start_with?('//') return url if !url.start_with?("//")
"http:#{url}" "http:#{url}"
end end
end end
end end
end end

View File

@ -6,25 +6,27 @@ module DiscourseChatIntegration
PROVIDER_NAME = "matrix".freeze PROVIDER_NAME = "matrix".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_matrix_enabled PROVIDER_ENABLED_SETTING = :chat_integration_matrix_enabled
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 },
] ]
def self.send_message(room_id, message) def self.send_message(room_id, message)
homeserver = SiteSetting.chat_integration_matrix_homeserver homeserver = SiteSetting.chat_integration_matrix_homeserver
event_type = 'm.room.message' event_type = "m.room.message"
uid = Time.now.to_i uid = Time.now.to_i
url_params = URI.encode_www_form(access_token: SiteSetting.chat_integration_matrix_access_token) url_params =
URI.encode_www_form(access_token: SiteSetting.chat_integration_matrix_access_token)
url = "#{homeserver}/_matrix/client/r0/rooms/#{CGI::escape(room_id)}/send/#{event_type}/#{uid}" url =
"#{homeserver}/_matrix/client/r0/rooms/#{CGI.escape(room_id)}/send/#{event_type}/#{uid}"
uri = URI([url, url_params].join('?')) uri = URI([url, url_params].join("?"))
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = true http.use_ssl = true
req = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Put.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
@ -35,16 +37,29 @@ module DiscourseChatIntegration
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user) display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
message = { message = {
msgtype: SiteSetting.chat_integration_matrix_use_notice ? 'm.notice' : 'm.text', msgtype: SiteSetting.chat_integration_matrix_use_notice ? "m.notice" : "m.text",
body: I18n.t('chat_integration.provider.matrix.text_message', user: display_name, body:
post_url: post.full_url, I18n.t(
title: post.topic.title), "chat_integration.provider.matrix.text_message",
format: 'org.matrix.custom.html', user: display_name,
formatted_body: I18n.t('chat_integration.provider.matrix.formatted_message', user: display_name, post_url: post.full_url,
post_url: post.full_url, title: post.topic.title,
title: post.topic.title, ),
excerpt: post.excerpt(SiteSetting.chat_integration_matrix_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true)) format: "org.matrix.custom.html",
formatted_body:
I18n.t(
"chat_integration.provider.matrix.formatted_message",
user: display_name,
post_url: post.full_url,
title: post.topic.title,
excerpt:
post.excerpt(
SiteSetting.chat_integration_matrix_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
),
} }
message message
@ -53,24 +68,26 @@ module DiscourseChatIntegration
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
message = generate_matrix_message(post) message = generate_matrix_message(post)
response = send_message(channel.data['room_id'], message) response = send_message(channel.data["room_id"], message)
if !response.kind_of?(Net::HTTPSuccess) if !response.kind_of?(Net::HTTPSuccess)
error_key = nil error_key = nil
begin begin
responseData = JSON.parse(response.body) responseData = JSON.parse(response.body)
if responseData['errcode'] == "M_UNKNOWN_TOKEN" if responseData["errcode"] == "M_UNKNOWN_TOKEN"
error_key = 'chat_integration.provider.matrix.errors.unknown_token' error_key = "chat_integration.provider.matrix.errors.unknown_token"
elsif responseData['errcode'] == "M_UNKNOWN" elsif responseData["errcode"] == "M_UNKNOWN"
error_key = 'chat_integration.provider.matrix.errors.unknown_room' error_key = "chat_integration.provider.matrix.errors.unknown_room"
end end
ensure ensure
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, message: message, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
message: message,
response_body: response.body,
}
end end
end end
end end
end end
end end
end end

View File

@ -15,22 +15,18 @@ module DiscourseChatIntegration::Provider::MattermostProvider
def command def command
text = process_command(params) text = process_command(params)
render json: { render json: { response_type: "ephemeral", text: text }
response_type: 'ephemeral',
text: text
}
end end
def process_command(params) def process_command(params)
tokens = params[:text].split(" ") tokens = params[:text].split(" ")
# channel name fix # channel name fix
channel_id = channel_id =
case params[:channel_name] case params[:channel_name]
when 'directmessage' when "directmessage"
"@#{params[:user_name]}" "@#{params[:user_name]}"
when 'privategroup' when "privategroup"
params[:channel_id] params[:channel_id]
else else
"##{params[:channel_name]}" "##{params[:channel_name]}"
@ -38,21 +34,29 @@ module DiscourseChatIntegration::Provider::MattermostProvider
provider = DiscourseChatIntegration::Provider::MattermostProvider::PROVIDER_NAME provider = DiscourseChatIntegration::Provider::MattermostProvider::PROVIDER_NAME
channel = DiscourseChatIntegration::Channel.with_provider(provider).with_data_value('identifier', channel_id).first channel =
DiscourseChatIntegration::Channel
.with_provider(provider)
.with_data_value("identifier", channel_id)
.first
# Create channel if doesn't exist # Create channel if doesn't exist
channel ||= DiscourseChatIntegration::Channel.create!(provider: provider, data: { identifier: channel_id }) channel ||=
DiscourseChatIntegration::Channel.create!(
provider: provider,
data: {
identifier: channel_id,
},
)
::DiscourseChatIntegration::Helper.process_command(channel, tokens) ::DiscourseChatIntegration::Helper.process_command(channel, tokens)
end end
def mattermost_token_valid? def mattermost_token_valid?
params.require(:token) params.require(:token)
if SiteSetting.chat_integration_mattermost_incoming_webhook_token.blank? || if SiteSetting.chat_integration_mattermost_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_mattermost_incoming_webhook_token != params[:token] SiteSetting.chat_integration_mattermost_incoming_webhook_token != params[:token]
raise Discourse::InvalidAccess.new raise Discourse::InvalidAccess.new
end end
end end
@ -63,8 +67,5 @@ module DiscourseChatIntegration::Provider::MattermostProvider
isolate_namespace DiscourseChatIntegration::Provider::MattermostProvider isolate_namespace DiscourseChatIntegration::Provider::MattermostProvider
end end
MattermostEngine.routes.draw do MattermostEngine.routes.draw { post "command" => "mattermost_command#command" }
post "command" => "mattermost_command#command"
end
end end

View File

@ -5,29 +5,30 @@ 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_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
{ key: "identifier", regex: '^[@#]\S*$', unique: true }
]
def self.send_via_webhook(message) def self.send_via_webhook(message)
uri = URI(SiteSetting.chat_integration_mattermost_webhook_url) uri = URI(SiteSetting.chat_integration_mattermost_webhook_url)
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
if response.body.include? "Couldn't find the channel" if response.body.include? "Couldn't find the channel"
error_key = 'chat_integration.provider.mattermost.errors.channel_not_found' error_key = "chat_integration.provider.mattermost.errors.channel_not_found"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, request: req.body, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
end end
def self.mattermost_message(post, channel) def self.mattermost_message(post, channel)
@ -35,17 +36,26 @@ module DiscourseChatIntegration
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]" category = "[#{I18n.t("uncategorized_category_name")}]"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
icon_url = icon_url =
if SiteSetting.chat_integration_mattermost_icon_url.present? if SiteSetting.chat_integration_mattermost_icon_url.present?
UrlHelper.absolute(SiteSetting.chat_integration_mattermost_icon_url) UrlHelper.absolute(SiteSetting.chat_integration_mattermost_icon_url)
elsif (url = (SiteSetting.try(:site_logo_small_url) || SiteSetting.logo_small_url)).present? elsif (
url = (SiteSetting.try(:site_logo_small_url) || SiteSetting.logo_small_url)
).present?
UrlHelper.absolute(url) UrlHelper.absolute(url)
end end
@ -53,7 +63,7 @@ module DiscourseChatIntegration
channel: channel, channel: channel,
username: SiteSetting.title || "Discourse", username: SiteSetting.title || "Discourse",
icon_url: icon_url, icon_url: icon_url,
attachments: [] attachments: [],
} }
summary = { summary = {
@ -61,8 +71,15 @@ module DiscourseChatIntegration
author_name: display_name, author_name: display_name,
author_icon: post.user.small_avatar_url, author_icon: post.user.small_avatar_url,
color: topic.category ? "##{topic.category.color}" : nil, color: topic.category ? "##{topic.category.color}" : nil,
text: post.excerpt(SiteSetting.chat_integration_mattermost_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), text:
title: "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}", post.excerpt(
SiteSetting.chat_integration_mattermost_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
title:
"#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
title_link: post.full_url, title_link: post.full_url,
} }
@ -71,12 +88,11 @@ module DiscourseChatIntegration
end end
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
channel_id = channel.data['identifier'] channel_id = channel.data["identifier"]
message = mattermost_message(post, channel_id) message = mattermost_message(post, channel_id)
self.send_via_webhook(message) self.send_via_webhook(message)
end end
end end
end end
end end

View File

@ -5,36 +5,45 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
PROVIDER_ENABLED_SETTING = :chat_integration_rocketchat_enabled PROVIDER_ENABLED_SETTING = :chat_integration_rocketchat_enabled
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
{ key: "identifier", regex: '^[@#]\S*$', unique: true }
]
def self.rocketchat_message(post, channel) def self.rocketchat_message(post, channel)
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user) display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]" category = "[#{I18n.t("uncategorized_category_name")}]"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
message = { message = { channel: channel, attachments: [] }
channel: channel,
attachments: []
}
summary = { summary = {
fallback: "#{topic.title} - #{display_name}", fallback: "#{topic.title} - #{display_name}",
author_name: display_name, author_name: display_name,
author_icon: post.user.small_avatar_url, author_icon: post.user.small_avatar_url,
color: topic.category ? "##{topic.category.color}" : nil, color: topic.category ? "##{topic.category.color}" : nil,
text: post.excerpt(SiteSetting.chat_integration_rocketchat_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), text:
post.excerpt(
SiteSetting.chat_integration_rocketchat_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
mrkdwn_in: ["text"], mrkdwn_in: ["text"],
title: "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}", title:
title_link: post.full_url "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
title_link: post.full_url,
} }
message[:attachments].push(summary) message[:attachments].push(summary)
@ -46,25 +55,29 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
uri = URI(SiteSetting.chat_integration_rocketchat_webhook_url) uri = URI(SiteSetting.chat_integration_rocketchat_webhook_url)
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
if response.body.include?('invalid-channel') if response.body.include?("invalid-channel")
error_key = 'chat_integration.provider.rocketchat.errors.invalid_channel' error_key = "chat_integration.provider.rocketchat.errors.invalid_channel"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, request: req.body, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
end end
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
channel_id = channel.data['identifier'] channel_id = channel.data["identifier"]
message = rocketchat_message(post, channel_id) message = rocketchat_message(post, channel_id)
self.send_via_webhook(message) self.send_via_webhook(message)

View File

@ -11,7 +11,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
:preload_json, :preload_json,
:verify_authenticity_token, :verify_authenticity_token,
:redirect_to_login_if_required, :redirect_to_login_if_required,
only: [:command, :interactive] only: %i[command interactive]
def command def command
message = process_command(params) message = process_command(params)
@ -28,15 +28,14 @@ module DiscourseChatIntegration::Provider::SlackProvider
private private
def process_command(params) def process_command(params)
tokens = params[:text].split(" ") tokens = params[:text].split(" ")
# channel name fix # channel name fix
channel_id = channel_id =
case params[:channel_name] case params[:channel_name]
when 'directmessage' when "directmessage"
"@#{params[:user_name]}" "@#{params[:user_name]}"
when 'privategroup' when "privategroup"
params[:channel_id] params[:channel_id]
else else
"##{params[:channel_name]}" "##{params[:channel_name]}"
@ -44,17 +43,28 @@ module DiscourseChatIntegration::Provider::SlackProvider
provider = DiscourseChatIntegration::Provider::SlackProvider::PROVIDER_NAME provider = DiscourseChatIntegration::Provider::SlackProvider::PROVIDER_NAME
channel = DiscourseChatIntegration::Channel.with_provider(provider) channel =
.with_data_value('identifier', channel_id) DiscourseChatIntegration::Channel
.first .with_provider(provider)
.with_data_value("identifier", channel_id)
.first
channel ||= DiscourseChatIntegration::Channel.create!( channel ||=
provider: provider, DiscourseChatIntegration::Channel.create!(
data: { identifier: channel_id } provider: provider,
) data: {
identifier: channel_id,
},
)
if tokens[0] == 'post' if tokens[0] == "post"
process_post_request(channel, tokens, params[:channel_id], channel_id, params[:response_url]) process_post_request(
channel,
tokens,
params[:channel_id],
channel_id,
params[:response_url],
)
else else
{ text: ::DiscourseChatIntegration::Helper.process_command(channel, tokens) } { text: ::DiscourseChatIntegration::Helper.process_command(channel, tokens) }
end end
@ -66,9 +76,10 @@ module DiscourseChatIntegration::Provider::SlackProvider
end end
Scheduler::Defer.later "Processing slack transcript request" do Scheduler::Defer.later "Processing slack transcript request" do
response = build_post_request_response(channel, tokens, slack_channel_id, channel_name, response_url) response =
build_post_request_response(channel, tokens, slack_channel_id, channel_name, response_url)
http = DiscourseChatIntegration::Provider::SlackProvider.slack_api_http http = DiscourseChatIntegration::Provider::SlackProvider.slack_api_http
req = Net::HTTP::Post.new(URI(response_url), 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(URI(response_url), "Content-Type" => "application/json")
req.body = response.to_json req.body = response.to_json
http.request(req) http.request(req)
end end
@ -81,15 +92,16 @@ module DiscourseChatIntegration::Provider::SlackProvider
first_message_ts = nil first_message_ts = nil
requested_thread_ts = nil requested_thread_ts = nil
thread_url_regex = /^https:\/\/\S+\.slack\.com\/archives\/\S+\/p[0-9]{16}\?thread_ts=([0-9]{10}.[0-9]{6})\S*$/ thread_url_regex =
/^https:\/\/\S+\.slack\.com\/archives\/\S+\/p[0-9]{16}\?thread_ts=([0-9]{10}.[0-9]{6})\S*$/
slack_url_regex = /^https:\/\/\S+\.slack\.com\/archives\/\S+\/p([0-9]{16})\/?$/ slack_url_regex = /^https:\/\/\S+\.slack\.com\/archives\/\S+\/p([0-9]{16})\/?$/
if tokens.size > 2 && tokens[1] == "thread" && match = slack_url_regex.match(tokens[2]) if tokens.size > 2 && tokens[1] == "thread" && match = slack_url_regex.match(tokens[2])
requested_thread_ts = match.captures[0].insert(10, '.') requested_thread_ts = match.captures[0].insert(10, ".")
elsif tokens.size > 1 && match = thread_url_regex.match(tokens[1]) elsif tokens.size > 1 && match = thread_url_regex.match(tokens[1])
requested_thread_ts = match.captures[0] requested_thread_ts = match.captures[0]
elsif tokens.size > 1 && match = slack_url_regex.match(tokens[1]) elsif tokens.size > 1 && match = slack_url_regex.match(tokens[1])
first_message_ts = match.captures[0].insert(10, '.') first_message_ts = match.captures[0].insert(10, ".")
elsif tokens.size > 1 elsif tokens.size > 1
begin begin
requested_messages = Integer(tokens[1], 10) requested_messages = Integer(tokens[1], 10)
@ -100,12 +112,21 @@ module DiscourseChatIntegration::Provider::SlackProvider
error_key = "chat_integration.provider.slack.transcript.error" error_key = "chat_integration.provider.slack.transcript.error"
return { text: I18n.t(error_key) } unless transcript = SlackTranscript.new(channel_name: channel_name, channel_id: slack_channel_id, requested_thread_ts: requested_thread_ts) unless transcript =
SlackTranscript.new(
channel_name: channel_name,
channel_id: slack_channel_id,
requested_thread_ts: requested_thread_ts,
)
return { text: I18n.t(error_key) }
end
return { text: I18n.t("#{error_key}_users") } unless transcript.load_user_data return { text: I18n.t("#{error_key}_users") } unless transcript.load_user_data
return { text: I18n.t("#{error_key}_history") } unless transcript.load_chat_history return { text: I18n.t("#{error_key}_history") } unless transcript.load_chat_history
if first_message_ts if first_message_ts
return { text: I18n.t("#{error_key}_ts") } unless transcript.set_first_message_by_ts(first_message_ts) unless transcript.set_first_message_by_ts(first_message_ts)
return { text: I18n.t("#{error_key}_ts") }
end
elsif requested_messages elsif requested_messages
transcript.set_first_message_by_index(-requested_messages) transcript.set_first_message_by_index(-requested_messages)
else else
@ -123,22 +144,21 @@ module DiscourseChatIntegration::Provider::SlackProvider
# Do nothing # Do nothing
elsif json[:type] == "message_action" && json[:message][:thread_ts] elsif json[:type] == "message_action" && json[:message][:thread_ts]
# Context menu used on a threaded message # Context menu used on a threaded message
transcript = SlackTranscript.new( transcript =
channel_name: "##{json[:channel][:name]}", SlackTranscript.new(
channel_id: json[:channel][:id], channel_name: "##{json[:channel][:name]}",
requested_thread_ts: json[:message][:thread_ts] channel_id: json[:channel][:id],
) requested_thread_ts: json[:message][:thread_ts],
)
# Send a loading modal within 3 seconds: # Send a loading modal within 3 seconds:
req = Net::HTTP::Post.new( req =
"https://slack.com/api/views.open", Net::HTTP::Post.new(
'Content-Type' => 'application/json', "https://slack.com/api/views.open",
'Authorization' => "Bearer #{SiteSetting.chat_integration_slack_access_token}" "Content-Type" => "application/json",
) "Authorization" => "Bearer #{SiteSetting.chat_integration_slack_access_token}",
req.body = { )
"trigger_id": json[:trigger_id], req.body = { trigger_id: json[:trigger_id], view: transcript.build_modal_ui }.to_json
"view": transcript.build_modal_ui
}.to_json
response = http.request(req) response = http.request(req)
view_id = JSON.parse(response.body).dig("view", "id") view_id = JSON.parse(response.body).dig("view", "id")
@ -147,19 +167,17 @@ module DiscourseChatIntegration::Provider::SlackProvider
error_view = generate_error_view("history") unless transcript.load_chat_history error_view = generate_error_view("history") unless transcript.load_chat_history
# Then update the modal with the transcript link: # Then update the modal with the transcript link:
req = Net::HTTP::Post.new( req =
"https://slack.com/api/views.update", Net::HTTP::Post.new(
'Content-Type' => 'application/json', "https://slack.com/api/views.update",
'Authorization' => "Bearer #{SiteSetting.chat_integration_slack_access_token}" "Content-Type" => "application/json",
) "Authorization" => "Bearer #{SiteSetting.chat_integration_slack_access_token}",
req.body = { )
"view_id": view_id, req.body = { view_id: view_id, view: error_view || transcript.build_modal_ui }.to_json
"view": error_view || transcript.build_modal_ui
}.to_json
response = http.request(req) response = http.request(req)
else else
# Button clicked in one of our interactive messages # Button clicked in one of our interactive messages
req = Net::HTTP::Post.new(URI(json[:response_url]), 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(URI(json[:response_url]), "Content-Type" => "application/json")
req.body = build_interactive_response(json).to_json req.body = build_interactive_response(json).to_json
response = http.request(req) response = http.request(req)
end end
@ -177,26 +195,33 @@ module DiscourseChatIntegration::Provider::SlackProvider
constant_val = json[:callback_id] constant_val = json[:callback_id]
changed_val = json[:actions][0][:selected_options][0][:value] changed_val = json[:actions][0][:selected_options][0][:value]
first_message = (action_name == 'first_message') ? changed_val : constant_val first_message = (action_name == "first_message") ? changed_val : constant_val
last_message = (action_name == 'first_message') ? constant_val : changed_val last_message = (action_name == "first_message") ? constant_val : changed_val
end end
error_key = "chat_integration.provider.slack.transcript.error" error_key = "chat_integration.provider.slack.transcript.error"
return { text: I18n.t(error_key) } unless transcript = SlackTranscript.new( unless transcript =
channel_name: "##{json[:channel][:name]}", SlackTranscript.new(
channel_id: json[:channel][:id], channel_name: "##{json[:channel][:name]}",
requested_thread_ts: requested_thread channel_id: json[:channel][:id],
) requested_thread_ts: requested_thread,
)
return { text: I18n.t(error_key) }
end
return { text: I18n.t("#{error_key}_users") } unless transcript.load_user_data return { text: I18n.t("#{error_key}_users") } unless transcript.load_user_data
return { text: I18n.t("#{error_key}_history") } unless transcript.load_chat_history return { text: I18n.t("#{error_key}_history") } unless transcript.load_chat_history
if first_message if first_message
return { text: I18n.t("#{error_key}_ts") } unless transcript.set_first_message_by_ts(first_message) unless transcript.set_first_message_by_ts(first_message)
return { text: I18n.t("#{error_key}_ts") }
end
end end
if last_message if last_message
return { text: I18n.t("#{error_key}_ts") } unless transcript.set_last_message_by_ts(last_message) unless transcript.set_last_message_by_ts(last_message)
return { text: I18n.t("#{error_key}_ts") }
end
end end
transcript.build_slack_ui transcript.build_slack_ui
@ -210,17 +235,11 @@ module DiscourseChatIntegration::Provider::SlackProvider
type: "modal", type: "modal",
title: { title: {
type: "plain_text", type: "plain_text",
text: I18n.t("chat_integration.provider.slack.transcript.modal_title") text: I18n.t("chat_integration.provider.slack.transcript.modal_title"),
}, },
blocks: [ blocks: [
{ { type: "section", text: { type: "mrkdwn", text: ":warning: *#{I18n.t(error_key)}*" } },
type: "section", ],
text: {
type: "mrkdwn",
text: ":warning: *#{I18n.t(error_key)}*"
}
}
]
} }
end end
@ -228,8 +247,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
params.require(:token) params.require(:token)
if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? || if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_slack_incoming_webhook_token != params[:token] SiteSetting.chat_integration_slack_incoming_webhook_token != params[:token]
raise Discourse::InvalidAccess.new raise Discourse::InvalidAccess.new
end end
end end
@ -240,8 +258,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
json = JSON.parse(params[:payload], symbolize_names: true) json = JSON.parse(params[:payload], symbolize_names: true)
if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? || if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_slack_incoming_webhook_token != json[:token] SiteSetting.chat_integration_slack_incoming_webhook_token != json[:token]
raise Discourse::InvalidAccess.new raise Discourse::InvalidAccess.new
end end
end end
@ -256,5 +273,4 @@ module DiscourseChatIntegration::Provider::SlackProvider
post "command" => "slack_command#command" post "command" => "slack_command#command"
post "interactive" => "slack_command#interactive" post "interactive" => "slack_command#interactive"
end end
end end

View File

@ -6,13 +6,15 @@ class ChatIntegrationSlackEnabledSettingValidator
end end
def valid_value?(val) def valid_value?(val)
return true if val == ('f') || val == (false) return true if val == ("f") || val == (false)
return false if SiteSetting.chat_integration_slack_outbound_webhook_url.blank? && SiteSetting.chat_integration_slack_access_token.blank? if SiteSetting.chat_integration_slack_outbound_webhook_url.blank? &&
SiteSetting.chat_integration_slack_access_token.blank?
return false
end
true true
end end
def error_message def error_message
I18n.t('site_settings.errors.chat_integration_slack_api_configs_are_empty') I18n.t("site_settings.errors.chat_integration_slack_api_configs_are_empty")
end end
end end

View File

@ -12,7 +12,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
user["_transcript_username"] user["_transcript_username"]
elsif @raw.key?("username") elsif @raw.key?("username")
# This is for bot messages # This is for bot messages
@raw["username"].gsub(' ', '_') @raw["username"].gsub(" ", "_")
end end
end end
@ -22,74 +22,69 @@ module DiscourseChatIntegration::Provider::SlackProvider
def url def url
channel_id = @transcript.channel_id channel_id = @transcript.channel_id
ts = @raw['ts'].gsub('.', '') ts = @raw["ts"].gsub(".", "")
"https://slack.com/archives/#{channel_id}/p#{ts}" "https://slack.com/archives/#{channel_id}/p#{ts}"
end end
def text def text
text = @raw['text'].nil? ? "" : @raw['text'] text = @raw["text"].nil? ? "" : @raw["text"]
pre = {} pre = {}
# Extract code blocks and replace with placeholder # Extract code blocks and replace with placeholder
text = text.gsub(/```(.*?)```/m) do |match| text =
key = "pre:" + SecureRandom.alphanumeric(50) text.gsub(/```(.*?)```/m) do |match|
pre[key] = HTMLEntities.new.decode $1 key = "pre:" + SecureRandom.alphanumeric(50)
"\n```\n#{key}\n```\n" pre[key] = HTMLEntities.new.decode $1
end "\n```\n#{key}\n```\n"
end
# # Extract inline code and replace with placeholder # # Extract inline code and replace with placeholder
text = text.gsub(/(?<!`)`([^`]+?)`(?!`)/) do |match| text =
key = "pre:" + SecureRandom.alphanumeric(50) text.gsub(/(?<!`)`([^`]+?)`(?!`)/) do |match|
pre[key] = HTMLEntities.new.decode $1 key = "pre:" + SecureRandom.alphanumeric(50)
"`#{key}`" pre[key] = HTMLEntities.new.decode $1
end "`#{key}`"
end
# Format links (don't worry about special cases @ # !) # Format links (don't worry about special cases @ # !)
text = text.gsub(/<(.*?)>/) do |match| text =
group = $1 text.gsub(/<(.*?)>/) do |match|
parts = group.split('|') group = $1
link = parts[0].start_with?('@', '#', '!') ? nil : parts[0] parts = group.split("|")
text = parts.length > 1 ? parts[1] : parts[0] link = parts[0].start_with?("@", "#", "!") ? nil : parts[0]
text = parts.length > 1 ? parts[1] : parts[0]
if parts[0].start_with?('@') if parts[0].start_with?("@")
user_id = parts[0][1..-1] user_id = parts[0][1..-1]
if user = @transcript.users[user_id] if user = @transcript.users[user_id]
user_name = user['_transcript_username'] user_name = user["_transcript_username"]
else else
user_name = user_id user_name = user_id
end
next "@#{user_name}"
end end
next "@#{user_name}"
end
if link.nil? if link.nil?
text text
elsif link == text elsif link == text
"<#{link}>" "<#{link}>"
else else
"[#{text}](#{link})" "[#{text}](#{link})"
end
end end
end
# Add an extra * to each side for bold # Add an extra * to each side for bold
text = text.gsub(/\*.*?\*/) do |match| text = text.gsub(/\*.*?\*/) { |match| "*#{match}*" }
"*#{match}*"
end
# Add an extra ~ to each side for strikethrough # Add an extra ~ to each side for strikethrough
text = text.gsub(/~.*?~/) do |match| text = text.gsub(/~.*?~/) { |match| "~#{match}~" }
"~#{match}~"
end
# Replace emoji - with _ # Replace emoji - with _
text = text.gsub(/:[a-z0-9_-]+:/) do |match| text = text.gsub(/:[a-z0-9_-]+:/) { |match| match.gsub("-") { "_" } }
match.gsub("-") { "_" }
end
# Restore pre-formatted code block content # Restore pre-formatted code block content
pre.each do |key, value| pre.each { |key, value| text = text.gsub(key) { value } }
text = text.gsub(key) { value }
end
text text
end end
@ -97,9 +92,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
def attachments_string def attachments_string
string = "" string = ""
string += "\n" if !attachments.empty? string += "\n" if !attachments.empty?
attachments.each do |attachment| attachments.each { |attachment| string += " - #{attachment}\n" }
string += " - #{attachment}\n"
end
string string
end end
@ -108,7 +101,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
end end
def raw_text def raw_text
raw_text = @raw['text'].nil? ? "" : @raw['text'] raw_text = @raw["text"].nil? ? "" : @raw["text"]
raw_text += attachments_string raw_text += attachments_string
raw_text raw_text
end end
@ -116,7 +109,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
def attachments def attachments
attachments = [] attachments = []
return attachments unless @raw.key?('attachments') return attachments unless @raw.key?("attachments")
@raw["attachments"].each do |attachment| @raw["attachments"].each do |attachment|
next unless attachment.key?("fallback") next unless attachment.key?("fallback")

View File

@ -8,7 +8,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
@excerpt = +"" @excerpt = +""
end end
def self.format(html = '') def self.format(html = "")
me = self.new me = self.new
parser = Nokogiri::HTML::SAX::Parser.new(me) parser = Nokogiri::HTML::SAX::Parser.new(me)
parser.parse(html) parser.parse(html)
@ -20,7 +20,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
when "a" when "a"
attributes = Hash[*attributes.flatten] attributes = Hash[*attributes.flatten]
@in_a = true @in_a = true
@excerpt << "<#{absolute_url(attributes['href'])}|" @excerpt << "<#{absolute_url(attributes["href"])}|"
end end
end end
@ -40,13 +40,18 @@ module DiscourseChatIntegration::Provider::SlackProvider
private private
def absolute_url(url) def absolute_url(url)
uri = URI(url) rescue nil uri =
begin
URI(url)
rescue StandardError
nil
end
return Discourse.current_hostname unless uri return Discourse.current_hostname unless uri
return uri.to_s if uri.scheme == "mailto" return uri.to_s if uri.scheme == "mailto"
uri.host = Discourse.current_hostname if !uri.host uri.host = Discourse.current_hostname if !uri.host
uri.scheme = (SiteSetting.force_https ? 'https' : 'http') if !uri.scheme uri.scheme = (SiteSetting.force_https ? "https" : "http") if !uri.scheme
uri.to_s uri.to_s
end end
end end

View File

@ -13,18 +13,19 @@ module DiscourseChatIntegration::Provider::SlackProvider
PROVIDER_ENABLED_SETTING = :chat_integration_slack_enabled PROVIDER_ENABLED_SETTING = :chat_integration_slack_enabled
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]?\S*$', unique: true }]
{ key: "identifier", regex: '^[@#]?\S*$', unique: true }
]
require_dependency 'topic' require_dependency "topic"
::Topic.register_custom_field_type(DiscourseChatIntegration::Provider::SlackProvider::THREAD_LEGACY, :string) ::Topic.register_custom_field_type(
DiscourseChatIntegration::Provider::SlackProvider::THREAD_LEGACY,
:string,
)
def self.excerpt(post, max_length = SiteSetting.chat_integration_slack_excerpt_length) def self.excerpt(post, max_length = SiteSetting.chat_integration_slack_excerpt_length)
doc = Nokogiri::HTML5.fragment(post.excerpt(max_length, doc =
remap_emoji: true, Nokogiri::HTML5.fragment(
keep_onebox_source: true post.excerpt(max_length, remap_emoji: true, keep_onebox_source: true),
)) )
SlackMessageFormatter.format(doc.to_html) SlackMessageFormatter.format(doc.to_html)
end end
@ -34,11 +35,18 @@ module DiscourseChatIntegration::Provider::SlackProvider
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]" category = "[#{I18n.t("uncategorized_category_name")}]"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
icon_url = icon_url =
@ -55,12 +63,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
SiteSetting.title || "Discourse" SiteSetting.title || "Discourse"
end end
message = { message = { channel: channel, username: slack_username, icon_url: icon_url, attachments: [] }
channel: channel,
username: slack_username,
icon_url: icon_url,
attachments: []
}
if filter == "thread" && thread_ts = get_slack_thread_ts(topic, channel) if filter == "thread" && thread_ts = get_slack_thread_ts(topic, channel)
message[:thread_ts] = thread_ts message[:thread_ts] = thread_ts
@ -73,9 +76,10 @@ module DiscourseChatIntegration::Provider::SlackProvider
color: topic.category ? "##{topic.category.color}" : nil, color: topic.category ? "##{topic.category.color}" : nil,
text: excerpt(post), text: excerpt(post),
mrkdwn_in: ["text"], mrkdwn_in: ["text"],
title: "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}", title:
"#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
title_link: post.full_url, title_link: post.full_url,
thumb_url: post.full_url thumb_url: post.full_url,
} }
message[:attachments].push(summary) message[:attachments].push(summary)
@ -92,14 +96,14 @@ module DiscourseChatIntegration::Provider::SlackProvider
# <!--SLACK_CHANNEL_ID=#{@channel_id};SLACK_TS=#{@requested_thread_ts}--> # <!--SLACK_CHANNEL_ID=#{@channel_id};SLACK_TS=#{@requested_thread_ts}-->
slack_thread_regex = /<!--SLACK_CHANNEL_ID=([^;.]+);SLACK_TS=([0-9]{10}.[0-9]{6})-->/ slack_thread_regex = /<!--SLACK_CHANNEL_ID=([^;.]+);SLACK_TS=([0-9]{10}.[0-9]{6})-->/
req = Net::HTTP::Post.new(URI('https://slack.com/api/chat.postMessage')) req = Net::HTTP::Post.new(URI("https://slack.com/api/chat.postMessage"))
data = { data = {
token: SiteSetting.chat_integration_slack_access_token, token: SiteSetting.chat_integration_slack_access_token,
username: message[:username], username: message[:username],
icon_url: message[:icon_url], icon_url: message[:icon_url],
channel: message[:channel].gsub('#', ''), channel: message[:channel].gsub("#", ""),
attachments: message[:attachments].to_json attachments: message[:attachments].to_json,
} }
if post if post
@ -116,18 +120,28 @@ module DiscourseChatIntegration::Provider::SlackProvider
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
raise ::DiscourseChatIntegration::ProviderError.new info: { request: uri, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
request: uri,
response_code: response.code,
response_body: response.body,
}
end end
json = JSON.parse(response.body) json = JSON.parse(response.body)
unless json["ok"] == true unless json["ok"] == true
if json.key?("error") && (json["error"] == ('channel_not_found') || json["error"] == ('is_archived')) if json.key?("error") &&
error_key = 'chat_integration.provider.slack.errors.channel_not_found' (json["error"] == ("channel_not_found") || json["error"] == ("is_archived"))
error_key = "chat_integration.provider.slack.errors.channel_not_found"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, request: uri, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: uri,
response_code: response.code,
response_body: response.body,
}
end end
ts = json["ts"] ts = json["ts"]
@ -139,25 +153,33 @@ module DiscourseChatIntegration::Provider::SlackProvider
def self.send_via_webhook(message) def self.send_via_webhook(message)
http = FinalDestination::HTTP.new("hooks.slack.com", 443) http = FinalDestination::HTTP.new("hooks.slack.com", 443)
http.use_ssl = true http.use_ssl = true
req = Net::HTTP::Post.new(URI(SiteSetting.chat_integration_slack_outbound_webhook_url), 'Content-Type' => 'application/json') req =
Net::HTTP::Post.new(
URI(SiteSetting.chat_integration_slack_outbound_webhook_url),
"Content-Type" => "application/json",
)
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
if response.code.to_s == '403' if response.code.to_s == "403"
error_key = 'chat_integration.provider.slack.errors.action_prohibited' error_key = "chat_integration.provider.slack.errors.action_prohibited"
elsif response.body == ('channel_not_found') || response.body == ('channel_is_archived') elsif response.body == ("channel_not_found") || response.body == ("channel_is_archived")
error_key = 'chat_integration.provider.slack.errors.channel_not_found' error_key = "chat_integration.provider.slack.errors.channel_not_found"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, request: req.body, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
end end
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
channel_id = channel.data['identifier'] channel_id = channel.data["identifier"]
filter = rule.nil? ? "" : rule.filter filter = rule.nil? ? "" : rule.filter
message = slack_message(post, channel_id, filter) message = slack_message(post, channel_id, filter)
@ -166,7 +188,6 @@ module DiscourseChatIntegration::Provider::SlackProvider
else else
self.send_via_api(post, channel_id, message) self.send_via_api(post, channel_id, message)
end end
end end
def self.slack_api_http def self.slack_api_http
@ -182,13 +203,16 @@ module DiscourseChatIntegration::Provider::SlackProvider
end end
def self.set_slack_thread_ts(topic, channel, value) def self.set_slack_thread_ts(topic, channel, value)
TopicCustomField.upsert({ TopicCustomField.upsert(
{
topic_id: topic.id, topic_id: topic.id,
name: "#{THREAD_CUSTOM_FIELD_PREFIX}#{channel}", name: "#{THREAD_CUSTOM_FIELD_PREFIX}#{channel}",
value: value, value: value,
created_at: Time.zone.now, created_at: Time.zone.now,
updated_at: Time.zone.now updated_at: Time.zone.now,
}, unique_by: [:topic_id, :name]) },
unique_by: %i[topic_id name],
)
end end
end end

View File

@ -2,7 +2,8 @@
module DiscourseChatIntegration::Provider::SlackProvider module DiscourseChatIntegration::Provider::SlackProvider
class SlackTranscript class SlackTranscript
class UserFetchError < RuntimeError; end class UserFetchError < RuntimeError
end
attr_reader :users, :channel_id, :messages attr_reader :users, :channel_id, :messages
@ -42,17 +43,14 @@ module DiscourseChatIntegration::Provider::SlackProvider
# Work through the messages in order. If a gap is found, this could be the first message # Work through the messages in order. If a gap is found, this could be the first message
new_first_message_index = nil new_first_message_index = nil
previous_message_ts = @messages[-skip_messages].ts.split('.').first.to_i previous_message_ts = @messages[-skip_messages].ts.split(".").first.to_i
possible_first_messages.each_with_index do |message, index| possible_first_messages.each_with_index do |message, index|
# Calculate the time since the last message # Calculate the time since the last message
this_ts = message.ts.split('.').first.to_i this_ts = message.ts.split(".").first.to_i
time_since_previous_message = this_ts - previous_message_ts time_since_previous_message = this_ts - previous_message_ts
# If greater than 3 minutes, this could be the first message # If greater than 3 minutes, this could be the first message
if time_since_previous_message > 3.minutes new_first_message_index = index if time_since_previous_message > 3.minutes
new_first_message_index = index
end
previous_message_ts = this_ts previous_message_ts = this_ts
end end
@ -84,11 +82,11 @@ module DiscourseChatIntegration::Provider::SlackProvider
def build_transcript def build_transcript
post_content = +"" post_content = +""
post_content << "[quote]\n" if SiteSetting.chat_integration_slack_transcript_quote post_content << "[quote]\n" if SiteSetting.chat_integration_slack_transcript_quote
post_content << "[**#{I18n.t('chat_integration.provider.slack.transcript.view_on_slack', name: @channel_name)}**](#{first_message.url})\n" post_content << "[**#{I18n.t("chat_integration.provider.slack.transcript.view_on_slack", name: @channel_name)}**](#{first_message.url})\n"
all_avatars = {} all_avatars = {}
last_username = '' last_username = ""
transcript_messages = @messages[@first_message_index..@last_message_index] transcript_messages = @messages[@first_message_index..@last_message_index]
@ -108,9 +106,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
post_content << m.text post_content << m.text
m.attachments.each do |attachment| m.attachments.each { |attachment| post_content << "\n> #{attachment}\n" }
post_content << "\n> #{attachment}\n"
end
post_content << "\n" post_content << "\n"
end end
@ -118,9 +114,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
post_content << "[/quote]" if SiteSetting.chat_integration_slack_transcript_quote post_content << "[/quote]" if SiteSetting.chat_integration_slack_transcript_quote
post_content << "\n\n" post_content << "\n\n"
all_avatars.each do |username, url| all_avatars.each { |username, url| post_content << "[#{username}]: #{url}\n" }
post_content << "[#{username}]: #{url}\n"
end
if not @requested_thread_ts.nil? if not @requested_thread_ts.nil?
post_content << "<!--SLACK_CHANNEL_ID=#{@channel_name};SLACK_TS=#{@requested_thread_ts}-->" post_content << "<!--SLACK_CHANNEL_ID=#{@channel_name};SLACK_TS=#{@requested_thread_ts}-->"
@ -134,17 +128,17 @@ module DiscourseChatIntegration::Provider::SlackProvider
type: "modal", type: "modal",
title: { title: {
type: "plain_text", type: "plain_text",
text: I18n.t("chat_integration.provider.slack.transcript.modal_title") text: I18n.t("chat_integration.provider.slack.transcript.modal_title"),
}, },
blocks: [ blocks: [
{ {
"type": "section", type: "section",
"text": { text: {
"type": "mrkdwn", type: "mrkdwn",
"text": I18n.t("chat_integration.provider.slack.transcript.modal_description") text: I18n.t("chat_integration.provider.slack.transcript.modal_description"),
} },
} },
] ],
} }
if @messages if @messages
@ -153,30 +147,31 @@ module DiscourseChatIntegration::Provider::SlackProvider
link = "#{Discourse.base_url}/chat-transcript/#{secret}" link = "#{Discourse.base_url}/chat-transcript/#{secret}"
data[:blocks] << { data[:blocks] << {
"type": "section", type: "section",
"text": { text: {
"type": "mrkdwn", type: "mrkdwn",
"text": ":writing_hand: *#{I18n.t("chat_integration.provider.slack.transcript.transcript_ready")}*" text:
":writing_hand: *#{I18n.t("chat_integration.provider.slack.transcript.transcript_ready")}*",
}, },
"accessory": { accessory: {
"type": "button", type: "button",
"text": { text: {
"type": "plain_text", type: "plain_text",
"text": I18n.t("chat_integration.provider.slack.transcript.continue_on_discourse"), text: I18n.t("chat_integration.provider.slack.transcript.continue_on_discourse"),
"emoji": true emoji: true,
}, },
"style": "primary", style: "primary",
"url": link, url: link,
"action_id": "null_action" action_id: "null_action",
} },
} }
else else
data[:blocks] << { data[:blocks] << {
"type": "section", type: "section",
"text": { text: {
"type": "mrkdwn", type: "mrkdwn",
"text": ":writing_hand: #{I18n.t("chat_integration.provider.slack.transcript.loading")}" text: ":writing_hand: #{I18n.t("chat_integration.provider.slack.transcript.loading")}",
} },
} }
end end
@ -188,76 +183,81 @@ module DiscourseChatIntegration::Provider::SlackProvider
secret = DiscourseChatIntegration::Helper.save_transcript(post_content) secret = DiscourseChatIntegration::Helper.save_transcript(post_content)
link = "#{Discourse.base_url}/chat-transcript/#{secret}" link = "#{Discourse.base_url}/chat-transcript/#{secret}"
return { text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>" } if @requested_thread_ts if @requested_thread_ts
return(
{
text:
"<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>",
}
)
end
{ {
text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>", text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>",
attachments: [ attachments: [
{ {
pretext: I18n.t( pretext:
"chat_integration.provider.slack.transcript.first_message_pretext", I18n.t(
n: @messages.length - first_message_number "chat_integration.provider.slack.transcript.first_message_pretext",
), n: @messages.length - first_message_number,
),
fallback: "#{first_message.username} - #{first_message.raw_text}", fallback: "#{first_message.username} - #{first_message.raw_text}",
color: "#007AB8", color: "#007AB8",
author_name: first_message.username, author_name: first_message.username,
author_icon: first_message.avatar, author_icon: first_message.avatar,
text: first_message.raw_text, text: first_message.raw_text,
footer: I18n.t( footer:
"chat_integration.provider.slack.transcript.posted_in", I18n.t("chat_integration.provider.slack.transcript.posted_in", name: @channel_name),
name: @channel_name
),
ts: first_message.ts, ts: first_message.ts,
callback_id: last_message.ts, callback_id: last_message.ts,
actions: [ actions: [
{ {
name: "first_message", name: "first_message",
text: I18n.t( text: I18n.t("chat_integration.provider.slack.transcript.change_first_message"),
"chat_integration.provider.slack.transcript.change_first_message"
),
type: "select", type: "select",
options: first_message_options = @messages[ [(first_message_number - 20), 0].max .. last_message_number] options:
.map { |m| { text: "#{m.username}: #{m.processed_text_with_attachments}", value: m.ts } } first_message_options =
} @messages[[(first_message_number - 20), 0].max..last_message_number].map do |m|
{ text: "#{m.username}: #{m.processed_text_with_attachments}", value: m.ts }
end,
},
], ],
}, },
{ {
pretext: I18n.t( pretext:
"chat_integration.provider.slack.transcript.last_message_pretext", I18n.t(
n: @messages.length - last_message_number "chat_integration.provider.slack.transcript.last_message_pretext",
), n: @messages.length - last_message_number,
),
fallback: "#{last_message.username} - #{last_message.raw_text}", fallback: "#{last_message.username} - #{last_message.raw_text}",
color: "#007AB8", color: "#007AB8",
author_name: last_message.username, author_name: last_message.username,
author_icon: last_message.avatar, author_icon: last_message.avatar,
text: last_message.raw_text, text: last_message.raw_text,
footer: I18n.t( footer:
"chat_integration.provider.slack.transcript.posted_in", I18n.t("chat_integration.provider.slack.transcript.posted_in", name: @channel_name),
name: @channel_name
),
ts: last_message.ts, ts: last_message.ts,
callback_id: first_message.ts, callback_id: first_message.ts,
actions: [ actions: [
{ {
name: "last_message", name: "last_message",
text: I18n.t( text: I18n.t("chat_integration.provider.slack.transcript.change_last_message"),
"chat_integration.provider.slack.transcript.change_last_message"
),
type: "select", type: "select",
options: @messages[first_message_number..(last_message_number + 20)] options:
.map { |m| { text: "#{m.username}: #{m.processed_text_with_attachments}", value: m.ts } } @messages[first_message_number..(last_message_number + 20)].map do |m|
} { text: "#{m.username}: #{m.processed_text_with_attachments}", value: m.ts }
end,
},
], ],
} },
] ],
} }
end end
def load_user_data def load_user_data
key = "slack_user_info_#{Digest::SHA1.hexdigest(SiteSetting.chat_integration_slack_access_token)}" key =
@users = Discourse.cache.fetch(key, expires_in: 10.minutes) do "slack_user_info_#{Digest::SHA1.hexdigest(SiteSetting.chat_integration_slack_access_token)}"
fetch_user_data @users = Discourse.cache.fetch(key, expires_in: 10.minutes) { fetch_user_data }
end
true true
rescue UserFetchError rescue UserFetchError
false false
@ -267,26 +267,30 @@ module DiscourseChatIntegration::Provider::SlackProvider
http = ::DiscourseChatIntegration::Provider::SlackProvider.slack_api_http http = ::DiscourseChatIntegration::Provider::SlackProvider.slack_api_http
cursor = nil cursor = nil
req = Net::HTTP::Post.new(URI('https://slack.com/api/users.list')) req = Net::HTTP::Post.new(URI("https://slack.com/api/users.list"))
users = {} users = {}
loop do loop do
break if cursor == "" break if cursor == ""
req.set_form_data(token: SiteSetting.chat_integration_slack_access_token, limit: 200, cursor: cursor) req.set_form_data(
token: SiteSetting.chat_integration_slack_access_token,
limit: 200,
cursor: cursor,
)
response = http.request(req) response = http.request(req)
raise UserFetchError.new unless response.kind_of? Net::HTTPSuccess raise UserFetchError.new unless response.kind_of? Net::HTTPSuccess
json = JSON.parse(response.body) json = JSON.parse(response.body)
raise UserFetchError.new unless json['ok'] raise UserFetchError.new unless json["ok"]
cursor = json['response_metadata']['next_cursor'] cursor = json["response_metadata"]["next_cursor"]
json['members'].each do |user| json["members"].each do |user|
# Slack uses display_name and falls back to real_name if it is not set # Slack uses display_name and falls back to real_name if it is not set
if user['profile']['display_name'].blank? if user["profile"]["display_name"].blank?
user['_transcript_username'] = user['profile']['real_name'] user["_transcript_username"] = user["profile"]["real_name"]
else else
user['_transcript_username'] = user['profile']['display_name'] user["_transcript_username"] = user["profile"]["display_name"]
end end
user['_transcript_username'] = user['_transcript_username'].gsub(' ', '_') user["_transcript_username"] = user["_transcript_username"].gsub(" ", "_")
users[user['id']] = user users[user["id"]] = user
end end
end end
users users
@ -302,7 +306,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
data = { data = {
token: SiteSetting.chat_integration_slack_access_token, token: SiteSetting.chat_integration_slack_access_token,
channel: @channel_id, channel: @channel_id,
limit: count limit: count,
} }
data[:ts] = @requested_thread_ts if @requested_thread_ts data[:ts] = @requested_thread_ts if @requested_thread_ts
@ -311,9 +315,9 @@ module DiscourseChatIntegration::Provider::SlackProvider
response = http.request(req) response = http.request(req)
return false unless response.kind_of? Net::HTTPSuccess return false unless response.kind_of? Net::HTTPSuccess
json = JSON.parse(response.body) json = JSON.parse(response.body)
return false unless json['ok'] return false unless json["ok"]
raw_messages = json['messages'] raw_messages = json["messages"]
raw_messages = raw_messages.reverse unless @requested_thread_ts raw_messages = raw_messages.reverse unless @requested_thread_ts
# Build some message objects # Build some message objects
@ -323,13 +327,13 @@ module DiscourseChatIntegration::Provider::SlackProvider
next unless message["type"] == "message" next unless message["type"] == "message"
# Don't load responses to threads unless specifically requested (if ts==thread_ts then it's the thread parent) # Don't load responses to threads unless specifically requested (if ts==thread_ts then it's the thread parent)
next if !@requested_thread_ts && message["thread_ts"] && message["thread_ts"] != message["ts"] if !@requested_thread_ts && message["thread_ts"] && message["thread_ts"] != message["ts"]
next
end
this_message = SlackMessage.new(message, self) this_message = SlackMessage.new(message, self)
@messages << this_message @messages << this_message
end end
end end
end end
end end

View File

@ -5,29 +5,33 @@ module DiscourseChatIntegration::Provider::TeamsProvider
PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled
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 },
] ]
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
message = get_message(post) message = get_message(post)
uri = URI(channel.data['webhook_url']) uri = URI(channel.data["webhook_url"])
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
if response.body.include?('Invalid webhook URL') if response.body.include?("Invalid webhook URL")
error_key = 'chat_integration.provider.teams.errors.invalid_channel' error_key = "chat_integration.provider.teams.errors.invalid_channel"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, request: req.body, response_code: response.code, response_body: response.body } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: req.body,
response_code: response.code,
response_body: response.body,
}
end end
end end
def self.get_message(post) def self.get_message(post)
@ -41,29 +45,41 @@ module DiscourseChatIntegration::Provider::TeamsProvider
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]" category = "[#{I18n.t("uncategorized_category_name")}]"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
message = { message = {
"@type": "MessageCard", "@type": "MessageCard",
"summary": topic.title, summary: topic.title,
"sections": [{ sections: [
"activityTitle": "[#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}](#{post.full_url})", {
"activitySubtitle": post.excerpt(SiteSetting.chat_integration_teams_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), activityTitle:
"activityImage": post.user.small_avatar_url, "[#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}](#{post.full_url})",
"facts": [{ activitySubtitle:
"name": full_name, post.excerpt(
"value": display_name SiteSetting.chat_integration_teams_excerpt_length,
}], text_entities: true,
"markdown": true strip_links: true,
}], remap_emoji: true,
),
activityImage: post.user.small_avatar_url,
facts: [{ name: full_name, value: display_name }],
markdown: true,
},
],
} }
message message
end end
end end

View File

@ -13,12 +13,11 @@ module DiscourseChatIntegration::Provider::TelegramProvider
only: :command only: :command
def command def command
# If it's a new message (telegram also sends hooks for other reasons that we don't care about) # If it's a new message (telegram also sends hooks for other reasons that we don't care about)
if params.key?('message') if params.key?("message")
chat_id = params['message']['chat']['id'] chat_id = params["message"]["chat"]["id"]
message_text = process_command(params['message']) message_text = process_command(params["message"])
if message_text.present? if message_text.present?
message = { message = {
@ -30,15 +29,15 @@ module DiscourseChatIntegration::Provider::TelegramProvider
DiscourseChatIntegration::Provider::TelegramProvider.sendMessage(message) DiscourseChatIntegration::Provider::TelegramProvider.sendMessage(message)
end end
elsif params.dig("channel_post", "text")&.include?("/getchatid")
chat_id = params["channel_post"]["chat"]["id"]
elsif params.dig('channel_post', 'text')&.include?('/getchatid') message_text =
chat_id = params['channel_post']['chat']['id'] I18n.t(
"chat_integration.provider.telegram.unknown_chat",
message_text = I18n.t( site_title: CGI.escapeHTML(SiteSetting.title),
"chat_integration.provider.telegram.unknown_chat", chat_id: chat_id,
site_title: CGI::escapeHTML(SiteSetting.title), )
chat_id: chat_id,
)
message = { message = {
chat_id: chat_id, chat_id: chat_id,
@ -51,43 +50,49 @@ module DiscourseChatIntegration::Provider::TelegramProvider
end end
# Always give telegram a success message, otherwise we'll stop receiving webhooks # Always give telegram a success message, otherwise we'll stop receiving webhooks
data = { data = { success: true }
success: true
}
render json: data render json: data
end end
def process_command(message) def process_command(message)
return unless message['text'] # No command to be processed return unless message["text"] # No command to be processed
chat_id = params['message']['chat']['id'] chat_id = params["message"]["chat"]["id"]
provider = DiscourseChatIntegration::Provider::TelegramProvider::PROVIDER_NAME provider = DiscourseChatIntegration::Provider::TelegramProvider::PROVIDER_NAME
channel = DiscourseChatIntegration::Channel.with_provider(provider).with_data_value('chat_id', chat_id).first channel =
DiscourseChatIntegration::Channel
.with_provider(provider)
.with_data_value("chat_id", chat_id)
.first
text_key = if channel.nil? text_key =
"unknown_chat" if channel.nil?
elsif !SiteSetting.chat_integration_telegram_enable_slash_commands || !message['text'].start_with?('/') "unknown_chat"
"silent" elsif !SiteSetting.chat_integration_telegram_enable_slash_commands ||
else !message["text"].start_with?("/")
"" "silent"
end else
""
end
return "" if text_key == "silent" return "" if text_key == "silent"
if text_key.present? if text_key.present?
return I18n.t( return(
"chat_integration.provider.telegram.#{text_key}", I18n.t(
site_title: CGI::escapeHTML(SiteSetting.title), "chat_integration.provider.telegram.#{text_key}",
chat_id: chat_id, site_title: CGI.escapeHTML(SiteSetting.title),
chat_id: chat_id,
)
) )
end end
tokens = message['text'].split(" ") tokens = message["text"].split(" ")
tokens[0][0] = '' # Remove the slash from the first token tokens[0][0] = "" # Remove the slash from the first token
tokens[0] = tokens[0].split('@')[0] # Remove the bot name from the command (necessary for group chats) tokens[0] = tokens[0].split("@")[0] # Remove the bot name from the command (necessary for group chats)
::DiscourseChatIntegration::Helper.process_command(channel, tokens) ::DiscourseChatIntegration::Helper.process_command(channel, tokens)
end end
@ -96,8 +101,7 @@ module DiscourseChatIntegration::Provider::TelegramProvider
params.require(:token) params.require(:token)
if SiteSetting.chat_integration_telegram_secret.blank? || if SiteSetting.chat_integration_telegram_secret.blank? ||
SiteSetting.chat_integration_telegram_secret != params[:token] SiteSetting.chat_integration_telegram_secret != params[:token]
raise Discourse::InvalidAccess.new raise Discourse::InvalidAccess.new
end end
end end
@ -108,7 +112,5 @@ module DiscourseChatIntegration::Provider::TelegramProvider
isolate_namespace DiscourseChatIntegration::Provider::TelegramProvider isolate_namespace DiscourseChatIntegration::Provider::TelegramProvider
end end
TelegramEngine.routes.draw do TelegramEngine.routes.draw { post "command/:token" => "telegram_command#command" }
post "command/:token" => "telegram_command#command"
end
end end

View File

@ -2,7 +2,7 @@
DiscourseEvent.on(:site_setting_changed) do |setting_name, old_value, new_value| DiscourseEvent.on(:site_setting_changed) do |setting_name, old_value, new_value|
isEnabledSetting = setting_name == :chat_integration_telegram_enabled isEnabledSetting = setting_name == :chat_integration_telegram_enabled
isAccessToken = setting_name == :chat_integration_telegram_access_token isAccessToken = setting_name == :chat_integration_telegram_access_token
if (isEnabledSetting || isAccessToken) if (isEnabledSetting || isAccessToken)
enabled = isEnabledSetting ? new_value == true : SiteSetting.chat_integration_telegram_enabled enabled = isEnabledSetting ? new_value == true : SiteSetting.chat_integration_telegram_enabled

View File

@ -6,30 +6,30 @@ module DiscourseChatIntegration
PROVIDER_NAME = "telegram".freeze PROVIDER_NAME = "telegram".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled
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 },
] ]
def self.setup_webhook def self.setup_webhook
newSecret = SecureRandom.hex newSecret = SecureRandom.hex
SiteSetting.chat_integration_telegram_secret = newSecret SiteSetting.chat_integration_telegram_secret = newSecret
message = { message = { url: Discourse.base_url + "/chat-integration/telegram/command/" + newSecret }
url: Discourse.base_url + '/chat-integration/telegram/command/' + newSecret,
}
response = self.do_api_request('setWebhook', message) response = self.do_api_request("setWebhook", message)
if response['ok'] != true if response["ok"] != true
# If setting up webhook failed, disable provider # If setting up webhook failed, disable provider
SiteSetting.chat_integration_telegram_enabled = false SiteSetting.chat_integration_telegram_enabled = false
Rails.logger.error("Failed to setup telegram webhook. Message data= " + message.to_json + " response=" + response.to_json) Rails.logger.error(
"Failed to setup telegram webhook. Message data= " + message.to_json + " response=" +
response.to_json,
)
end end
end end
def self.sendMessage(message) def self.sendMessage(message)
self.do_api_request('sendMessage', message) self.do_api_request("sendMessage", message)
end end
def self.do_api_request(methodName, message) def self.do_api_request(methodName, message)
@ -40,7 +40,7 @@ module DiscourseChatIntegration
uri = URI("https://api.telegram.org/bot#{access_token}/#{methodName}") uri = URI("https://api.telegram.org/bot#{access_token}/#{methodName}")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
@ -54,28 +54,38 @@ module DiscourseChatIntegration
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category if topic.category
category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" category =
(
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
tags = '' tags = ""
if topic.tags.present? tags = topic.tags.map(&:name).join(", ") if topic.tags.present?
tags = topic.tags.map(&:name).join(', ')
end
I18n.t( I18n.t(
"chat_integration.provider.telegram.message", "chat_integration.provider.telegram.message",
user: display_name, user: display_name,
post_url: post.full_url, post_url: post.full_url,
title: CGI::escapeHTML(topic.title), title: CGI.escapeHTML(topic.title),
post_excerpt: post.excerpt(SiteSetting.chat_integration_telegram_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true), post_excerpt:
) post.excerpt(
SiteSetting.chat_integration_telegram_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
)
end end
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
chat_id = channel.data['chat_id'] chat_id = channel.data["chat_id"]
message = { message = {
chat_id: chat_id, chat_id: chat_id,
@ -86,18 +96,20 @@ module DiscourseChatIntegration
response = sendMessage(message) response = sendMessage(message)
if response['ok'] != true if response["ok"] != true
error_key = nil error_key = nil
if response['description'].include? 'chat not found' if response["description"].include? "chat not found"
error_key = 'chat_integration.provider.telegram.errors.channel_not_found' error_key = "chat_integration.provider.telegram.errors.channel_not_found"
elsif response['description'].include? 'Forbidden' elsif response["description"].include? "Forbidden"
error_key = 'chat_integration.provider.telegram.errors.forbidden' error_key = "chat_integration.provider.telegram.errors.forbidden"
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, message: message, response_body: response } raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
message: message,
response_body: response,
}
end end
end end
end end
end end
end end

View File

@ -5,35 +5,38 @@ module DiscourseChatIntegration::Provider::WebexProvider
PROVIDER_ENABLED_SETTING = :chat_integration_webex_enabled PROVIDER_ENABLED_SETTING = :chat_integration_webex_enabled
CHANNEL_PARAMETERS = [ CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true }, { key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url", {
key: "webhook_url",
regex: '^https:\/\/webexapis\.com\/v1\/webhooks\/incoming\/[A-Za-z0-9\-@\/]+\S+$', regex: '^https:\/\/webexapis\.com\/v1\/webhooks\/incoming\/[A-Za-z0-9\-@\/]+\S+$',
unique: true, unique: true,
hidden: true } hidden: true,
},
] ]
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
message = get_message(post) message = get_message(post)
uri = URI(channel.data['webhook_url']) uri = URI(channel.data["webhook_url"])
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json req.body = message.to_json
response = http.request(req) response = http.request(req)
unless response.kind_of? Net::HTTPSuccess unless response.kind_of? Net::HTTPSuccess
if response.body.include?('Invalid webhook URL') if response.body.include?("Invalid webhook URL")
error_key = 'chat_integration.provider.webex.errors.invalid_channel' error_key = "chat_integration.provider.webex.errors.invalid_channel"
else else
error_key = nil error_key = nil
end end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, raise ::DiscourseChatIntegration::ProviderError.new info: {
request: req.body, error_key: error_key,
response_code: response.code, request: req.body,
response_body: response.body } response_code: response.code,
response_body: response.body,
}
end end
end end
def self.get_message(post) def self.get_message(post)
@ -41,24 +44,31 @@ module DiscourseChatIntegration::Provider::WebexProvider
topic = post.topic topic = post.topic
category = '' category = ""
if topic.category&.uncategorized? if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]" category = "[#{I18n.t("uncategorized_category_name")}]"
elsif topic.category elsif topic.category
category = (topic.category.parent_category) ? category =
"[#{topic.category.parent_category.name}/#{topic.category.name}]" : "[#{topic.category.name}]" (
if (topic.category.parent_category)
"[#{topic.category.parent_category.name}/#{topic.category.name}]"
else
"[#{topic.category.name}]"
end
)
end end
markdown = "**#{topic.title}**: #{category}" markdown = "**#{topic.title}**: #{category}"
markdown += " #{topic.tags.map(&:name).join(', ')} " if topic.tags.present? markdown += " #{topic.tags.map(&:name).join(", ")} " if topic.tags.present?
markdown += "(#{post.full_url}) from #{display_name}:\n" markdown += "(#{post.full_url}) from #{display_name}:\n"
markdown += post.excerpt(SiteSetting.chat_integration_webex_excerpt_length, markdown +=
text_entities: true, post.excerpt(
strip_links: true, SiteSetting.chat_integration_webex_excerpt_length,
remap_emoji: true text_entities: true,
) strip_links: true,
remap_emoji: true,
)
{ "markdown": markdown } { markdown: markdown }
end end
end end

View File

@ -6,18 +6,21 @@ module DiscourseChatIntegration
PROVIDER_NAME = "zulip".freeze PROVIDER_NAME = "zulip".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_zulip_enabled PROVIDER_ENABLED_SETTING = :chat_integration_zulip_enabled
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+' },
] ]
def self.send_message(message) def self.send_message(message)
uri = URI("#{SiteSetting.chat_integration_zulip_server}/api/v1/messages") uri = URI("#{SiteSetting.chat_integration_zulip_server}/api/v1/messages")
http = FinalDestination::HTTP.new(uri.host, uri.port) http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https') http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri) req = Net::HTTP::Post.new(uri)
req.basic_auth(SiteSetting.chat_integration_zulip_bot_email_address, SiteSetting.chat_integration_zulip_bot_api_key) req.basic_auth(
SiteSetting.chat_integration_zulip_bot_email_address,
SiteSetting.chat_integration_zulip_bot_api_key,
)
req.set_form_data(message) req.set_form_data(message)
response = http.request(req) response = http.request(req)
@ -28,23 +31,27 @@ module DiscourseChatIntegration
def self.generate_zulip_message(post, stream, subject) def self.generate_zulip_message(post, stream, subject)
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user) display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
message = I18n.t('chat_integration.provider.zulip.message', user: display_name, message =
post_url: post.full_url, I18n.t(
title: post.topic.title, "chat_integration.provider.zulip.message",
excerpt: post.excerpt(SiteSetting.chat_integration_zulip_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true)) user: display_name,
post_url: post.full_url,
title: post.topic.title,
excerpt:
post.excerpt(
SiteSetting.chat_integration_zulip_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
)
data = { data = { type: "stream", to: stream, subject: subject, content: message }
type: 'stream',
to: stream,
subject: subject,
content: message
}
end end
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
stream = channel.data["stream"]
stream = channel.data['stream'] subject = channel.data["subject"]
subject = channel.data['subject']
message = self.generate_zulip_message(post, stream, subject) message = self.generate_zulip_message(post, stream, subject)
@ -52,12 +59,18 @@ module DiscourseChatIntegration
if !response.kind_of?(Net::HTTPSuccess) if !response.kind_of?(Net::HTTPSuccess)
error_key = nil error_key = nil
error_key = 'chat_integration.provider.zulip.errors.does_not_exist' if response.body.include?('does not exist') error_key =
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key, message: message, response_code: response.code, response_body: response.body } "chat_integration.provider.zulip.errors.does_not_exist" if response.body.include?(
"does not exist",
)
raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
message: message,
response_code: response.code,
response_body: response.body,
}
end end
end end
end end
end end
end end

View File

@ -26,7 +26,7 @@ after_initialize do
Jobs.enqueue_in(time, :notify_chats, post_id: post.id) Jobs.enqueue_in(time, :notify_chats, post_id: post.id)
end end
add_admin_route 'chat_integration.menu_title', 'chat-integration' add_admin_route "chat_integration.menu_title", "chat-integration"
AdminDashboardData.add_problem_check do AdminDashboardData.add_problem_check do
next if !SiteSetting.chat_integration_enabled next if !SiteSetting.chat_integration_enabled
@ -47,7 +47,7 @@ after_initialize do
DiscourseChatIntegration::Provider.mount_engines DiscourseChatIntegration::Provider.mount_engines
if defined?(DiscourseAutomation) if defined?(DiscourseAutomation)
add_automation_scriptable('send_slack_message') do add_automation_scriptable("send_slack_message") do
field :message, component: :message, required: true, accepts_placeholders: true field :message, component: :message, required: true, accepts_placeholders: true
field :url, component: :text, required: true field :url, component: :text, required: true
field :channel, component: :text, required: true field :channel, component: :text, required: true
@ -59,16 +59,24 @@ after_initialize do
script do |context, fields, automation| script do |context, fields, automation|
sender = Discourse.system_user sender = Discourse.system_user
content = fields.dig('message', 'value') content = fields.dig("message", "value")
url = fields.dig('url', 'value') url = fields.dig("url", "value")
full_content = "#{content} - #{url}" full_content = "#{content} - #{url}"
channel_name = fields.dig('channel', 'value') channel_name = fields.dig("channel", "value")
channel = DiscourseChatIntegration::Channel.new(provider: "slack", data: { identifier: "##{channel_name}" }) channel =
DiscourseChatIntegration::Channel.new(
provider: "slack",
data: {
identifier: "##{channel_name}",
},
)
icon_url = icon_url =
if SiteSetting.chat_integration_slack_icon_url.present? if SiteSetting.chat_integration_slack_icon_url.present?
"#{Discourse.base_url}#{SiteSetting.chat_integration_slack_icon_url}" "#{Discourse.base_url}#{SiteSetting.chat_integration_slack_icon_url}"
elsif (url = (SiteSetting.try(:site_logo_small_url) || SiteSetting.logo_small_url)).present? elsif (
url = (SiteSetting.try(:site_logo_small_url) || SiteSetting.logo_small_url)
).present?
"#{Discourse.base_url}#{url}" "#{Discourse.base_url}#{url}"
end end
@ -83,7 +91,7 @@ after_initialize do
channel: "##{channel_name}", channel: "##{channel_name}",
username: slack_username, username: slack_username,
icon_url: icon_url, icon_url: icon_url,
attachments: [] attachments: [],
} }
summary = { summary = {
@ -94,7 +102,7 @@ after_initialize do
mrkdwn_in: ["text"], mrkdwn_in: ["text"],
title: content.truncate(100), title: content.truncate(100),
title_link: url, title_link: url,
thumb_url: nil thumb_url: nil,
} }
message[:attachments].push(summary) message[:attachments].push(summary)

View File

@ -11,9 +11,7 @@ RSpec.shared_context "with dummy provider" do
@@raise_exception = nil @@raise_exception = nil
def self.trigger_notification(post, channel, rule) def self.trigger_notification(post, channel, rule)
if @@raise_exception raise @@raise_exception if @@raise_exception
raise @@raise_exception
end
@@sent_messages.push(post: post.id, channel: channel) @@sent_messages.push(post: post.id, channel: channel)
end end
@ -32,9 +30,7 @@ RSpec.shared_context "with dummy provider" do
end end
end end
after(:each) do after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :DummyProvider) }
::DiscourseChatIntegration::Provider.send(:remove_const, :DummyProvider)
end
let(:provider) { ::DiscourseChatIntegration::Provider::DummyProvider } let(:provider) { ::DiscourseChatIntegration::Provider::DummyProvider }
end end
@ -44,9 +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_PARAMETERS = [ CHANNEL_PARAMETERS = [{ key: "val", regex: '^\S+$', unique: true }]
{ key: "val", regex: '^\S+$', unique: true }
]
@@sent_messages = [] @@sent_messages = []
@ -58,10 +52,7 @@ RSpec.shared_context "with validated dummy provider" do
@@sent_messages @@sent_messages
end end
end end
end end
after(:each) do after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider) }
::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider)
end
end end

View File

@ -1,229 +1,279 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_relative '../dummy_provider' require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Manager do RSpec.describe DiscourseChatIntegration::Manager do
include_context "with dummy provider" include_context "with dummy provider"
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') } let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') } let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:tag1) { Fabricate(:tag) } let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) } let(:tag2) { Fabricate(:tag) }
let(:tag3) { Fabricate(:tag) } let(:tag3) { Fabricate(:tag) }
describe '.process_command' do describe ".process_command" do
describe "add new rule" do
describe 'add new rule' do
# Not testing how filters are merged here, that's done in .smart_create_rule # Not testing how filters are merged here, that's done in .smart_create_rule
# We just want to make sure the commands are being interpretted correctly # We just want to make sure the commands are being interpretted correctly
it 'should add a new rule correctly' do it "should add a new rule correctly" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug]) response = DiscourseChatIntegration::Helper.process_command(chan1, ["watch", category.slug])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created")) expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created"))
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1) expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
it 'should work with all four filter types' do it "should work with all four filter types" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['thread', category.slug]) response =
DiscourseChatIntegration::Helper.process_command(chan1, ["thread", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq("thread")
response = DiscourseChatIntegration::Helper.process_command(chan1, ["watch", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq("watch")
response =
DiscourseChatIntegration::Helper.process_command(chan1, ["follow", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq("follow")
response = DiscourseChatIntegration::Helper.process_command(chan1, ["mute", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq("mute")
end
it "errors on incorrect categories" do
response = DiscourseChatIntegration::Helper.process_command(chan1, %w[watch blah])
expect(response).to eq(
I18n.t(
"chat_integration.provider.dummy.not_found.category",
name: "blah",
list: "uncategorized",
),
)
end
context "with tags enabled" do
before { SiteSetting.tagging_enabled = true }
it "should add a new tag rule correctly" do
response =
DiscourseChatIntegration::Helper.process_command(chan1, ["watch", "tag:#{tag1.name}"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created"))
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('thread') expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq("watch")
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug]) expect(rule.category_id).to eq(nil)
expect(rule.tags).to eq([tag1.name])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('watch')
response = DiscourseChatIntegration::Helper.process_command(chan1, ['follow', category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('follow')
response = DiscourseChatIntegration::Helper.process_command(chan1, ['mute', category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('mute')
end end
it 'errors on incorrect categories' do it "should work with a category and multiple tags" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', 'blah']) response =
DiscourseChatIntegration::Helper.process_command(
chan1,
["watch", category.slug, "tag:#{tag1.name}", "tag:#{tag2.name}"],
)
expect(response).to eq(I18n.t("chat_integration.provider.dummy.not_found.category", name: 'blah', list: 'uncategorized')) expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created"))
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id)
expect(rule.tags).to contain_exactly(tag1.name, tag2.name)
end end
context 'with tags enabled' do it "errors on incorrect tags" do
before do response =
SiteSetting.tagging_enabled = true DiscourseChatIntegration::Helper.process_command(
end chan1,
["watch", category.slug, "tag:blah"],
it 'should add a new tag rule correctly' do )
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', "tag:#{tag1.name}"]) expect(response).to eq(
I18n.t("chat_integration.provider.dummy.not_found.tag", name: "blah"),
expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created")) )
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch')
expect(rule.category_id).to eq(nil)
expect(rule.tags).to eq([tag1.name])
end
it 'should work with a category and multiple tags' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug, "tag:#{tag1.name}", "tag:#{tag2.name}"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created"))
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch')
expect(rule.category_id).to eq(category.id)
expect(rule.tags).to contain_exactly(tag1.name, tag2.name)
end
it 'errors on incorrect tags' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug, "tag:blah"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.not_found.tag", name: "blah"))
end
end end
end
end end
describe 'remove rule' do describe "remove rule" do
it 'removes the rule' do it "removes the rule" do
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1, rule1 =
filter: 'watch', DiscourseChatIntegration::Rule.create(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag2.name] filter: "watch",
) category_id: category.id,
tags: [tag1.name, tag2.name],
)
expect(DiscourseChatIntegration::Rule.all.size).to eq(1) expect(DiscourseChatIntegration::Rule.all.size).to eq(1)
response = DiscourseChatIntegration::Helper.process_command(chan1, ['remove', '1']) response = DiscourseChatIntegration::Helper.process_command(chan1, %w[remove 1])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.success")) expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.success"))
expect(DiscourseChatIntegration::Rule.all.size).to eq(0) expect(DiscourseChatIntegration::Rule.all.size).to eq(0)
end
it 'errors correctly' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['remove', '1'])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.error"))
end
end end
describe 'help command' do it "errors correctly" do
it 'should return the right response' do response = DiscourseChatIntegration::Helper.process_command(chan1, %w[remove 1])
response = DiscourseChatIntegration::Helper.process_command(chan1, ["help"]) expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.error"))
expect(response).to eq(I18n.t("chat_integration.provider.dummy.help"))
end
end end
end
describe 'status command' do describe "help command" do
it 'should return the right response' do it "should return the right response" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['status']) response = DiscourseChatIntegration::Helper.process_command(chan1, ["help"])
expect(response).to eq(DiscourseChatIntegration::Helper.status_for_channel(chan1)) expect(response).to eq(I18n.t("chat_integration.provider.dummy.help"))
end
end end
end
describe 'unknown command' do describe "status command" do
it 'should return the right response' do it "should return the right response" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['somerandomtext']) response = DiscourseChatIntegration::Helper.process_command(chan1, ["status"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.parse_error")) expect(response).to eq(DiscourseChatIntegration::Helper.status_for_channel(chan1))
end
end end
end
describe "unknown command" do
it "should return the right response" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ["somerandomtext"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.parse_error"))
end
end
end end
describe '.status_for_channel' do describe ".status_for_channel" do
context "with no rules" do
context 'with no rules' do it "includes the heading" do
it 'includes the heading' do
string = DiscourseChatIntegration::Helper.status_for_channel(chan1) string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string).to include('dummy.status.header') expect(string).to include("dummy.status.header")
end end
it 'includes the no_rules string' do it "includes the no_rules string" do
string = DiscourseChatIntegration::Helper.status_for_channel(chan1) string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string).to include('dummy.status.no_rules') expect(string).to include("dummy.status.no_rules")
end end
end end
context 'with some rules' do context "with some rules" do
let(:group) { Fabricate(:group) } let(:group) { Fabricate(:group) }
before do before do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id, tags: nil) DiscourseChatIntegration::Rule.create!(
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'mute', category_id: nil, tags: nil) channel: chan1,
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil, tags: [tag1.name]) filter: "watch",
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', type: 'group_message', group_id: group.id) category_id: category.id,
DiscourseChatIntegration::Rule.create!(channel: chan2, filter: 'watch', category_id: 1, tags: nil) tags: nil,
)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "mute",
category_id: nil,
tags: nil,
)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "follow",
category_id: nil,
tags: [tag1.name],
)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
type: "group_message",
group_id: group.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan2,
filter: "watch",
category_id: 1,
tags: nil,
)
SiteSetting.tagging_enabled = false SiteSetting.tagging_enabled = false
end end
it 'displays the correct rules' do it "displays the correct rules" do
string = DiscourseChatIntegration::Helper.status_for_channel(chan1) string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string.scan('status.rule_string').size).to eq(4) expect(string.scan("status.rule_string").size).to eq(4)
end end
it 'only displays tags for rules with tags' do it "only displays tags for rules with tags" do
string = DiscourseChatIntegration::Helper.status_for_channel(chan1) string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string.scan('rule_string_tags_suffix').size).to eq(0) expect(string.scan("rule_string_tags_suffix").size).to eq(0)
SiteSetting.tagging_enabled = true SiteSetting.tagging_enabled = true
string = DiscourseChatIntegration::Helper.status_for_channel(chan1) string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string.scan('rule_string_tags_suffix').size).to eq(1) expect(string.scan("rule_string_tags_suffix").size).to eq(1)
end end
end end
end end
describe '.delete_by_index' do describe ".delete_by_index" do
let(:category2) { Fabricate(:category) } let(:category2) { Fabricate(:category) }
let(:category3) { Fabricate(:category) } let(:category3) { Fabricate(:category) }
it 'deletes the correct rule' do it "deletes the correct rule" do
# Three identical rules, with different filters # Three identical rules, with different filters
# Status will be sorted by precedence # Status will be sorted by precedence
# be in this order # be in this order
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1, rule1 =
filter: 'mute', DiscourseChatIntegration::Rule.create(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag2.name] filter: "mute",
) category_id: category.id,
rule2 = DiscourseChatIntegration::Rule.create(channel: chan1, tags: [tag1.name, tag2.name],
filter: 'watch', )
category_id: category2.id, rule2 =
tags: [tag1.name, tag2.name] DiscourseChatIntegration::Rule.create(
) channel: chan1,
rule3 = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "watch",
filter: 'follow', category_id: category2.id,
category_id: category3.id, tags: [tag1.name, tag2.name],
tags: [tag1.name, tag2.name] )
) rule3 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "follow",
category_id: category3.id,
tags: [tag1.name, tag2.name],
)
expect(DiscourseChatIntegration::Rule.all.size).to eq(3) expect(DiscourseChatIntegration::Rule.all.size).to eq(3)
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 2)).to eq(:deleted) expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 2)).to eq(:deleted)
expect(DiscourseChatIntegration::Rule.all.size).to eq(2) expect(DiscourseChatIntegration::Rule.all.size).to eq(2)
expect(DiscourseChatIntegration::Rule.all.map(&:category_id)).to contain_exactly(category.id, category3.id) expect(DiscourseChatIntegration::Rule.all.map(&:category_id)).to contain_exactly(
category.id,
category3.id,
)
end end
it 'fails gracefully for out of range indexes' do it "fails gracefully for out of range indexes" do
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1, rule1 =
filter: 'watch', DiscourseChatIntegration::Rule.create(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag2.name] filter: "watch",
) category_id: category.id,
tags: [tag1.name, tag2.name],
)
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, -1)).to eq(false) expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, -1)).to eq(false)
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 0)).to eq(false) expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 0)).to eq(false)
@ -231,87 +281,100 @@ RSpec.describe DiscourseChatIntegration::Manager do
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 1)).to eq(:deleted) expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 1)).to eq(:deleted)
end end
end end
describe '.smart_create_rule' do describe ".smart_create_rule" do
it "creates a rule when there are none" do
it 'creates a rule when there are none' do val =
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, DiscourseChatIntegration::Helper.smart_create_rule(
filter: 'watch', channel: chan1,
category_id: category.id, filter: "watch",
tags: [tag1.name] category_id: category.id,
) tags: [tag1.name],
)
expect(val).to eq(:created) expect(val).to eq(:created)
record = DiscourseChatIntegration::Rule.all.first record = DiscourseChatIntegration::Rule.all.first
expect(record.channel).to eq(chan1) expect(record.channel).to eq(chan1)
expect(record.filter).to eq('watch') expect(record.filter).to eq("watch")
expect(record.category_id).to eq(category.id) expect(record.category_id).to eq(category.id)
expect(record.tags).to eq([tag1.name]) expect(record.tags).to eq([tag1.name])
end end
it 'updates a rule when it has the same category and tags' do it "updates a rule when it has the same category and tags" do
existing = DiscourseChatIntegration::Rule.create!(channel: chan1, existing =
filter: 'watch', DiscourseChatIntegration::Rule.create!(
category_id: category.id, channel: chan1,
tags: [tag2.name, tag1.name] filter: "watch",
) category_id: category.id,
tags: [tag2.name, tag1.name],
)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, val =
filter: 'mute', DiscourseChatIntegration::Helper.smart_create_rule(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag2.name] filter: "mute",
) category_id: category.id,
tags: [tag1.name, tag2.name],
)
expect(val).to eq(:updated) expect(val).to eq(:updated)
expect(DiscourseChatIntegration::Rule.all.size).to eq(1) expect(DiscourseChatIntegration::Rule.all.size).to eq(1)
expect(DiscourseChatIntegration::Rule.all.first.filter).to eq('mute') expect(DiscourseChatIntegration::Rule.all.first.filter).to eq("mute")
end end
it 'updates a rule when it has the same category and filter' do it "updates a rule when it has the same category and filter" do
existing = DiscourseChatIntegration::Rule.create(channel: chan1, existing =
filter: 'watch', DiscourseChatIntegration::Rule.create(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag2.name] filter: "watch",
) category_id: category.id,
tags: [tag1.name, tag2.name],
)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, val =
filter: 'watch', DiscourseChatIntegration::Helper.smart_create_rule(
category_id: category.id, channel: chan1,
tags: [tag1.name, tag3.name] filter: "watch",
) category_id: category.id,
tags: [tag1.name, tag3.name],
)
expect(val).to eq(:updated) expect(val).to eq(:updated)
expect(DiscourseChatIntegration::Rule.all.size).to eq(1) expect(DiscourseChatIntegration::Rule.all.size).to eq(1)
expect(DiscourseChatIntegration::Rule.all.first.tags).to contain_exactly(tag1.name, tag2.name, tag3.name) expect(DiscourseChatIntegration::Rule.all.first.tags).to contain_exactly(
tag1.name,
tag2.name,
tag3.name,
)
end end
it 'destroys duplicate rules on save' do it "destroys duplicate rules on save" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch') DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch")
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch') DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch")
expect(DiscourseChatIntegration::Rule.all.size).to eq(2) expect(DiscourseChatIntegration::Rule.all.size).to eq(2)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, val =
filter: 'watch', DiscourseChatIntegration::Helper.smart_create_rule(
category_id: nil, channel: chan1,
tags: nil filter: "watch",
) category_id: nil,
tags: nil,
)
expect(val).to eq(:updated) expect(val).to eq(:updated)
expect(DiscourseChatIntegration::Rule.all.size).to eq(1) expect(DiscourseChatIntegration::Rule.all.size).to eq(1)
end end
it 'returns false on error' do it "returns false on error" do
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, filter: 'blah') val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, filter: "blah")
expect(val).to eq(false) expect(val).to eq(false)
end end
end end
describe '.save_transcript' do describe ".save_transcript" do
it "saves a transcript to redis" do
it 'saves a transcript to redis' do
key = DiscourseChatIntegration::Helper.save_transcript("Some content here") key = DiscourseChatIntegration::Helper.save_transcript("Some content here")
expect(Discourse.redis.get("chat_integration:transcript:#{key}")).to eq("Some content here") expect(Discourse.redis.get("chat_integration:transcript:#{key}")).to eq("Some content here")
@ -325,19 +388,25 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
describe ".formatted_display_name" do describe ".formatted_display_name" do
let(:user) { Fabricate(:user, name: "John Smith", username: 'js1') } let(:user) { Fabricate(:user, name: "John Smith", username: "js1") }
it "prioritizes correctly" do it "prioritizes correctly" do
SiteSetting.prioritize_username_in_ux = true SiteSetting.prioritize_username_in_ux = true
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("@#{user.username} (John Smith)") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq(
"@#{user.username} (John Smith)",
)
SiteSetting.prioritize_username_in_ux = false SiteSetting.prioritize_username_in_ux = false
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("John Smith (@#{user.username})") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq(
"John Smith (@#{user.username})",
)
end end
it "only displays one when name/username are similar" do it "only displays one when name/username are similar" do
user.update!(username: "john_smith") user.update!(username: "john_smith")
SiteSetting.prioritize_username_in_ux = true SiteSetting.prioritize_username_in_ux = true
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("@#{user.username}") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq(
"@#{user.username}",
)
SiteSetting.prioritize_username_in_ux = false SiteSetting.prioritize_username_in_ux = false
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("John Smith") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("John Smith")
end end
@ -346,10 +415,13 @@ RSpec.describe DiscourseChatIntegration::Manager do
SiteSetting.enable_names = false SiteSetting.enable_names = false
SiteSetting.prioritize_username_in_ux = true SiteSetting.prioritize_username_in_ux = true
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("@#{user.username}") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq(
"@#{user.username}",
)
SiteSetting.prioritize_username_in_ux = false SiteSetting.prioritize_username_in_ux = false
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("@#{user.username}") expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq(
"@#{user.username}",
)
end end
end end
end end

View File

@ -1,42 +1,46 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe Jobs::DiscourseChatMigrateFromSlackOfficial do RSpec.describe Jobs::DiscourseChatMigrateFromSlackOfficial do
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
describe 'site settings' do describe "site settings" do
before do before do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: 'discourse-slack-official', plugin_name: "discourse-slack-official",
key: "category_#{category.id}", key: "category_#{category.id}",
type_name: "JSON", type_name: "JSON",
value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]" value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]",
) )
SiteSetting.create!(value: 't', data_type: 5, name: 'slack_enabled') SiteSetting.create!(value: "t", data_type: 5, name: "slack_enabled")
SiteSetting.create!(value: 'token', data_type: 1, name: 'slack_access_token') SiteSetting.create!(value: "token", data_type: 1, name: "slack_access_token")
SiteSetting.create!(value: 'token2', data_type: 1, name: 'slack_incoming_webhook_token') SiteSetting.create!(value: "token2", data_type: 1, name: "slack_incoming_webhook_token")
SiteSetting.create!(value: 300, data_type: 3, name: 'slack_discourse_excerpt_length') SiteSetting.create!(value: 300, data_type: 3, name: "slack_discourse_excerpt_length")
SiteSetting.create!(value: "https://hooks.slack.com/services/something", data_type: 1, name: 'slack_outbound_webhook_url') SiteSetting.create!(
SiteSetting.create!(value: "http://outbound2.com", data_type: 1, name: 'slack_icon_url') value: "https://hooks.slack.com/services/something",
SiteSetting.create!(value: 100, data_type: 3, name: 'post_to_slack_window_secs') data_type: 1,
SiteSetting.create!(value: User.last.username, data_type: 1, name: 'slack_discourse_username') name: "slack_outbound_webhook_url",
)
SiteSetting.create!(value: "http://outbound2.com", data_type: 1, name: "slack_icon_url")
SiteSetting.create!(value: 100, data_type: 3, name: "post_to_slack_window_secs")
SiteSetting.create!(value: User.last.username, data_type: 1, name: "slack_discourse_username")
end end
it 'should migrate the site settings correctly' do it "should migrate the site settings correctly" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(SiteSetting.find_by(name: 'slack_enabled').value).to eq('f') expect(SiteSetting.find_by(name: "slack_enabled").value).to eq("f")
expect(SiteSetting.chat_integration_slack_access_token).to eq('token') expect(SiteSetting.chat_integration_slack_access_token).to eq("token")
expect(SiteSetting.chat_integration_slack_incoming_webhook_token).to eq('token2') expect(SiteSetting.chat_integration_slack_incoming_webhook_token).to eq("token2")
expect(SiteSetting.chat_integration_slack_excerpt_length).to eq(300) expect(SiteSetting.chat_integration_slack_excerpt_length).to eq(300)
expect(SiteSetting.chat_integration_slack_outbound_webhook_url) expect(SiteSetting.chat_integration_slack_outbound_webhook_url).to eq(
.to eq("https://hooks.slack.com/services/something") "https://hooks.slack.com/services/something",
)
expect(SiteSetting.chat_integration_slack_icon_url) expect(SiteSetting.chat_integration_slack_icon_url).to eq("http://outbound2.com")
.to eq("http://outbound2.com")
expect(SiteSetting.chat_integration_delay_seconds).to eq(100) expect(SiteSetting.chat_integration_delay_seconds).to eq(100)
expect(SiteSetting.chat_integration_discourse_username).to eq(User.last.username) expect(SiteSetting.chat_integration_discourse_username).to eq(User.last.username)
@ -44,31 +48,31 @@ RSpec.describe Jobs::DiscourseChatMigrateFromSlackOfficial do
expect(SiteSetting.chat_integration_enabled).to eq(true) expect(SiteSetting.chat_integration_enabled).to eq(true)
end end
describe 'when slack_discourse_username is not valid' do describe "when slack_discourse_username is not valid" do
before do before { SiteSetting.find_by(name: "slack_discourse_username").update!(value: "someguy") }
SiteSetting.find_by(name: 'slack_discourse_username').update!(value: 'someguy')
end
it 'should default to the system user' do it "should default to the system user" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(SiteSetting.chat_integration_discourse_username) expect(SiteSetting.chat_integration_discourse_username).to eq(
.to eq(Discourse.system_user.username) Discourse.system_user.username,
)
end end
end end
end end
describe 'when a uncategorized filter is present' do describe "when a uncategorized filter is present" do
before do before do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: 'discourse-slack-official', plugin_name: "discourse-slack-official",
key: "category_*", key: "category_*",
type_name: "JSON", type_name: "JSON",
value: "[{\"channel\":\"#channel1\",\"filter\":\"watch\"},{\"channel\":\"channel2\",\"filter\":\"follow\"},{\"channel\":\"#channel1\",\"filter\":\"mute\"}]" value:
"[{\"channel\":\"#channel1\",\"filter\":\"watch\"},{\"channel\":\"channel2\",\"filter\":\"follow\"},{\"channel\":\"#channel1\",\"filter\":\"mute\"}]",
) )
end end
it 'should create the right channels and rules' do it "should create the right channels and rules" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(DiscourseChatIntegration::Channel.count).to eq(2) expect(DiscourseChatIntegration::Channel.count).to eq(2)
@ -76,86 +80,87 @@ RSpec.describe Jobs::DiscourseChatMigrateFromSlackOfficial do
channel = DiscourseChatIntegration::Channel.first channel = DiscourseChatIntegration::Channel.first
expect(channel.value['provider']).to eq("slack") expect(channel.value["provider"]).to eq("slack")
expect(channel.value['data']['identifier']).to eq("#channel1") expect(channel.value["data"]["identifier"]).to eq("#channel1")
rule = DiscourseChatIntegration::Rule.first rule = DiscourseChatIntegration::Rule.first
expect(rule.value['channel_id']).to eq(channel.id) expect(rule.value["channel_id"]).to eq(channel.id)
expect(rule.value['filter']).to eq('mute') expect(rule.value["filter"]).to eq("mute")
expect(rule.value['category_id']).to eq(nil) expect(rule.value["category_id"]).to eq(nil)
channel = DiscourseChatIntegration::Channel.last channel = DiscourseChatIntegration::Channel.last
expect(channel.value['provider']).to eq("slack") expect(channel.value["provider"]).to eq("slack")
expect(channel.value['data']['identifier']).to eq("#channel2") expect(channel.value["data"]["identifier"]).to eq("#channel2")
rule = DiscourseChatIntegration::Rule.last rule = DiscourseChatIntegration::Rule.last
expect(rule.value['channel_id']).to eq(channel.id) expect(rule.value["channel_id"]).to eq(channel.id)
expect(rule.value['filter']).to eq('follow') expect(rule.value["filter"]).to eq("follow")
expect(rule.value['category_id']).to eq(nil) expect(rule.value["category_id"]).to eq(nil)
end end
end end
describe 'when filter contains an invalid tag' do describe "when filter contains an invalid tag" do
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
before do before do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: 'discourse-slack-official', plugin_name: "discourse-slack-official",
key: "category_#{category.id}", key: "category_#{category.id}",
type_name: "JSON", type_name: "JSON",
value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\",\"tags\":[\"#{tag.name}\",\"random-tag\"]}]" value:
"[{\"channel\":\"#slack-channel\",\"filter\":\"mute\",\"tags\":[\"#{tag.name}\",\"random-tag\"]}]",
) )
end end
it 'should discard invalid tags' do it "should discard invalid tags" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
rule = DiscourseChatIntegration::Rule.first rule = DiscourseChatIntegration::Rule.first
expect(rule.value['tags']).to eq([tag.name]) expect(rule.value["tags"]).to eq([tag.name])
end end
end end
describe 'when a category filter is present' do describe "when a category filter is present" do
before do before do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: 'discourse-slack-official', plugin_name: "discourse-slack-official",
key: "category_#{category.id}", key: "category_#{category.id}",
type_name: "JSON", type_name: "JSON",
value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]" value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]",
) )
end end
it 'should migrate the settings correctly' do it "should migrate the settings correctly" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
channel = DiscourseChatIntegration::Channel.first channel = DiscourseChatIntegration::Channel.first
expect(channel.value['provider']).to eq("slack") expect(channel.value["provider"]).to eq("slack")
expect(channel.value['data']['identifier']).to eq("#slack-channel") expect(channel.value["data"]["identifier"]).to eq("#slack-channel")
rule = DiscourseChatIntegration::Rule.first rule = DiscourseChatIntegration::Rule.first
expect(rule.value['channel_id']).to eq(channel.id) expect(rule.value["channel_id"]).to eq(channel.id)
expect(rule.value['filter']).to eq('mute') expect(rule.value["filter"]).to eq("mute")
expect(rule.value['category_id']).to eq(category.id) expect(rule.value["category_id"]).to eq(category.id)
end end
end end
describe 'when a category has been deleted' do describe "when a category has been deleted" do
before do before do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: 'discourse-slack-official', plugin_name: "discourse-slack-official",
key: 'category_9999', key: "category_9999",
type_name: "JSON", type_name: "JSON",
value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]" value: "[{\"channel\":\"#slack-channel\",\"filter\":\"mute\"}]",
) )
end end
it 'should not migrate the settings' do it "should not migrate the settings" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(DiscourseChatIntegration::Channel.count).to eq(0) expect(DiscourseChatIntegration::Channel.count).to eq(0)

View File

@ -1,48 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe PostCreator do RSpec.describe PostCreator do
let(:topic) { Fabricate(:post).topic } let(:topic) { Fabricate(:post).topic }
before do before { Jobs::NotifyChats.jobs.clear }
Jobs::NotifyChats.jobs.clear
end
describe 'when a post is created' do describe "when a post is created" do
describe 'when plugin is enabled' do describe "when plugin is enabled" do
before do before { SiteSetting.chat_integration_enabled = true }
SiteSetting.chat_integration_enabled = true
end
it 'should schedule a chat notification job' do it "should schedule a chat notification job" do
freeze_time Time.now.beginning_of_day freeze_time Time.now.beginning_of_day
post = PostCreator.new(topic.user, post = PostCreator.new(topic.user, raw: "Some post content", topic_id: topic.id).create!
raw: 'Some post content',
topic_id: topic.id
).create!
job = Jobs::NotifyChats.jobs.last job = Jobs::NotifyChats.jobs.last
expect(job['at']) expect(job["at"]).to eq(
.to eq(Time.now.to_f + SiteSetting.chat_integration_delay_seconds.seconds.to_f) Time.now.to_f + SiteSetting.chat_integration_delay_seconds.seconds.to_f,
)
expect(job['args'].first['post_id']).to eq(post.id)
expect(job["args"].first["post_id"]).to eq(post.id)
end end
end end
describe 'when plugin is not enabled' do describe "when plugin is not enabled" do
before do before { SiteSetting.chat_integration_enabled = false }
SiteSetting.chat_integration_enabled = false
end
it 'should not schedule a job for chat notifications' do it "should not schedule a job for chat notifications" do
PostCreator.new(topic.user, PostCreator.new(topic.user, raw: "Some post content", topic_id: topic.id).create!
raw: 'Some post content',
topic_id: topic.id
).create!
expect(Jobs::NotifyChats.jobs).to eq([]) expect(Jobs::NotifyChats.jobs).to eq([])
end end

View File

@ -1,38 +1,52 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::DiscordProvider do RSpec.describe DiscourseChatIntegration::Provider::DiscordProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_discord_enabled = true }
SiteSetting.chat_integration_discord_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "discord",
data: {
name: "Awesome Channel",
webhook_url: "https://discord.com/api/webhooks/1234/abcd",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'discord', data: { name: "Awesome Channel", webhook_url: 'https://discord.com/api/webhooks/1234/abcd' }) } it "sends a webhook request" do
stub1 =
it 'sends a webhook request' do stub_request(:post, "https://discord.com/api/webhooks/1234/abcd?wait=true").to_return(
stub1 = stub_request(:post, 'https://discord.com/api/webhooks/1234/abcd?wait=true').to_return(status: 200) status: 200,
)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'includes the protocol in the avatar URL' do it "includes the protocol in the avatar URL" do
stub1 = stub_request(:post, 'https://discord.com/api/webhooks/1234/abcd?wait=true') stub1 =
.with(body: hash_including(embeds: [hash_including(author: hash_including(url: /^https?:\/\//))])) stub_request(:post, "https://discord.com/api/webhooks/1234/abcd?wait=true").with(
.to_return(status: 200) body:
hash_including(embeds: [hash_including(author: hash_including(url: %r{^https?://}))]),
).to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, "https://discord.com/api/webhooks/1234/abcd?wait=true").to_return(status: 400) stub1 =
stub_request(:post, "https://discord.com/api/webhooks/1234/abcd?wait=true").to_return(
status: 400,
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,27 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::FlowdockProvider do RSpec.describe DiscourseChatIntegration::Provider::FlowdockProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_flowdock_enabled = true }
SiteSetting.chat_integration_flowdock_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "flowdock",
data: {
flow_token: "5d1fe04cf66e078d6a2b579ddb8a465b",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'flowdock', data: { flow_token: '5d1fe04cf66e078d6a2b579ddb8a465b' }) } it "sends a request" do
it 'sends a request' do
stub1 = stub_request(:post, "https://api.flowdock.com/messages").to_return(status: 200) stub1 = stub_request(:post, "https://api.flowdock.com/messages").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, "https://api.flowdock.com/messages").to_return(status: 404, body: "{ \"error\": \"Not Found\"}") stub1 =
stub_request(:post, "https://api.flowdock.com/messages").to_return(
status: 404,
body: "{ \"error\": \"Not Found\"}",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end

View File

@ -1,27 +1,39 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GitterProvider do RSpec.describe DiscourseChatIntegration::Provider::GitterProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_gitter_enabled = true }
SiteSetting.chat_integration_gitter_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "gitter",
data: {
name: "gitterHQ/services",
webhook_url: "https://webhooks.gitter.im/e/a1e2i3o4u5",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'gitter', data: { name: 'gitterHQ/services', webhook_url: 'https://webhooks.gitter.im/e/a1e2i3o4u5' }) } it "sends a webhook request" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(body: "OK")
it 'sends a webhook request' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(body: "OK")
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 404, body: "{ \"error\": \"Not Found\"}") stub1 =
stub_request(:post, chan1.data["webhook_url"]).to_return(
status: 404,
body: "{ \"error\": \"Not Found\"}",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end

View File

@ -1,30 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GoogleProvider do RSpec.describe DiscourseChatIntegration::Provider::GoogleProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_google_enabled = true }
SiteSetting.chat_integration_google_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "google",
data: {
name: "discourse",
webhook_url: "https://chat.googleapis.com/v1/abcdefg",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'google', data: { name: 'discourse', webhook_url: 'https://chat.googleapis.com/v1/abcdefg' }) } it "sends a webhook request" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(body: "1")
it 'sends a webhook request' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(body: "1")
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 400, body: "{}") stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(status: 400, body: "{}")
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,28 +1,41 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GroupmeProvider do RSpec.describe DiscourseChatIntegration::Provider::GroupmeProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_groupme_enabled = true SiteSetting.chat_integration_groupme_enabled = true
SiteSetting.chat_integration_groupme_bot_ids = '1a2b3c4d5e6f7g' SiteSetting.chat_integration_groupme_bot_ids = "1a2b3c4d5e6f7g"
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'groupme', data: { groupme_bot_id: '1a2b3c4d5e6f7g' }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "groupme",
data: {
groupme_bot_id: "1a2b3c4d5e6f7g",
},
)
end
it 'sends a request' do it "sends a request" do
stub1 = stub_request(:post, "https://api.groupme.com/v3/bots/post").to_return(status: 200) stub1 = stub_request(:post, "https://api.groupme.com/v3/bots/post").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, "https://api.groupme.com/v3/bots/post").to_return(status: 404, body: "{ \"error\": \"Not Found\"}") stub1 =
stub_request(:post, "https://api.groupme.com/v3/bots/post").to_return(
status: 404,
body: "{ \"error\": \"Not Found\"}",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end

View File

@ -1,30 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GuildedProvider do RSpec.describe DiscourseChatIntegration::Provider::GuildedProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_guilded_enabled = true }
SiteSetting.chat_integration_guilded_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "guilded",
data: {
name: "Awesome Channel",
webhook_url: "https://media.guilded.gg/webhooks/1234/abcd",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'guilded', data: { name: "Awesome Channel", webhook_url: 'https://media.guilded.gg/webhooks/1234/abcd' }) } it "sends a webhook request" do
stub1 =
it 'sends a webhook request' do stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 200)
stub1 = stub_request(:post, 'https://media.guilded.gg/webhooks/1234/abcd').to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 400) stub1 =
stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 400)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,31 +1,47 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::MatrixProvider do RSpec.describe DiscourseChatIntegration::Provider::MatrixProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_matrix_enabled = true SiteSetting.chat_integration_matrix_enabled = true
SiteSetting.chat_integration_matrix_access_token = 'abcd' SiteSetting.chat_integration_matrix_access_token = "abcd"
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'matrix', data: { name: "Awesome Channel", room_id: '!blah:matrix.org' }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "matrix",
data: {
name: "Awesome Channel",
room_id: "!blah:matrix.org",
},
)
end
it 'sends the message' do it "sends the message" do
stub1 = stub_request(:put, %r{https://matrix.org/_matrix/client/r0/rooms/!blah:matrix.org/send/m.room.message/*}).to_return(status: 200) stub1 =
stub_request(
:put,
%r{https://matrix.org/_matrix/client/r0/rooms/!blah:matrix.org/send/m.room.message/*},
).to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:put, %r{https://matrix.org/_matrix/client/r0/rooms/!blah:matrix.org/send/m.room.message/*}).to_return(status: 400, body: '{"errmsg":"M_UNKNOWN"}') stub1 =
stub_request(
:put,
%r{https://matrix.org/_matrix/client/r0/rooms/!blah:matrix.org/send/m.room.message/*},
).to_return(status: 400, body: '{"errmsg":"M_UNKNOWN"}')
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,88 +1,94 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe 'Mattermost Command Controller', type: :request do describe "Mattermost Command Controller", type: :request do
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) } let(:tag2) { Fabricate(:tag) }
let!(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'mattermost', data: { identifier: '#welcome' }) } let!(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "mattermost",
data: {
identifier: "#welcome",
},
)
end
describe 'with plugin disabled' do describe "with plugin disabled" do
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/mattermost/command.json' post "/chat-integration/mattermost/command.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'with plugin enabled and provider disabled' do describe "with plugin enabled and provider disabled" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_mattermost_enabled = false SiteSetting.chat_integration_mattermost_enabled = false
end end
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/mattermost/command.json' post "/chat-integration/mattermost/command.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'slash commands endpoint' do describe "slash commands endpoint" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_mattermost_webhook_url = "https://hooks.mattermost.com/services/abcde" SiteSetting.chat_integration_mattermost_webhook_url =
"https://hooks.mattermost.com/services/abcde"
SiteSetting.chat_integration_mattermost_enabled = true SiteSetting.chat_integration_mattermost_enabled = true
end end
describe 'when forum is private' do describe "when forum is private" do
it 'should not redirect to login page' do it "should not redirect to login page" do
SiteSetting.login_required = true SiteSetting.login_required = true
token = 'sometoken' token = "sometoken"
SiteSetting.chat_integration_mattermost_incoming_webhook_token = token SiteSetting.chat_integration_mattermost_incoming_webhook_token = token
post '/chat-integration/mattermost/command.json', params: { post "/chat-integration/mattermost/command.json", params: { text: "help", token: token }
text: 'help', token: token
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
end end
describe 'when the token is invalid' do describe "when the token is invalid" do
it 'should raise the right error' do it "should raise the right error" do
post '/chat-integration/mattermost/command.json', params: { text: 'help' } post "/chat-integration/mattermost/command.json", params: { text: "help" }
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
describe 'when incoming webhook token has not been set' do describe "when incoming webhook token has not been set" do
it 'should raise the right error' do it "should raise the right error" do
post '/chat-integration/mattermost/command.json', params: { post "/chat-integration/mattermost/command.json",
text: 'help', token: 'some token' params: {
} text: "help",
token: "some token",
}
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
describe 'when token is valid' do describe "when token is valid" do
let(:token) { "Secret Sauce" } let(:token) { "Secret Sauce" }
# No need to test every single command here, that's tested # No need to test every single command here, that's tested
# by helper_spec upstream # by helper_spec upstream
before do before { SiteSetting.chat_integration_mattermost_incoming_webhook_token = token }
SiteSetting.chat_integration_mattermost_incoming_webhook_token = token
end
describe 'add new rule' do describe "add new rule" do
it "should add a new rule correctly" do
it 'should add a new rule correctly' do post "/chat-integration/mattermost/command.json",
post "/chat-integration/mattermost/command.json", params: { params: {
text: "watch #{category.slug}", text: "watch #{category.slug}",
channel_name: 'welcome', channel_name: "welcome",
token: token token: token,
} }
json = response.parsed_body json = response.parsed_body
@ -90,34 +96,40 @@ describe 'Mattermost Command Controller', type: :request do
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1) expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
describe 'from an unknown channel' do describe "from an unknown channel" do
it 'creates the channel' do it "creates the channel" do
post "/chat-integration/mattermost/command.json", params: { post "/chat-integration/mattermost/command.json",
text: "watch #{category.slug}", params: {
channel_name: 'general', text: "watch #{category.slug}",
token: token channel_name: "general",
} token: token,
}
json = response.parsed_body json = response.parsed_body
expect(json["text"]).to eq(I18n.t("chat_integration.provider.mattermost.create.created")) expect(json["text"]).to eq(
I18n.t("chat_integration.provider.mattermost.create.created"),
)
chan = DiscourseChatIntegration::Channel.with_provider('mattermost').with_data_value('identifier', '#general').first chan =
expect(chan.provider).to eq('mattermost') DiscourseChatIntegration::Channel
.with_provider("mattermost")
.with_data_value("identifier", "#general")
.first
expect(chan.provider).to eq("mattermost")
rule = chan.rules.first rule = chan.rules.first
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
end end
end end
end end
end end
end end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::MattermostProvider do RSpec.describe DiscourseChatIntegration::Provider::MattermostProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
let(:upload) { Fabricate(:upload) } let(:upload) { Fabricate(:upload) }
before do before do
@ -14,36 +14,47 @@ RSpec.describe DiscourseChatIntegration::Provider::MattermostProvider do
SiteSetting.logo_small = upload SiteSetting.logo_small = upload
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'mattermost', data: { identifier: "#awesomechannel" }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "mattermost",
data: {
identifier: "#awesomechannel",
},
)
end
it 'sends a webhook request' do it "sends a webhook request" do
stub1 = stub_request(:post, 'https://mattermost.blah/hook/abcd').to_return(status: 200) stub1 = stub_request(:post, "https://mattermost.blah/hook/abcd").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
describe 'when mattermost icon is not configured' do describe "when mattermost icon is not configured" do
it 'defaults to the right icon' do it "defaults to the right icon" do
message = described_class.mattermost_message(post, chan1) message = described_class.mattermost_message(post, chan1)
expect(message[:icon_url]).to eq(UrlHelper.absolute(upload.url)) expect(message[:icon_url]).to eq(UrlHelper.absolute(upload.url))
end end
end end
describe 'when mattermost icon has been configured' do describe "when mattermost icon has been configured" do
it 'should use the right icon' do it "should use the right icon" do
SiteSetting.chat_integration_mattermost_icon_url = "https://specific_logo" SiteSetting.chat_integration_mattermost_icon_url = "https://specific_logo"
message = described_class.mattermost_message(post, chan1) message = described_class.mattermost_message(post, chan1)
expect(message[:icon_url]).to eq(SiteSetting.chat_integration_mattermost_icon_url) expect(message[:icon_url]).to eq(SiteSetting.chat_integration_mattermost_icon_url)
end end
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, "https://mattermost.blah/hook/abcd").to_return(status: 500, body: "error") stub1 =
stub_request(:post, "https://mattermost.blah/hook/abcd").to_return(
status: 500,
body: "error",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,31 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::RocketchatProvider do RSpec.describe DiscourseChatIntegration::Provider::RocketchatProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_rocketchat_enabled = true SiteSetting.chat_integration_rocketchat_enabled = true
SiteSetting.chat_integration_rocketchat_webhook_url = "https://example.com/abcd" SiteSetting.chat_integration_rocketchat_webhook_url = "https://example.com/abcd"
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'rocketchat', data: { identifier: "#general" }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "rocketchat",
data: {
identifier: "#general",
},
)
end
it 'sends a webhook request' do it "sends a webhook request" do
stub1 = stub_request(:post, 'https://example.com/abcd').to_return(body: "{\"success\":true}") stub1 = stub_request(:post, "https://example.com/abcd").to_return(body: "{\"success\":true}")
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, 'https://example.com/abcd').to_return(status: 400, body: "{}") stub1 = stub_request(:post, "https://example.com/abcd").to_return(status: 400, body: "{}")
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,106 +1,99 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe 'Slack Command Controller', type: :request do describe "Slack Command Controller", type: :request do
before do before { Discourse.cache.clear }
Discourse.cache.clear
end
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) } let(:tag2) { Fabricate(:tag) }
let!(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'slack', data: { identifier: '#welcome' }) } let!(:chan1) do
DiscourseChatIntegration::Channel.create!(provider: "slack", data: { identifier: "#welcome" })
end
describe 'with plugin disabled' do describe "with plugin disabled" do
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/slack/command.json' post "/chat-integration/slack/command.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'with plugin enabled and provider disabled' do describe "with plugin enabled and provider disabled" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_slack_enabled = false SiteSetting.chat_integration_slack_enabled = false
end end
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/slack/command.json' post "/chat-integration/slack/command.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'slash commands endpoint' do describe "slash commands endpoint" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_slack_outbound_webhook_url = "https://hooks.slack.com/services/abcde" SiteSetting.chat_integration_slack_outbound_webhook_url =
"https://hooks.slack.com/services/abcde"
SiteSetting.chat_integration_slack_enabled = true SiteSetting.chat_integration_slack_enabled = true
end end
describe 'when forum is private' do describe "when forum is private" do
it 'should not redirect to login page' do it "should not redirect to login page" do
SiteSetting.login_required = true SiteSetting.login_required = true
token = 'sometoken' token = "sometoken"
SiteSetting.chat_integration_slack_incoming_webhook_token = token SiteSetting.chat_integration_slack_incoming_webhook_token = token
post '/chat-integration/slack/command.json', params: { post "/chat-integration/slack/command.json", params: { text: "help", token: token }
text: 'help', token: token
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
end end
describe 'when the token is invalid' do describe "when the token is invalid" do
it 'should raise the right error' do it "should raise the right error" do
post '/chat-integration/slack/command.json', params: { text: 'help' } post "/chat-integration/slack/command.json", params: { text: "help" }
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
describe 'backwards compatibility with discourse-slack-official' do describe "backwards compatibility with discourse-slack-official" do
it 'should return the right response' do it "should return the right response" do
token = 'secret sauce' token = "secret sauce"
SiteSetting.chat_integration_slack_incoming_webhook_token = token SiteSetting.chat_integration_slack_incoming_webhook_token = token
post '/slack/command.json', params: { post "/slack/command.json", params: { text: "help", token: token }
text: 'help', token: token
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["text"]).to be_present expect(response.parsed_body["text"]).to be_present
end end
end end
describe 'when incoming webhook token has not been set' do describe "when incoming webhook token has not been set" do
it 'should raise the right error' do it "should raise the right error" do
post '/chat-integration/slack/command.json', params: { post "/chat-integration/slack/command.json", params: { text: "help", token: "some token" }
text: 'help', token: 'some token'
}
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
describe 'when token is valid' do describe "when token is valid" do
let(:token) { "Secret Sauce" } let(:token) { "Secret Sauce" }
# No need to test every single command here, that's tested # No need to test every single command here, that's tested
# by helper_spec upstream # by helper_spec upstream
before do before { SiteSetting.chat_integration_slack_incoming_webhook_token = token }
SiteSetting.chat_integration_slack_incoming_webhook_token = token
end
describe 'add new rule' do describe "add new rule" do
it "should add a new rule correctly" do
it 'should add a new rule correctly' do post "/chat-integration/slack/command.json",
post "/chat-integration/slack/command.json", params: { params: {
text: "watch #{category.slug}", text: "watch #{category.slug}",
channel_name: 'welcome', channel_name: "welcome",
token: token token: token,
} }
json = response.parsed_body json = response.parsed_body
@ -108,277 +101,314 @@ describe 'Slack Command Controller', type: :request do
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1) expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
describe 'from an unknown channel' do describe "from an unknown channel" do
it 'creates the channel' do it "creates the channel" do
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "watch #{category.slug}", params: {
channel_name: 'general', text: "watch #{category.slug}",
token: token channel_name: "general",
} token: token,
}
json = response.parsed_body json = response.parsed_body
expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.create.created")) expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.create.created"))
chan = DiscourseChatIntegration::Channel.with_provider('slack').with_data_value('identifier', '#general').first chan =
expect(chan.provider).to eq('slack') DiscourseChatIntegration::Channel
.with_provider("slack")
.with_data_value("identifier", "#general")
.first
expect(chan.provider).to eq("slack")
rule = chan.rules.first rule = chan.rules.first
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
end end
end end
describe 'post transcript' do describe "post transcript" do
let(:messages_fixture) { let(:messages_fixture) do
[ [
{ {
"type": "message", type: "message",
"user": "U6JSSESES", user: "U6JSSESES",
"text": "Yeah, should make posting slack transcripts much easier", text: "Yeah, should make posting slack transcripts much easier",
"ts": "1501801665.062694" ts: "1501801665.062694",
}, },
{ {
"type": "message", type: "message",
"user": "U5Z773QLS", user: "U5Z773QLS",
"text": "Oooh a new discourse plugin???", text: "Oooh a new discourse plugin???",
"ts": "1501801643.056375" ts: "1501801643.056375",
},
{ type: "message", user: "U6E2W7R8C", text: "Which one?", ts: "1501801634.053761" },
{
type: "message",
user: "U6JSSESES",
text:
"So, who's interested in the new <https://meta.discourse.org|discourse plugin>?",
ts: "1501801629.052212",
}, },
{ {
"type": "message", text: "",
"user": "U6E2W7R8C", username: "Test Community",
"text": "Which one?", bot_id: "B6C6JNUDN",
"ts": "1501801634.053761" attachments: [
{
author_name: "@david",
fallback: "Discourse can now be integrated with Mattermost! - @david",
text: "Hey <http://localhost/groups/team|@team>, what do you think about this?",
title: "Discourse can now be integrated with Mattermost! [Announcements] ",
id: 1,
title_link:
"http://localhost:3000/t/discourse-can-now-be-integrated-with-mattermost/51/4",
color: "283890",
mrkdwn_in: ["text"],
},
],
type: "message",
subtype: "bot_message",
ts: "1501615820.949638",
}, },
{ {
"type": "message", type: "message",
"user": "U6JSSESES", user: "U5Z773QLS",
"text": "So, who's interested in the new <https://meta.discourse.org|discourse plugin>?", text: "Lets try some *bold text*",
"ts": "1501801629.052212" ts: "1501093331.439776",
}, },
{
"text": "",
"username": "Test Community",
"bot_id": "B6C6JNUDN",
"attachments": [
{
"author_name": "@david",
"fallback": "Discourse can now be integrated with Mattermost! - @david",
"text": "Hey <http://localhost/groups/team|@team>, what do you think about this?",
"title": "Discourse can now be integrated with Mattermost! [Announcements] ",
"id": 1,
"title_link": "http://localhost:3000/t/discourse-can-now-be-integrated-with-mattermost/51/4",
"color": "283890",
"mrkdwn_in": [
"text"
]
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1501615820.949638"
},
{
"type": "message",
"user": "U5Z773QLS",
"text": "Lets try some *bold text*",
"ts": "1501093331.439776"
},
] ]
}
before do
SiteSetting.chat_integration_slack_access_token = 'abcde'
end end
before { SiteSetting.chat_integration_slack_access_token = "abcde" }
context "with valid slack responses" do context "with valid slack responses" do
before do before do
stub_request(:post, "https://slack.com/api/users.list").to_return(body: '{"ok":true,"members":[{"id":"U5Z773QLS","profile":{"display_name":"david","real_name":"david","icon_24":"https://example.com/avatar"}}],"response_metadata":{"next_cursor":""}}') stub_request(:post, "https://slack.com/api/users.list").to_return(
stub_request(:post, "https://slack.com/api/conversations.history").to_return(body: { ok: true, messages: messages_fixture }.to_json) body:
'{"ok":true,"members":[{"id":"U5Z773QLS","profile":{"display_name":"david","real_name":"david","icon_24":"https://example.com/avatar"}}],"response_metadata":{"next_cursor":""}}',
)
stub_request(:post, "https://slack.com/api/conversations.history").to_return(
body: { ok: true, messages: messages_fixture }.to_json,
)
end end
it 'generates the transcript UI properly' do it "generates the transcript UI properly" do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.with(body: /attachments/) stub_request(:post, "https://slack.com/commands/1234").with(
.to_return(body: { ok: true }.to_json) body: /attachments/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post", params: {
response_url: 'https://hooks.slack.com/commands/1234', text: "post",
channel_name: 'general', response_url: "https://hooks.slack.com/commands/1234",
channel_id: 'C6029G78F', channel_name: "general",
token: token channel_id: "C6029G78F",
} token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
end end
it 'can select by url' do it "can select by url" do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.with(body: /1501801629\.052212/) stub_request(:post, "https://slack.com/commands/1234").with(
.to_return(body: { ok: true }.to_json) body: /1501801629\.052212/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212", params: {
response_url: 'https://hooks.slack.com/commands/1234', text:
channel_name: 'general', "post https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
channel_id: 'C6029G78F', response_url: "https://hooks.slack.com/commands/1234",
token: token channel_name: "general",
} channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
end end
it 'can select by url with thread parameter' do it "can select by url with thread parameter" do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies") replies_stub =
.with(body: /1501801629\.052212/) stub_request(:post, "https://slack.com/api/conversations.replies").with(
.to_return(body: { ok: true, messages: messages_fixture }.to_json) body: /1501801629\.052212/,
).to_return(body: { ok: true, messages: messages_fixture }.to_json)
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.to_return(body: { ok: true }.to_json) stub_request(:post, "https://slack.com/commands/1234").to_return(
body: { ok: true }.to_json,
)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post https://sometestslack.slack.com/archives/C6029G78F/p1501201669054212?thread_ts=1501801629.052212", params: {
response_url: 'https://hooks.slack.com/commands/1234', text:
channel_name: 'general', "post https://sometestslack.slack.com/archives/C6029G78F/p1501201669054212?thread_ts=1501801629.052212",
channel_id: 'C6029G78F', response_url: "https://hooks.slack.com/commands/1234",
token: token channel_name: "general",
} channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
expect(replies_stub).to have_been_requested expect(replies_stub).to have_been_requested
end end
it 'can select by thread' do it "can select by thread" do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies") replies_stub =
.with(body: /1501801629\.052212/) stub_request(:post, "https://slack.com/api/conversations.replies").with(
.to_return(body: { ok: true, messages: messages_fixture }.to_json) body: /1501801629\.052212/,
).to_return(body: { ok: true, messages: messages_fixture }.to_json)
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.to_return(body: { ok: true }.to_json) stub_request(:post, "https://slack.com/commands/1234").to_return(
body: { ok: true }.to_json,
)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post thread https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212", params: {
response_url: 'https://hooks.slack.com/commands/1234', text:
channel_name: 'general', "post thread https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
channel_id: 'C6029G78F', response_url: "https://hooks.slack.com/commands/1234",
token: token channel_name: "general",
} channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
expect(replies_stub).to have_been_requested expect(replies_stub).to have_been_requested
end end
it 'can select by count' do it "can select by count" do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.with(body: /1501801629\.052212/) stub_request(:post, "https://slack.com/commands/1234").with(
.to_return(body: { ok: true }.to_json) body: /1501801629\.052212/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post 4", params: {
response_url: 'https://hooks.slack.com/commands/1234', text: "post 4",
channel_name: 'general', response_url: "https://hooks.slack.com/commands/1234",
channel_id: 'C6029G78F', channel_name: "general",
token: token channel_id: "C6029G78F",
} token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
end end
it 'can auto select' do it "can auto select" do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.with(body: /1501615820\.949638/) stub_request(:post, "https://slack.com/commands/1234").with(
.to_return(body: { ok: true }.to_json) body: /1501615820\.949638/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post", params: {
response_url: 'https://hooks.slack.com/commands/1234', text: "post",
channel_name: 'general', response_url: "https://hooks.slack.com/commands/1234",
channel_id: 'C6029G78F', channel_name: "general",
token: token channel_id: "C6029G78F",
} token: token,
}
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
end end
it "supports using shortcuts to create a thread transcript" do it "supports using shortcuts to create a thread transcript" do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies") replies_stub =
.with(body: /1501801629\.052212/) stub_request(:post, "https://slack.com/api/conversations.replies").with(
.to_return(body: { ok: true, messages: messages_fixture }.to_json) body: /1501801629\.052212/,
).to_return(body: { ok: true, messages: messages_fixture }.to_json)
view_open_stub = stub_request(:post, "https://slack.com/api/views.open") view_open_stub =
.with(body: /TRIGGERID/) stub_request(:post, "https://slack.com/api/views.open").with(
.to_return(body: { ok: true, view: { id: "VIEWID" } }.to_json) body: /TRIGGERID/,
).to_return(body: { ok: true, view: { id: "VIEWID" } }.to_json)
view_update_stub = stub_request(:post, "https://slack.com/api/views.update") view_update_stub =
.with(body: /VIEWID/) stub_request(:post, "https://slack.com/api/views.update").with(
.to_return(body: { ok: true }.to_json) body: /VIEWID/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/interactive.json", params: { post "/chat-integration/slack/interactive.json",
payload: { params: {
type: "message_action", payload: {
channel: { name: 'general', id: 'C6029G78F' }, type: "message_action",
trigger_id: "TRIGGERID", channel: {
message: { thread_ts: "1501801629.052212" }, name: "general",
token: token id: "C6029G78F",
}.to_json },
} trigger_id: "TRIGGERID",
message: {
thread_ts: "1501801629.052212",
},
token: token,
}.to_json,
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(view_open_stub).to have_been_requested expect(view_open_stub).to have_been_requested
expect(view_update_stub).to have_been_requested expect(view_update_stub).to have_been_requested
end end
end end
it 'deals with failed API calls correctly' do it "deals with failed API calls correctly" do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub =
.with(body: { text: I18n.t("chat_integration.provider.slack.transcript.error_users") }) stub_request(:post, "https://slack.com/commands/1234").with(
.to_return(body: { ok: true }.to_json) body: {
text: I18n.t("chat_integration.provider.slack.transcript.error_users"),
},
).to_return(body: { ok: true }.to_json)
stub_request(:post, "https://slack.com/api/users.list").to_return(status: 403) stub_request(:post, "https://slack.com/api/users.list").to_return(status: 403)
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post 2", params: {
response_url: 'https://hooks.slack.com/commands/1234', text: "post 2",
channel_name: 'general', response_url: "https://hooks.slack.com/commands/1234",
channel_id: 'C6029G78F', channel_name: "general",
token: token channel_id: "C6029G78F",
} token: token,
}
json = response.parsed_body json = response.parsed_body
expect(json["text"]).to include(I18n.t("chat_integration.provider.slack.transcript.loading")) expect(json["text"]).to include(
I18n.t("chat_integration.provider.slack.transcript.loading"),
)
expect(command_stub).to have_been_requested expect(command_stub).to have_been_requested
end end
it 'errors correctly if there is no api key' do it "errors correctly if there is no api key" do
SiteSetting.chat_integration_slack_access_token = '' SiteSetting.chat_integration_slack_access_token = ""
post "/chat-integration/slack/command.json", params: { post "/chat-integration/slack/command.json",
text: "post 2", params: {
response_url: 'https://hooks.slack.com/commands/1234', text: "post 2",
channel_name: 'general', response_url: "https://hooks.slack.com/commands/1234",
channel_id: 'C6029G78F', channel_name: "general",
token: token channel_id: "C6029G78F",
} token: token,
}
json = response.parsed_body json = response.parsed_body
expect(json["text"]).to include(I18n.t("chat_integration.provider.slack.transcript.api_required")) expect(json["text"]).to include(
I18n.t("chat_integration.provider.slack.transcript.api_required"),
)
end end
end end
end end
end end
end end

View File

@ -1,31 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackMessageFormatter do RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackMessageFormatter do
describe '.format' do describe ".format" do
context 'with links' do context "with links" do
it 'should return the right message' do it "should return the right message" do
expect(described_class.format("<a href='http://somepath.com'>test</a>")) expect(described_class.format("<a href='http://somepath.com'>test</a>")).to eq(
.to eq('<http://somepath.com|test>') "<http://somepath.com|test>",
)
end end
describe 'when text contains a link with an incomplete URL' do describe "when text contains a link with an incomplete URL" do
it 'should return the right message' do it "should return the right message" do
expect(described_class.format("test <a href='//localhost:3000/some/path'></a>")) expect(described_class.format("test <a href='//localhost:3000/some/path'></a>")).to eq(
.to eq("test <http://localhost:3000/some/path|>") "test <http://localhost:3000/some/path|>",
)
SiteSetting.force_https = true SiteSetting.force_https = true
expect(described_class.format("test <a href='//localhost:3000/some/path'></a>")) expect(described_class.format("test <a href='//localhost:3000/some/path'></a>")).to eq(
.to eq("test <https://localhost:3000/some/path|>") "test <https://localhost:3000/some/path|>",
)
end end
end end
it "should not raise an error with unparseable urls" do it "should not raise an error with unparseable urls" do
expect(described_class.format("<a>test</a>")).to eq("<test.localhost|test>") expect(described_class.format("<a>test</a>")).to eq("<test.localhost|test>")
end end
end end
end end
end end

View File

@ -1,24 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.excerpt' do describe ".excerpt" do
describe 'when post contains emoijs' do describe "when post contains emoijs" do
before do before { post.update!(raw: ":slight_smile: This is a test") }
post.update!(raw: ':slight_smile: This is a test')
end
it 'should return the right excerpt' do it "should return the right excerpt" do
expect(described_class.excerpt(post)).to eq('🙂 This is a test') expect(described_class.excerpt(post)).to eq("🙂 This is a test")
end end
end end
describe 'when post contains onebox' do describe "when post contains onebox" do
it 'should return the right excerpt' do it "should return the right excerpt" do
post.update!(cooked: <<~COOKED post.update!(cooked: <<~COOKED)
<aside class=\"onebox whitelistedgeneric\"> <aside class=\"onebox whitelistedgeneric\">
<header class=\"source\"> <header class=\"source\">
<a href=\"http://somesource.com\"> <a href=\"http://somesource.com\">
@ -45,59 +43,97 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
<div style=\"clear: both\"></div> <div style=\"clear: both\"></div>
</aside> </aside>
COOKED COOKED
)
expect(described_class.excerpt(post)) expect(described_class.excerpt(post)).to eq("<http://somesource.com|meta.discourse.org>")
.to eq('<http://somesource.com|meta.discourse.org>')
end end
end end
describe 'when post contains an email' do describe "when post contains an email" do
it 'should return the right excerpt' do it "should return the right excerpt" do
post.update!(cooked: <<~COOKED post.update!(cooked: <<~COOKED)
The address is <a href=\"mailto:someone@domain.com\">my email</a> The address is <a href=\"mailto:someone@domain.com\">my email</a>
COOKED COOKED
)
expect(described_class.excerpt(post)) expect(described_class.excerpt(post)).to eq(
.to eq('The address is <mailto:someone@domain.com|my email>') "The address is <mailto:someone@domain.com|my email>",
)
end end
end end
end end
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_slack_outbound_webhook_url = "https://hooks.slack.com/services/abcde" SiteSetting.chat_integration_slack_outbound_webhook_url =
"https://hooks.slack.com/services/abcde"
SiteSetting.chat_integration_slack_enabled = true SiteSetting.chat_integration_slack_enabled = true
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'slack', data: { identifier: '#general' }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(provider: "slack", data: { identifier: "#general" })
end
it 'sends a webhook request' do it "sends a webhook request" do
stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(body: "success") stub1 =
stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(
body: "success",
)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(status: 400, body: "error") stub1 =
stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(
status: 400,
body: "error",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
describe 'with api token' do describe "with api token" do
before do before do
SiteSetting.chat_integration_slack_access_token = "magic" SiteSetting.chat_integration_slack_access_token = "magic"
@ts = "#{Time.now.to_i}.012345" @ts = "#{Time.now.to_i}.012345"
@ts2 = "#{Time.now.to_i}.012346" @ts2 = "#{Time.now.to_i}.012346"
@stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(body: "success") @stub1 =
@stub2 = stub_request(:post, %r{https://slack.com/api/chat.postMessage}).to_return(body: "{\"ok\":true, \"ts\": \"#{@ts}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }", headers: { 'Content-Type' => 'application/json' }) stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(
@thread_stub = stub_request(:post, %r{https://slack.com/api/chat.postMessage}).with(body: hash_including("thread_ts" => @ts)).to_return(body: "{\"ok\":true, \"ts\": \"#{@ts}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }", headers: { 'Content-Type' => 'application/json' }) body: "success",
@thread_stub2 = stub_request(:post, %r{https://slack.com/api/chat.postMessage}).with(body: hash_including("thread_ts" => @ts2)).to_return(body: "{\"ok\":true, \"ts\": \"#{@ts2}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }", headers: { 'Content-Type' => 'application/json' }) )
@stub2 =
stub_request(:post, %r{https://slack.com/api/chat.postMessage}).to_return(
body:
"{\"ok\":true, \"ts\": \"#{@ts}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }",
headers: {
"Content-Type" => "application/json",
},
)
@thread_stub =
stub_request(:post, %r{https://slack.com/api/chat.postMessage}).with(
body: hash_including("thread_ts" => @ts),
).to_return(
body:
"{\"ok\":true, \"ts\": \"#{@ts}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }",
headers: {
"Content-Type" => "application/json",
},
)
@thread_stub2 =
stub_request(:post, %r{https://slack.com/api/chat.postMessage}).with(
body: hash_including("thread_ts" => @ts2),
).to_return(
body:
"{\"ok\":true, \"ts\": \"#{@ts2}\", \"message\": {\"attachments\": [], \"username\":\"blah\", \"text\":\"blah2\"} }",
headers: {
"Content-Type" => "application/json",
},
)
end end
it 'sends an api request' do it "sends an api request" do
expect(@stub2).to have_been_requested.times(0) expect(@stub2).to have_been_requested.times(0)
expect(@thread_stub).to have_been_requested.times(0) expect(@thread_stub).to have_been_requested.times(0)
@ -108,7 +144,7 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
expect(@thread_stub).to have_been_requested.times(0) expect(@thread_stub).to have_been_requested.times(0)
end end
it 'sends thread id for thread' do it "sends thread id for thread" do
expect(@thread_stub).to have_been_requested.times(0) expect(@thread_stub).to have_been_requested.times(0)
rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread") rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread")
@ -118,9 +154,15 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
expect(@thread_stub).to have_been_requested.once expect(@thread_stub).to have_been_requested.once
end end
it 'tracks threading in different channels separately' do it "tracks threading in different channels separately" do
expect(@thread_stub).to have_been_requested.times(0) expect(@thread_stub).to have_been_requested.times(0)
chan2 = DiscourseChatIntegration::Channel.create(provider: 'dummy2', data: { "identifier" => "#random" }) chan2 =
DiscourseChatIntegration::Channel.create(
provider: "dummy2",
data: {
"identifier" => "#random",
},
)
rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread") rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread")
rule2 = DiscourseChatIntegration::Rule.create(channel: chan2, filter: "thread") rule2 = DiscourseChatIntegration::Rule.create(channel: chan2, filter: "thread")
@ -137,12 +179,11 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
expect(described_class.get_slack_thread_ts(post.topic, "#random")).to eq(@ts2) expect(described_class.get_slack_thread_ts(post.topic, "#random")).to eq(@ts2)
end end
it 'recognizes slack thread ts in comment' do it "recognizes slack thread ts in comment" do
post.update!(cooked: "cooked", raw: <<~RAW post.update!(cooked: "cooked", raw: <<~RAW)
My fingers are typing words that improve `raw_quality` My fingers are typing words that improve `raw_quality`
<!--SLACK_CHANNEL_ID=#general;SLACK_TS=#{@ts}--> <!--SLACK_CHANNEL_ID=#general;SLACK_TS=#{@ts}-->
RAW RAW
)
rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread") rule = DiscourseChatIntegration::Rule.create(channel: chan1, filter: "thread")
@ -152,14 +193,19 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
expect(@thread_stub).to have_been_requested.times(1) expect(@thread_stub).to have_been_requested.times(1)
end end
it 'handles errors correctly' do it "handles errors correctly" do
@stub2 = stub_request(:post, %r{https://slack.com/api/chat.postMessage}).to_return(body: "{\"ok\":false }", headers: { 'Content-Type' => 'application/json' }) @stub2 =
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) stub_request(:post, %r{https://slack.com/api/chat.postMessage}).to_return(
body: "{\"ok\":false }",
headers: {
"Content-Type" => "application/json",
},
)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(@stub2).to have_been_requested.once expect(@stub2).to have_been_requested.once
end end
end end
end end
end end

View File

@ -1,77 +1,69 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscript do RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscript do
before do before { Discourse.cache.clear }
Discourse.cache.clear
end
let(:messages_fixture) { let(:messages_fixture) do
[ [
{ {
"type": "message", type: "message",
"user": "U6JSSESES", user: "U6JSSESES",
"text": "Yeah, should make posting slack transcripts much easier", text: "Yeah, should make posting slack transcripts much easier",
"ts": "1501801665.062694" ts: "1501801665.062694",
}, },
{ {
"type": "message", type: "message",
"user": "U5Z773QLZ", user: "U5Z773QLZ",
"text": "Oooh a new discourse plugin <@U5Z773QLS> ???", text: "Oooh a new discourse plugin <@U5Z773QLS> ???",
"ts": "1501801643.056375" ts: "1501801643.056375",
},
{ type: "message", user: "U6E2W7R8C", text: "Which one?", ts: "1501801635.053761" },
{
type: "message",
user: "U6JSSESES",
text: "So, who's interested in the new <https://meta.discourse.org|discourse plugin>?",
ts: "1501801629.052212",
}, },
{ {
"type": "message", type: "message",
"user": "U6E2W7R8C", user: "U820GH3LA",
"text": "Which one?", text: "I'm interested!!",
"ts": "1501801635.053761" ts: "1501801634.053761",
thread_ts: "1501801629.052212",
}, },
{ {
"type": "message", text: "Check this out!",
"user": "U6JSSESES", username: "Test Community",
"text": "So, who's interested in the new <https://meta.discourse.org|discourse plugin>?", bot_id: "B6C6JNUDN",
"ts": "1501801629.052212" attachments: [
{
author_name: "@david",
fallback: "Discourse can now be integrated with Mattermost! - @david",
text: "Hey <http://localhost/groups/team|@team>, what do you think about this?",
title: "Discourse can now be integrated with Mattermost! [Announcements] ",
id: 1,
title_link:
"http://localhost:3000/t/discourse-can-now-be-integrated-with-mattermost/51/4",
color: "283890",
mrkdwn_in: ["text"],
},
],
type: "message",
subtype: "bot_message",
ts: "1501615820.949638",
}, },
{ {
"type": "message", type: "message",
"user": "U820GH3LA", user: "U5Z773QLS",
"text": "I'm interested!!", text: "Lets try some *bold text* <@U5Z773QLZ> <@someotheruser>",
"ts": "1501801634.053761", ts: "1501093331.439776",
"thread_ts": "1501801629.052212"
}, },
{
"text": "Check this out!",
"username": "Test Community",
"bot_id": "B6C6JNUDN",
"attachments": [
{
"author_name": "@david",
"fallback": "Discourse can now be integrated with Mattermost! - @david",
"text": "Hey <http://localhost/groups/team|@team>, what do you think about this?",
"title": "Discourse can now be integrated with Mattermost! [Announcements] ",
"id": 1,
"title_link": "http://localhost:3000/t/discourse-can-now-be-integrated-with-mattermost/51/4",
"color": "283890",
"mrkdwn_in": [
"text"
]
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1501615820.949638"
},
{
"type": "message",
"user": "U5Z773QLS",
"text": "Lets try some *bold text* <@U5Z773QLZ> <@someotheruser>",
"ts": "1501093331.439776"
},
] ]
} end
let(:users_fixture) { let(:users_fixture) do
[ [
{ {
id: "U6JSSESES", id: "U6JSSESES",
@ -79,8 +71,8 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
profile: { profile: {
image_24: "https://example.com/avatar", image_24: "https://example.com/avatar",
display_name: "Threader", display_name: "Threader",
real_name: "A. Threader" real_name: "A. Threader",
} },
}, },
{ {
id: "U820GH3LA", id: "U820GH3LA",
@ -88,8 +80,8 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
profile: { profile: {
image_24: "https://example.com/avatar", image_24: "https://example.com/avatar",
display_name: "Responder", display_name: "Responder",
real_name: "A. Responder" real_name: "A. Responder",
} },
}, },
{ {
id: "U5Z773QLS", id: "U5Z773QLS",
@ -97,8 +89,8 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
profile: { profile: {
image_24: "https://example.com/avatar", image_24: "https://example.com/avatar",
display_name: "awesomeguy", display_name: "awesomeguy",
real_name: "actually just a guy" real_name: "actually just a guy",
} },
}, },
{ {
id: "U5Z773QLZ", id: "U5Z773QLZ",
@ -106,196 +98,221 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
profile: { profile: {
image_24: "https://example.com/avatar", image_24: "https://example.com/avatar",
display_name: "", display_name: "",
real_name: "another guy" real_name: "another guy",
} },
} },
] ]
} end
let(:transcript) { described_class.new(channel_name: "#general", channel_id: "G1234") } let(:transcript) { described_class.new(channel_name: "#general", channel_id: "G1234") }
before do before { SiteSetting.chat_integration_slack_access_token = "abcde" }
SiteSetting.chat_integration_slack_access_token = "abcde"
end
it "doesn't raise an error when there are no messages to guess" do it "doesn't raise an error when there are no messages to guess" do
transcript.instance_variable_set(:@messages, []) transcript.instance_variable_set(:@messages, [])
expect(transcript.guess_first_message(skip_messages: 1)).to eq(false) expect(transcript.guess_first_message(skip_messages: 1)).to eq(false)
end end
describe 'loading users' do describe "loading users" do
it 'loads users correctly' do it "loads users correctly" do
stub_request(:post, "https://slack.com/api/users.list") stub_request(:post, "https://slack.com/api/users.list").with(
.with(body: { token: "abcde", "cursor": nil, "limit": "200" }) body: {
.to_return(status: 200, body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json) token: "abcde",
cursor: nil,
limit: "200",
},
).to_return(
status: 200,
body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json,
)
expect(transcript.load_user_data).to be_truthy expect(transcript.load_user_data).to be_truthy
end end
it 'handles failed connection' do it "handles failed connection" do
stub_request(:post, "https://slack.com/api/users.list") stub_request(:post, "https://slack.com/api/users.list").to_return(status: 500, body: "")
.to_return(status: 500, body: '')
expect(transcript.load_user_data).to eq(false) expect(transcript.load_user_data).to eq(false)
end end
it 'handles slack failure' do it "handles slack failure" do
stub_request(:post, "https://slack.com/api/users.list") stub_request(:post, "https://slack.com/api/users.list").to_return(
.to_return(status: 200, body: { ok: false }.to_json) status: 200,
body: { ok: false }.to_json,
)
expect(transcript.load_user_data).to eq(false) expect(transcript.load_user_data).to eq(false)
end end
end end
context 'with loaded users' do context "with loaded users" do
before do before do
stub_request(:post, "https://slack.com/api/users.list") stub_request(:post, "https://slack.com/api/users.list").to_return(
.to_return(status: 200, body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json) status: 200,
body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json,
)
transcript.load_user_data transcript.load_user_data
end end
describe 'loading history' do describe "loading history" do
it 'loads messages correctly' do it "loads messages correctly" do
stub_request(:post, "https://slack.com/api/conversations.history") stub_request(:post, "https://slack.com/api/conversations.history").with(
.with(body: hash_including(token: "abcde", channel: 'G1234')) body: hash_including(token: "abcde", channel: "G1234"),
.to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json) ).to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json)
expect(transcript.load_chat_history).to be_truthy expect(transcript.load_chat_history).to be_truthy
end end
it 'handles failed connection' do it "handles failed connection" do
stub_request(:post, "https://slack.com/api/conversations.history") stub_request(:post, "https://slack.com/api/conversations.history").to_return(
.to_return(status: 500, body: {}.to_json) status: 500,
body: {}.to_json,
)
expect(transcript.load_chat_history).to be_falsey expect(transcript.load_chat_history).to be_falsey
end end
it 'handles slack failure' do it "handles slack failure" do
stub_request(:post, "https://slack.com/api/conversations.history") stub_request(:post, "https://slack.com/api/conversations.history").to_return(
.to_return(status: 200, body: { ok: false }.to_json) status: 200,
body: { ok: false }.to_json,
)
expect(transcript.load_chat_history).to be_falsey expect(transcript.load_chat_history).to be_falsey
end end
end end
context 'with thread_ts specified' do context "with thread_ts specified" do
let(:thread_transcript) { described_class.new(channel_name: "#general", channel_id: "G1234", requested_thread_ts: "1501801629.052212") } let(:thread_transcript) do
described_class.new(
channel_name: "#general",
channel_id: "G1234",
requested_thread_ts: "1501801629.052212",
)
end
before do before do
thread_transcript.load_user_data thread_transcript.load_user_data
stub_request(:post, "https://slack.com/api/conversations.replies") stub_request(:post, "https://slack.com/api/conversations.replies").with(
.with(body: hash_including(token: "abcde", channel: 'G1234', ts: "1501801629.052212")) body: hash_including(token: "abcde", channel: "G1234", ts: "1501801629.052212"),
.to_return(status: 200, body: { ok: true, messages: messages_fixture[3..4] }.to_json) ).to_return(status: 200, body: { ok: true, messages: messages_fixture[3..4] }.to_json)
thread_transcript.load_chat_history thread_transcript.load_chat_history
end end
it 'includes messages in a thread' do it "includes messages in a thread" do
expect(thread_transcript.messages.length).to eq(2) expect(thread_transcript.messages.length).to eq(2)
end end
it 'loads in chronological order' do # replies API presents messages in actual chronological order it "loads in chronological order" do # replies API presents messages in actual chronological order
expect(thread_transcript.messages.first.ts).to eq('1501801629.052212') expect(thread_transcript.messages.first.ts).to eq("1501801629.052212")
end end
it 'includes slack thread identifiers in body' do it "includes slack thread identifiers in body" do
text = thread_transcript.build_transcript text = thread_transcript.build_transcript
expect(text).to include("<!--SLACK_CHANNEL_ID=#general;SLACK_TS=1501801629.052212-->") expect(text).to include("<!--SLACK_CHANNEL_ID=#general;SLACK_TS=1501801629.052212-->")
end end
end end
context 'with loaded messages' do context "with loaded messages" do
before do before do
stub_request(:post, "https://slack.com/api/conversations.history") stub_request(:post, "https://slack.com/api/conversations.history").with(
.with(body: hash_including(token: "abcde", channel: 'G1234')) body: hash_including(token: "abcde", channel: "G1234"),
.to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json) ).to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json)
transcript.load_chat_history transcript.load_chat_history
end end
it 'ignores messages in a thread' do it "ignores messages in a thread" do
expect(transcript.messages.length).to eq(6) expect(transcript.messages.length).to eq(6)
end end
it 'loads in chronological order' do # API presents in reverse chronological it "loads in chronological order" do # API presents in reverse chronological
expect(transcript.messages.first.ts).to eq('1501093331.439776') expect(transcript.messages.first.ts).to eq("1501093331.439776")
end end
it 'handles bold text' do it "handles bold text" do
expect(transcript.messages.first.text).to start_with("Lets try some **bold text** ") expect(transcript.messages.first.text).to start_with("Lets try some **bold text** ")
end end
it 'handles links' do it "handles links" do
expect(transcript.messages[2].text).to eq("So, who's interested in the new [discourse plugin](https://meta.discourse.org)?") expect(transcript.messages[2].text).to eq(
"So, who's interested in the new [discourse plugin](https://meta.discourse.org)?",
)
end end
it 'includes attachments' do it "includes attachments" do
expect(transcript.messages[1].attachments.first).to eq("Discourse can now be integrated with Mattermost! - @david") expect(transcript.messages[1].attachments.first).to eq(
"Discourse can now be integrated with Mattermost! - @david",
)
end end
it 'can generate URL' do it "can generate URL" do
expect(transcript.messages.first.url).to eq("https://slack.com/archives/G1234/p1501093331439776") expect(transcript.messages.first.url).to eq(
"https://slack.com/archives/G1234/p1501093331439776",
)
end end
it 'includes attachments in raw text' do it "includes attachments in raw text" do
transcript.set_first_message_by_ts('1501615820.949638') transcript.set_first_message_by_ts("1501615820.949638")
expect(transcript.first_message.raw_text).to eq("Check this out!\n - Discourse can now be integrated with Mattermost! - @david\n") expect(transcript.first_message.raw_text).to eq(
"Check this out!\n - Discourse can now be integrated with Mattermost! - @david\n",
)
end end
it 'gives correct first and last messages' do it "gives correct first and last messages" do
expect(transcript.first_message_number).to eq(0) expect(transcript.first_message_number).to eq(0)
expect(transcript.last_message_number).to eq(transcript.messages.length - 1) expect(transcript.last_message_number).to eq(transcript.messages.length - 1)
expect(transcript.first_message.ts).to eq('1501093331.439776') expect(transcript.first_message.ts).to eq("1501093331.439776")
expect(transcript.last_message.ts).to eq('1501801665.062694') expect(transcript.last_message.ts).to eq("1501801665.062694")
end end
it 'can change first and last messages by index' do it "can change first and last messages by index" do
expect(transcript.set_first_message_by_index(999)).to be_falsey expect(transcript.set_first_message_by_index(999)).to be_falsey
expect(transcript.set_first_message_by_index(1)).to be_truthy expect(transcript.set_first_message_by_index(1)).to be_truthy
expect(transcript.set_last_message_by_index(-2)).to be_truthy expect(transcript.set_last_message_by_index(-2)).to be_truthy
expect(transcript.first_message.ts).to eq('1501615820.949638') expect(transcript.first_message.ts).to eq("1501615820.949638")
expect(transcript.last_message.ts).to eq('1501801643.056375') expect(transcript.last_message.ts).to eq("1501801643.056375")
end end
it 'can change first and last messages by ts' do it "can change first and last messages by ts" do
expect(transcript.set_first_message_by_ts('blah')).to be_falsey expect(transcript.set_first_message_by_ts("blah")).to be_falsey
expect(transcript.set_first_message_by_ts('1501615820.949638')).to be_truthy expect(transcript.set_first_message_by_ts("1501615820.949638")).to be_truthy
expect(transcript.set_last_message_by_ts('1501801629.052212')).to be_truthy expect(transcript.set_last_message_by_ts("1501801629.052212")).to be_truthy
expect(transcript.first_message_number).to eq(1) expect(transcript.first_message_number).to eq(1)
expect(transcript.last_message_number).to eq(2) expect(transcript.last_message_number).to eq(2)
end end
it 'can guess the first message' do it "can guess the first message" do
expect(transcript.guess_first_message(skip_messages: 1)).to eq(true) expect(transcript.guess_first_message(skip_messages: 1)).to eq(true)
expect(transcript.first_message.ts).to eq('1501801629.052212') expect(transcript.first_message.ts).to eq("1501801629.052212")
end end
it 'handles usernames correctly' do it "handles usernames correctly" do
expect(transcript.first_message.username).to eq('awesomeguy') # Normal user expect(transcript.first_message.username).to eq("awesomeguy") # Normal user
expect(transcript.messages[1].username).to eq('Test_Community') # Bot user expect(transcript.messages[1].username).to eq("Test_Community") # Bot user
expect(transcript.messages[3].username).to eq(nil) # Unknown normal user expect(transcript.messages[3].username).to eq(nil) # Unknown normal user
# Normal user, display_name not set (fall back to real_name) # Normal user, display_name not set (fall back to real_name)
expect(transcript.messages[4].username).to eq('another_guy') expect(transcript.messages[4].username).to eq("another_guy")
end end
it 'handles user mentions correctly' do it "handles user mentions correctly" do
# User with display_name not set, unrecognized user # User with display_name not set, unrecognized user
expect(transcript.first_message.text).to \ expect(transcript.first_message.text).to eq(
eq('Lets try some **bold text** @another_guy @someotheruser') "Lets try some **bold text** @another_guy @someotheruser",
)
# Normal user # Normal user
expect(transcript.messages[4].text).to \ expect(transcript.messages[4].text).to eq("Oooh a new discourse plugin @awesomeguy ???")
eq('Oooh a new discourse plugin @awesomeguy ???')
end end
it 'handles avatars correctly' do it "handles avatars correctly" do
expect(transcript.first_message.avatar).to eq("https://example.com/avatar") # Normal user expect(transcript.first_message.avatar).to eq("https://example.com/avatar") # Normal user
expect(transcript.messages[1].avatar).to eq(nil) # Bot user expect(transcript.messages[1].avatar).to eq(nil) # Bot user
end end
it 'creates a transcript correctly' do it "creates a transcript correctly" do
transcript.set_last_message_by_index(1) transcript.set_last_message_by_index(1)
text = transcript.build_transcript text = transcript.build_transcript
@ -317,7 +334,7 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
expect(text).to eq(expected) expect(text).to eq(expected)
end end
it 'omits quote tags when disabled' do it "omits quote tags when disabled" do
transcript.set_last_message_by_index(1) transcript.set_last_message_by_index(1)
text = transcript.build_transcript text = transcript.build_transcript
@ -331,7 +348,7 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
expect(text).not_to include("[/quote]") expect(text).not_to include("[/quote]")
end end
it 'creates the slack UI correctly' do it "creates the slack UI correctly" do
transcript.set_last_message_by_index(1) transcript.set_last_message_by_index(1)
ui = transcript.build_slack_ui ui = transcript.build_slack_ui
@ -352,16 +369,17 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
end end
describe "message formatting" do describe "message formatting" do
it 'handles code block newlines' do it "handles code block newlines" do
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "Here is some code```my code\nwith newline```", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" => "Here is some code```my code\nwith newline```",
}, "ts" => "1501093331.439776",
transcript },
) transcript,
)
expect(message.text).to eq(<<~MD) expect(message.text).to eq(<<~MD)
Here is some code Here is some code
``` ```
@ -371,16 +389,18 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
MD MD
end end
it 'handles multiple code blocks' do it "handles multiple code blocks" do
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "Here is some code```my code\nwith newline```and another```some more code```", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" =>
}, "Here is some code```my code\nwith newline```and another```some more code```",
transcript "ts" => "1501093331.439776",
) },
transcript,
)
expect(message.text).to eq(<<~MD) expect(message.text).to eq(<<~MD)
Here is some code Here is some code
``` ```
@ -394,73 +414,83 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider::SlackTranscrip
MD MD
end end
it 'handles strikethrough' do it "handles strikethrough" do
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "Some ~strikethrough~", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" => "Some ~strikethrough~",
}, "ts" => "1501093331.439776",
transcript },
) transcript,
)
expect(message.text).to eq("Some ~~strikethrough~~") expect(message.text).to eq("Some ~~strikethrough~~")
end end
it 'handles slack links' do it "handles slack links" do
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "A link to <https://google.com|google>, <https://autolinked.com|https://autolinked.com>, <https://notext.com>, <#channel>, <@user>", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" =>
}, "A link to <https://google.com|google>, <https://autolinked.com|https://autolinked.com>, <https://notext.com>, <#channel>, <@user>",
transcript "ts" => "1501093331.439776",
},
transcript,
)
expect(message.text).to eq(
"A link to [google](https://google.com), <https://autolinked.com>, <https://notext.com>, #channel, @user",
) )
expect(message.text).to eq("A link to [google](https://google.com), <https://autolinked.com>, <https://notext.com>, #channel, @user")
end end
it 'does not format things inside backticks' do it "does not format things inside backticks" do
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "You can strikethrough like `~this~`, bold like `*this*` and link like `[https://example.com](https://example.com)`", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" =>
}, "You can strikethrough like `~this~`, bold like `*this*` and link like `[https://example.com](https://example.com)`",
transcript "ts" => "1501093331.439776",
},
transcript,
)
expect(message.text).to eq(
"You can strikethrough like `~this~`, bold like `*this*` and link like `[https://example.com](https://example.com)`",
) )
expect(message.text).to eq("You can strikethrough like `~this~`, bold like `*this*` and link like `[https://example.com](https://example.com)`")
end end
it 'unescapes html in backticks' do it "unescapes html in backticks" do
# Because Slack escapes HTML entities, even in backticks # Because Slack escapes HTML entities, even in backticks
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "The code is `&lt;stuff&gt;`", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" => "The code is `&lt;stuff&gt;`",
}, "ts" => "1501093331.439776",
transcript },
) transcript,
)
expect(message.text).to eq("The code is `<stuff>`") expect(message.text).to eq("The code is `<stuff>`")
end end
it 'updates emoji dashes to underscores' do it "updates emoji dashes to underscores" do
# Discourse does not allow dashes in emoji names, so this helps communities have matching custom emojis # Discourse does not allow dashes in emoji names, so this helps communities have matching custom emojis
message = DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new( message =
{ DiscourseChatIntegration::Provider::SlackProvider::SlackMessage.new(
"type" => "message", {
"user" => "U5Z773QLS", "type" => "message",
"text" => "This is :my-emoji:", "user" => "U5Z773QLS",
"ts" => "1501093331.439776" "text" => "This is :my-emoji:",
}, "ts" => "1501093331.439776",
transcript },
) transcript,
)
expect(message.text).to eq("This is :my_emoji:") expect(message.text).to eq("This is :my_emoji:")
end end
end end
end end
end end

View File

@ -1,42 +1,47 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::TeamsProvider do RSpec.describe DiscourseChatIntegration::Provider::TeamsProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_teams_enabled = true }
SiteSetting.chat_integration_teams_enabled = true
let(:chan1) do
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",
},
)
end end
let(:chan1) { 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' }) } it "sends a webhook request" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(body: "1")
it 'sends a webhook request' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(body: "1")
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 400, body: "{}") stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(status: 400, body: "{}")
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
describe 'with nil user.name' do describe "with nil user.name" do
before do before { post.user.update!(name: nil) }
post.user.update!(name: nil)
end
it 'handles nil username correctly' do it "handles nil username correctly" do
message = described_class.get_message(post) message = described_class.get_message(post)
name = message[:sections].first[:facts].first[:name] name = message[:sections].first[:facts].first[:name]
expect(name).to eq("") expect(name).to eq("")
end end
end end
end end
end end

View File

@ -1,32 +1,44 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe 'Telegram Command Controller', type: :request do describe "Telegram Command Controller", type: :request do
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let!(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'telegram', data: { name: 'Amazing Channel', chat_id: '123' }) } let!(:chan1) do
let!(:webhook_stub) { stub_request(:post, 'https://api.telegram.org/botTOKEN/setWebhook').to_return(body: "{\"ok\":true}") } DiscourseChatIntegration::Channel.create!(
provider: "telegram",
data: {
name: "Amazing Channel",
chat_id: "123",
},
)
end
let!(:webhook_stub) do
stub_request(:post, "https://api.telegram.org/botTOKEN/setWebhook").to_return(
body: "{\"ok\":true}",
)
end
describe 'with plugin disabled' do describe "with plugin disabled" do
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/telegram/command/abcd.json' post "/chat-integration/telegram/command/abcd.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'with plugin enabled and provider disabled' do describe "with plugin enabled and provider disabled" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_telegram_enabled = false SiteSetting.chat_integration_telegram_enabled = false
end end
it 'should return a 404' do it "should return a 404" do
post '/chat-integration/telegram/command/abcd.json' post "/chat-integration/telegram/command/abcd.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'slash commands endpoint' do describe "slash commands endpoint" do
before do before do
SiteSetting.chat_integration_enabled = true SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_telegram_access_token = "TOKEN" SiteSetting.chat_integration_telegram_access_token = "TOKEN"
@ -34,85 +46,122 @@ describe 'Telegram Command Controller', type: :request do
SiteSetting.chat_integration_telegram_secret = "shhh" SiteSetting.chat_integration_telegram_secret = "shhh"
end end
let!(:stub) { stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":true}") } let!(:stub) do
stub_request(:post, "https://api.telegram.org/botTOKEN/sendMessage").to_return(
body: "{\"ok\":true}",
)
end
describe 'when forum is private' do describe "when forum is private" do
it 'should not redirect to login page' do it "should not redirect to login page" do
SiteSetting.login_required = true SiteSetting.login_required = true
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
message: { chat: { id: 123 }, text: '/help' } params: {
} message: {
chat: {
id: 123,
},
text: "/help",
},
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
end end
describe 'when the token is invalid' do describe "when the token is invalid" do
it 'should raise the right error' do it "should raise the right error" do
post '/chat-integration/telegram/command/blah.json', params: { post "/chat-integration/telegram/command/blah.json",
message: { chat: { id: 123 }, text: '/help' } params: {
} message: {
chat: {
id: 123,
},
text: "/help",
},
}
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
describe 'when token has not been set' do describe "when token has not been set" do
it 'should raise the right error' do it "should raise the right error" do
SiteSetting.chat_integration_telegram_access_token = "" SiteSetting.chat_integration_telegram_access_token = ""
post '/chat-integration/telegram/command/blah.json', params: { post "/chat-integration/telegram/command/blah.json",
message: { chat: { id: 123 }, text: '/help' } params: {
} message: {
chat: {
id: 123,
},
text: "/help",
},
}
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
describe 'when token is valid' do describe "when token is valid" do
let(:token) { "TOKEN" } let(:token) { "TOKEN" }
before do before { SiteSetting.chat_integration_telegram_enable_slash_commands = true }
SiteSetting.chat_integration_telegram_enable_slash_commands = true
end
describe 'add new rule' do describe "add new rule" do
it "should add a new rule correctly" do
it 'should add a new rule correctly' do post "/chat-integration/telegram/command/shhh.json",
post '/chat-integration/telegram/command/shhh.json', params: { params: {
message: { chat: { id: 123 }, text: "/watch #{category.slug}" } message: {
} chat: {
id: 123,
},
text: "/watch #{category.slug}",
},
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(stub).to have_been_requested.once expect(stub).to have_been_requested.once
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1) expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
it 'should add a new rule correctly using group chat syntax' do it "should add a new rule correctly using group chat syntax" do
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
message: { chat: { id: 123 }, text: "/watch@my-awesome-bot #{category.slug}" } params: {
} message: {
chat: {
id: 123,
},
text: "/watch@my-awesome-bot #{category.slug}",
},
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(stub).to have_been_requested.once expect(stub).to have_been_requested.once
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
expect(rule.channel).to eq(chan1) expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil) expect(rule.tags).to eq(nil)
end end
describe 'from an unknown channel' do describe "from an unknown channel" do
it 'does nothing' do it "does nothing" do
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
message: { chat: { id: 456 }, text: "/watch #{category.slug}" } params: {
} message: {
chat: {
id: 456,
},
text: "/watch #{category.slug}",
},
}
expect(DiscourseChatIntegration::Rule.all.size).to eq(0) expect(DiscourseChatIntegration::Rule.all.size).to eq(0)
expect(DiscourseChatIntegration::Channel.all.size).to eq(1) expect(DiscourseChatIntegration::Channel.all.size).to eq(1)
@ -121,16 +170,28 @@ describe 'Telegram Command Controller', type: :request do
end end
it "should respond only to a specific command in a broadcast channel" do it "should respond only to a specific command in a broadcast channel" do
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
channel_post: { chat: { id: 123 }, text: "something" } params: {
} channel_post: {
chat: {
id: 123,
},
text: "something",
},
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(stub).to have_been_requested.times(0) expect(stub).to have_been_requested.times(0)
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
channel_post: { chat: { id: 123 }, text: "/getchatid" } params: {
} channel_post: {
chat: {
id: 123,
},
text: "/getchatid",
},
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(stub).to have_been_requested.times(1) expect(stub).to have_been_requested.times(1)
@ -138,9 +199,14 @@ describe 'Telegram Command Controller', type: :request do
context "when 'text' is missing" do context "when 'text' is missing" do
it "does not break" do it "does not break" do
post '/chat-integration/telegram/command/shhh.json', params: { post "/chat-integration/telegram/command/shhh.json",
message: { chat: { id: 123 } } params: {
} message: {
chat: {
id: 123,
},
},
}
expect(response).to have_http_status :ok expect(response).to have_http_status :ok
expect(DiscourseChatIntegration::Rule.count).to eq(0) expect(DiscourseChatIntegration::Rule.count).to eq(0)

View File

@ -1,33 +1,51 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::TelegramProvider do RSpec.describe DiscourseChatIntegration::Provider::TelegramProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
let!(:webhook_stub) { stub_request(:post, 'https://api.telegram.org/botTOKEN/setWebhook').to_return(body: "{\"ok\":true}") } let!(:webhook_stub) do
stub_request(:post, "https://api.telegram.org/botTOKEN/setWebhook").to_return(
body: "{\"ok\":true}",
)
end
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_telegram_access_token = "TOKEN" SiteSetting.chat_integration_telegram_access_token = "TOKEN"
SiteSetting.chat_integration_telegram_enabled = true SiteSetting.chat_integration_telegram_enabled = true
SiteSetting.chat_integration_telegram_secret = 'shhh' SiteSetting.chat_integration_telegram_secret = "shhh"
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'telegram', data: { name: "Awesome Channel", chat_id: '123' }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "telegram",
data: {
name: "Awesome Channel",
chat_id: "123",
},
)
end
it 'sends a webhook request' do it "sends a webhook request" do
stub1 = stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":true}") stub1 =
stub_request(:post, "https://api.telegram.org/botTOKEN/sendMessage").to_return(
body: "{\"ok\":true}",
)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":false, \"description\":\"chat not found\"}") stub1 =
stub_request(:post, "https://api.telegram.org/botTOKEN/sendMessage").to_return(
body: "{\"ok\":false, \"description\":\"chat not found\"}",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,30 +1,37 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::WebexProvider do RSpec.describe DiscourseChatIntegration::Provider::WebexProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before { SiteSetting.chat_integration_webex_enabled = true }
SiteSetting.chat_integration_webex_enabled = true
let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "webex",
data: {
name: "discourse",
webhook_url:
"https://webexapis.com/v1/webhooks/incoming/jAHJjVVQ1cgEwb4ikQQawIrGdUtlocKA9fSNvIyADQoYo0mI70pztWUDOu22gDRPJOEJtCsc688zi1RMa",
},
)
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'webex', data: { name: 'discourse', webhook_url: 'https://webexapis.com/v1/webhooks/incoming/jAHJjVVQ1cgEwb4ikQQawIrGdUtlocKA9fSNvIyADQoYo0mI70pztWUDOu22gDRPJOEJtCsc688zi1RMa' }) } it "sends a webhook request" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(body: "1")
it 'sends a webhook request' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(body: "1")
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 400, body: "{}") stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(status: 400, body: "{}")
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::ZulipProvider do RSpec.describe DiscourseChatIntegration::Provider::ZulipProvider do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
before do before do
SiteSetting.chat_integration_zulip_enabled = true SiteSetting.chat_integration_zulip_enabled = true
SiteSetting.chat_integration_zulip_server = "https://hello.world" SiteSetting.chat_integration_zulip_server = "https://hello.world"
@ -13,21 +13,33 @@ RSpec.describe DiscourseChatIntegration::Provider::ZulipProvider do
SiteSetting.chat_integration_zulip_bot_api_key = "secret" SiteSetting.chat_integration_zulip_bot_api_key = "secret"
end end
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'zulip', data: { stream: "general", subject: "Discourse Notifications" }) } let(:chan1) do
DiscourseChatIntegration::Channel.create!(
provider: "zulip",
data: {
stream: "general",
subject: "Discourse Notifications",
},
)
end
it 'sends a webhook request' do it "sends a webhook request" do
stub1 = stub_request(:post, 'https://hello.world/api/v1/messages').to_return(status: 200) stub1 = stub_request(:post, "https://hello.world/api/v1/messages").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil) described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
it 'handles errors correctly' do it "handles errors correctly" do
stub1 = stub_request(:post, 'https://hello.world/api/v1/messages').to_return(status: 400, body: '{}') stub1 =
stub_request(:post, "https://hello.world/api/v1/messages").to_return(
status: 400,
body: "{}",
)
expect(stub1).to have_been_requested.times(0) expect(stub1).to have_been_requested.times(0)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError) expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(stub1).to have_been_requested.once expect(stub1).to have_been_requested.once
end end
end end
end end

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_relative '../dummy_provider' require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Channel do RSpec.describe DiscourseChatIntegration::Channel do
include_context "with dummy provider" include_context "with dummy provider"
include_context "with validated dummy provider" include_context "with validated dummy provider"
it 'should save and load successfully' do it "should save and load successfully" do
expect(DiscourseChatIntegration::Channel.all.length).to eq(0) expect(DiscourseChatIntegration::Channel.all.length).to eq(0)
chan = DiscourseChatIntegration::Channel.create(provider: "dummy") chan = DiscourseChatIntegration::Channel.create(provider: "dummy")
@ -16,49 +16,53 @@ RSpec.describe DiscourseChatIntegration::Channel do
loadedChan = DiscourseChatIntegration::Channel.find(chan.id) loadedChan = DiscourseChatIntegration::Channel.find(chan.id)
expect(loadedChan.provider).to eq('dummy') expect(loadedChan.provider).to eq("dummy")
end end
it 'should edit successfully' do it "should edit successfully" do
channel = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "hello" }) channel = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "hello" })
expect(channel.valid?).to eq(true) expect(channel.valid?).to eq(true)
channel.save! channel.save!
end end
it 'can be filtered by provider' do it "can be filtered by provider" do
channel1 = DiscourseChatIntegration::Channel.create!(provider: 'dummy') channel1 = DiscourseChatIntegration::Channel.create!(provider: "dummy")
channel2 = DiscourseChatIntegration::Channel.create!(provider: 'dummy2', data: { val: "blah" }) channel2 = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "blah" })
channel3 = DiscourseChatIntegration::Channel.create!(provider: 'dummy2', data: { val: "blah2" }) channel3 = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "blah2" })
expect(DiscourseChatIntegration::Channel.all.length).to eq(3) expect(DiscourseChatIntegration::Channel.all.length).to eq(3)
expect(DiscourseChatIntegration::Channel.with_provider('dummy2').length).to eq(2) expect(DiscourseChatIntegration::Channel.with_provider("dummy2").length).to eq(2)
expect(DiscourseChatIntegration::Channel.with_provider('dummy').length).to eq(1) expect(DiscourseChatIntegration::Channel.with_provider("dummy").length).to eq(1)
end end
it 'can be filtered by data value' do it "can be filtered by data value" do
channel2 = DiscourseChatIntegration::Channel.create!(provider: 'dummy2', data: { val: "foo" }) channel2 = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "foo" })
channel3 = DiscourseChatIntegration::Channel.create!(provider: 'dummy2', data: { val: "blah" }) channel3 = DiscourseChatIntegration::Channel.create!(provider: "dummy2", data: { val: "blah" })
expect(DiscourseChatIntegration::Channel.all.length).to eq(2) expect(DiscourseChatIntegration::Channel.all.length).to eq(2)
for_provider = DiscourseChatIntegration::Channel.with_provider('dummy2') for_provider = DiscourseChatIntegration::Channel.with_provider("dummy2")
expect(for_provider.length).to eq(2) expect(for_provider.length).to eq(2)
expect(DiscourseChatIntegration::Channel.with_provider('dummy2').with_data_value('val', 'blah').length).to eq(1) expect(
DiscourseChatIntegration::Channel
.with_provider("dummy2")
.with_data_value("val", "blah")
.length,
).to eq(1)
end end
it 'can find its own rules' do it "can find its own rules" do
channel = DiscourseChatIntegration::Channel.create(provider: 'dummy') channel = DiscourseChatIntegration::Channel.create(provider: "dummy")
expect(channel.rules.size).to eq(0) expect(channel.rules.size).to eq(0)
DiscourseChatIntegration::Rule.create(channel: channel) DiscourseChatIntegration::Rule.create(channel: channel)
DiscourseChatIntegration::Rule.create(channel: channel) DiscourseChatIntegration::Rule.create(channel: channel)
expect(channel.rules.size).to eq(2) expect(channel.rules.size).to eq(2)
end end
it 'destroys its rules on destroy' do it "destroys its rules on destroy" do
channel = DiscourseChatIntegration::Channel.create(provider: 'dummy') channel = DiscourseChatIntegration::Channel.create(provider: "dummy")
expect(channel.rules.size).to eq(0) expect(channel.rules.size).to eq(0)
rule1 = DiscourseChatIntegration::Rule.create(channel: channel) rule1 = DiscourseChatIntegration::Rule.create(channel: channel)
rule2 = DiscourseChatIntegration::Rule.create(channel: channel) rule2 = DiscourseChatIntegration::Rule.create(channel: channel)
@ -68,42 +72,48 @@ RSpec.describe DiscourseChatIntegration::Channel do
expect(DiscourseChatIntegration::Rule.with_channel(channel).exists?).to eq(false) expect(DiscourseChatIntegration::Rule.with_channel(channel).exists?).to eq(false)
end end
describe 'validations' do describe "validations" do
it "validates provider correctly" do
it 'validates provider correctly' do
channel = DiscourseChatIntegration::Channel.create!(provider: "dummy") channel = DiscourseChatIntegration::Channel.create!(provider: "dummy")
expect(channel.valid?).to eq(true) expect(channel.valid?).to eq(true)
channel.provider = 'somerandomprovider' channel.provider = "somerandomprovider"
expect(channel.valid?).to eq(false) expect(channel.valid?).to eq(false)
end end
it 'succeeds with valid data' do it "succeeds with valid data" do
channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: "hello" }) channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: "hello" })
expect(channel2.valid?).to eq(true) expect(channel2.valid?).to eq(true)
end end
it 'disallows invalid data' do it "disallows invalid data" do
channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: ' ' }) channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: " " })
expect(channel2.valid?).to eq(false) expect(channel2.valid?).to eq(false)
end end
it 'disallows unknown keys' do it "disallows unknown keys" do
channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: "hello", unknown: "world" }) channel2 =
DiscourseChatIntegration::Channel.new(
provider: "dummy2",
data: {
val: "hello",
unknown: "world",
},
)
expect(channel2.valid?).to eq(false) expect(channel2.valid?).to eq(false)
end end
it 'requires all keys' do it "requires all keys" do
channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: {}) channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: {})
expect(channel2.valid?).to eq(false) expect(channel2.valid?).to eq(false)
end end
it 'disallows duplicate channels' do it "disallows duplicate channels" do
channel1 = DiscourseChatIntegration::Channel.create(provider: "dummy2", data: { val: "hello" }) channel1 =
DiscourseChatIntegration::Channel.create(provider: "dummy2", data: { val: "hello" })
channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: "hello" }) channel2 = DiscourseChatIntegration::Channel.new(provider: "dummy2", data: { val: "hello" })
expect(channel2.valid?).to eq(false) expect(channel2.valid?).to eq(false)
channel2.data[:val] = "hello2" channel2.data[:val] = "hello2"
expect(channel2.valid?).to eq(true) expect(channel2.valid?).to eq(true)
end end
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_relative '../dummy_provider' require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Rule do RSpec.describe DiscourseChatIntegration::Rule do
include_context "with dummy provider" include_context "with dummy provider"
@ -9,31 +9,34 @@ RSpec.describe DiscourseChatIntegration::Rule do
let(:tag1) { Fabricate(:tag) } let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) } let(:tag2) { Fabricate(:tag) }
let(:channel) { DiscourseChatIntegration::Channel.create(provider: 'dummy') } let(:channel) { DiscourseChatIntegration::Channel.create(provider: "dummy") }
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:group) { Fabricate(:group) } let(:group) { Fabricate(:group) }
describe '.alloc_key' do describe ".alloc_key" do
it 'should return sequential numbers' do it "should return sequential numbers" do
expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:1") expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:1")
expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:2") expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:2")
expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:3") expect(DiscourseChatIntegration::Rule.create(channel: channel).key).to eq("rule:3")
end end
end end
it 'should convert between channel and channel_id successfully' do it "should convert between channel and channel_id successfully" do
rule = DiscourseChatIntegration::Rule.create(channel: channel) rule = DiscourseChatIntegration::Rule.create(channel: channel)
expect(rule.channel_id).to eq(channel.id) expect(rule.channel_id).to eq(channel.id)
expect(rule.channel.id).to eq(channel.id) expect(rule.channel.id).to eq(channel.id)
end end
it 'should save and load successfully' do it "should save and load successfully" do
expect(DiscourseChatIntegration::Rule.all.length).to eq(0) expect(DiscourseChatIntegration::Rule.all.length).to eq(0)
rule = DiscourseChatIntegration::Rule.create(channel: channel, rule =
category_id: category.id, DiscourseChatIntegration::Rule.create(
tags: [tag1.name, tag2.name], channel: channel,
filter: 'watch') category_id: category.id,
tags: [tag1.name, tag2.name],
filter: "watch",
)
expect(DiscourseChatIntegration::Rule.all.length).to eq(1) expect(DiscourseChatIntegration::Rule.all.length).to eq(1)
@ -42,18 +45,20 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(loadedRule.channel.id).to eq(channel.id) expect(loadedRule.channel.id).to eq(channel.id)
expect(loadedRule.category_id).to eq(category.id) expect(loadedRule.category_id).to eq(category.id)
expect(loadedRule.tags).to contain_exactly(tag1.name, tag2.name) expect(loadedRule.tags).to contain_exactly(tag1.name, tag2.name)
expect(loadedRule.filter).to eq('watch') expect(loadedRule.filter).to eq("watch")
end end
describe 'general operations' do describe "general operations" do
before do before do
rule = DiscourseChatIntegration::Rule.create(channel: channel, rule =
category_id: category.id, DiscourseChatIntegration::Rule.create(
tags: [tag1.name, tag2.name]) channel: channel,
category_id: category.id,
tags: [tag1.name, tag2.name],
)
end end
it 'can be modified' do it "can be modified" do
rule = DiscourseChatIntegration::Rule.all.first rule = DiscourseChatIntegration::Rule.all.first
rule.tags = [tag1.name] rule.tags = [tag1.name]
@ -63,7 +68,7 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(rule.tags).to contain_exactly(tag1.name) expect(rule.tags).to contain_exactly(tag1.name)
end end
it 'can be deleted' do it "can be deleted" do
DiscourseChatIntegration::Rule.new(channel: channel).save! DiscourseChatIntegration::Rule.new(channel: channel).save!
expect(DiscourseChatIntegration::Rule.all.length).to eq(2) expect(DiscourseChatIntegration::Rule.all.length).to eq(2)
@ -73,7 +78,7 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(DiscourseChatIntegration::Rule.all.length).to eq(1) expect(DiscourseChatIntegration::Rule.all.length).to eq(1)
end end
it 'can delete all' do it "can delete all" do
DiscourseChatIntegration::Rule.create(channel: channel) DiscourseChatIntegration::Rule.create(channel: channel)
DiscourseChatIntegration::Rule.create(channel: channel) DiscourseChatIntegration::Rule.create(channel: channel)
DiscourseChatIntegration::Rule.create(channel: channel) DiscourseChatIntegration::Rule.create(channel: channel)
@ -86,9 +91,9 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(DiscourseChatIntegration::Rule.all.length).to eq(0) expect(DiscourseChatIntegration::Rule.all.length).to eq(0)
end end
it 'can be filtered by channel' do it "can be filtered by channel" do
channel2 = DiscourseChatIntegration::Channel.create(provider: 'dummy') channel2 = DiscourseChatIntegration::Channel.create(provider: "dummy")
channel3 = DiscourseChatIntegration::Channel.create(provider: 'dummy') channel3 = DiscourseChatIntegration::Channel.create(provider: "dummy")
rule2 = DiscourseChatIntegration::Rule.create(channel: channel) rule2 = DiscourseChatIntegration::Rule.create(channel: channel)
rule3 = DiscourseChatIntegration::Rule.create(channel: channel) rule3 = DiscourseChatIntegration::Rule.create(channel: channel)
@ -101,7 +106,7 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(DiscourseChatIntegration::Rule.with_channel(channel2).length).to eq(1) expect(DiscourseChatIntegration::Rule.with_channel(channel2).length).to eq(1)
end end
it 'can be filtered by category' do it "can be filtered by category" do
rule2 = DiscourseChatIntegration::Rule.create(channel: channel, category_id: category.id) rule2 = DiscourseChatIntegration::Rule.create(channel: channel, category_id: category.id)
rule3 = DiscourseChatIntegration::Rule.create(channel: channel, category_id: nil) rule3 = DiscourseChatIntegration::Rule.create(channel: channel, category_id: nil)
@ -111,11 +116,21 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(DiscourseChatIntegration::Rule.with_category_id(nil).length).to eq(1) expect(DiscourseChatIntegration::Rule.with_category_id(nil).length).to eq(1)
end end
it 'can be filtered by group' do it "can be filtered by group" do
group1 = Fabricate(:group) group1 = Fabricate(:group)
group2 = Fabricate(:group) group2 = Fabricate(:group)
rule2 = DiscourseChatIntegration::Rule.create!(channel: channel, type: 'group_message', group_id: group1.id) rule2 =
rule3 = DiscourseChatIntegration::Rule.create!(channel: channel, type: 'group_message', group_id: group2.id) DiscourseChatIntegration::Rule.create!(
channel: channel,
type: "group_message",
group_id: group1.id,
)
rule3 =
DiscourseChatIntegration::Rule.create!(
channel: channel,
type: "group_message",
group_id: group2.id,
)
expect(DiscourseChatIntegration::Rule.all.length).to eq(3) expect(DiscourseChatIntegration::Rule.all.length).to eq(3)
@ -125,42 +140,55 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(DiscourseChatIntegration::Rule.with_group_ids([group2.id]).length).to eq(1) expect(DiscourseChatIntegration::Rule.with_group_ids([group2.id]).length).to eq(1)
end end
it 'can be filtered by type' do it "can be filtered by type" do
group1 = Fabricate(:group) group1 = Fabricate(:group)
rule2 = DiscourseChatIntegration::Rule.create!(channel: channel, type: 'group_message', group_id: group1.id) rule2 =
rule3 = DiscourseChatIntegration::Rule.create!(channel: channel, type: 'group_mention', group_id: group1.id) DiscourseChatIntegration::Rule.create!(
channel: channel,
type: "group_message",
group_id: group1.id,
)
rule3 =
DiscourseChatIntegration::Rule.create!(
channel: channel,
type: "group_mention",
group_id: group1.id,
)
expect(DiscourseChatIntegration::Rule.all.length).to eq(3) expect(DiscourseChatIntegration::Rule.all.length).to eq(3)
expect(DiscourseChatIntegration::Rule.with_type('group_message').length).to eq(1) expect(DiscourseChatIntegration::Rule.with_type("group_message").length).to eq(1)
expect(DiscourseChatIntegration::Rule.with_type('group_mention').length).to eq(1) expect(DiscourseChatIntegration::Rule.with_type("group_mention").length).to eq(1)
expect(DiscourseChatIntegration::Rule.with_type('normal').length).to eq(1) expect(DiscourseChatIntegration::Rule.with_type("normal").length).to eq(1)
end end
it 'can be sorted by precedence' do it "can be sorted by precedence" do
rule2 = DiscourseChatIntegration::Rule.create(channel: channel, filter: 'mute') rule2 = DiscourseChatIntegration::Rule.create(channel: channel, filter: "mute")
rule3 = DiscourseChatIntegration::Rule.create(channel: channel, filter: 'follow') rule3 = DiscourseChatIntegration::Rule.create(channel: channel, filter: "follow")
rule4 = DiscourseChatIntegration::Rule.create(channel: channel, filter: 'thread') rule4 = DiscourseChatIntegration::Rule.create(channel: channel, filter: "thread")
rule5 = DiscourseChatIntegration::Rule.create(channel: channel, filter: 'mute') rule5 = DiscourseChatIntegration::Rule.create(channel: channel, filter: "mute")
expect(DiscourseChatIntegration::Rule.all.length).to eq(5) expect(DiscourseChatIntegration::Rule.all.length).to eq(5)
expect(DiscourseChatIntegration::Rule.all.order_by_precedence.map(&:filter)).to eq(["mute", "mute", "thread", "watch", "follow"]) expect(DiscourseChatIntegration::Rule.all.order_by_precedence.map(&:filter)).to eq(
%w[mute mute thread watch follow],
)
end end
end end
describe 'validations' do describe "validations" do
let(:rule) do let(:rule) do
DiscourseChatIntegration::Rule.create(filter: 'watch', DiscourseChatIntegration::Rule.create(
channel: channel, filter: "watch",
category_id: category.id) channel: channel,
category_id: category.id,
)
end end
it 'validates channel correctly' do it "validates channel correctly" do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.channel_id = 'blahblahblah' rule.channel_id = "blahblahblah"
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
rule.channel_id = -1 rule.channel_id = -1
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
@ -175,7 +203,7 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
end end
it 'validates group correctly' do it "validates group correctly" do
rule.category_id = nil rule.category_id = nil
rule.group_id = group.id rule.group_id = group.id
rule.type = "group_message" rule.type = "group_message"
@ -184,42 +212,41 @@ RSpec.describe DiscourseChatIntegration::Rule do
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
end end
it 'validates category correctly' do it "validates category correctly" do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.category_id = -99 rule.category_id = -99
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
end end
it 'validates filter correctly' do it "validates filter correctly" do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.filter = 'thread' rule.filter = "thread"
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.filter = 'follow' rule.filter = "follow"
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.filter = 'mute' rule.filter = "mute"
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.filter = '' rule.filter = ""
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
rule.filter = 'somerandomstring' rule.filter = "somerandomstring"
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
end end
it 'validates tags correctly' do it "validates tags correctly" do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.tags = [] rule.tags = []
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.tags = [tag1.name] rule.tags = [tag1.name]
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.tags = [tag1.name, 'blah'] rule.tags = [tag1.name, "blah"]
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
end end
it "doesn't allow save when invalid" do it "doesn't allow save when invalid" do
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.filter = 'somerandomfilter' rule.filter = "somerandomfilter"
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
expect(rule.save).to eq(false) expect(rule.save).to eq(false)
end end
end end
end end

View File

@ -1,34 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_relative '../dummy_provider' require_relative "../dummy_provider"
describe 'Chat Controller', type: :request do
describe "Chat Controller", type: :request do
let(:topic) { Fabricate(:post).topic } let(:topic) { Fabricate(:post).topic }
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:category2) { Fabricate(:category) } let(:category2) { Fabricate(:category) }
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
let(:channel) { DiscourseChatIntegration::Channel.create(provider: 'dummy') } let(:channel) { DiscourseChatIntegration::Channel.create(provider: "dummy") }
include_context "with dummy provider" include_context "with dummy provider"
include_context "with validated dummy provider" include_context "with validated dummy provider"
before do before { SiteSetting.chat_integration_enabled = true }
SiteSetting.chat_integration_enabled = true
end
shared_examples 'admin constraints' do |action, route| shared_examples "admin constraints" do |action, route|
context 'when user is not signed in' do context "when user is not signed in" do
it 'should raise the right error' do it "should raise the right error" do
public_send(action, route) public_send(action, route)
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
context 'when user is not an admin' do context "when user is not an admin" do
it 'should raise the right error' do it "should raise the right error" do
sign_in(Fabricate(:user)) sign_in(Fabricate(:user))
public_send(action, route) public_send(action, route)
expect(response.status).to eq(404) expect(response.status).to eq(404)
@ -36,154 +33,168 @@ describe 'Chat Controller', type: :request do
end end
end end
describe 'listing providers' do describe "listing providers" do
include_examples 'admin constraints', 'get', '/admin/plugins/chat-integration/providers.json' include_examples "admin constraints", "get", "/admin/plugins/chat-integration/providers.json"
context 'when signed in as an admin' do context "when signed in as an admin" do
before do before { sign_in(admin) }
sign_in(admin)
end
it 'should return the right response' do it "should return the right response" do
get '/admin/plugins/chat-integration/providers.json' get "/admin/plugins/chat-integration/providers.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['providers'].size).to eq(2) expect(json["providers"].size).to eq(2)
expect(json['providers'].find { |h| h['name'] == 'dummy' }).to eq( expect(json["providers"].find { |h| h["name"] == "dummy" }).to eq(
'name' => 'dummy', "name" => "dummy",
'id' => 'dummy', "id" => "dummy",
'channel_parameters' => [] "channel_parameters" => [],
) )
end end
end end
end end
describe 'testing channels' do describe "testing channels" do
include_examples 'admin constraints', 'get', '/admin/plugins/chat-integration/test.json' include_examples "admin constraints", "get", "/admin/plugins/chat-integration/test.json"
context 'when signed in as an admin' do context "when signed in as an admin" do
before do before { sign_in(admin) }
sign_in(admin)
end
it 'should return the right response' do it "should return the right response" do
post '/admin/plugins/chat-integration/test.json', params: { post "/admin/plugins/chat-integration/test.json",
channel_id: channel.id, topic_id: topic.id params: {
} channel_id: channel.id,
topic_id: topic.id,
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
it 'should fail for invalid channel' do it "should fail for invalid channel" do
post '/admin/plugins/chat-integration/test.json', params: { post "/admin/plugins/chat-integration/test.json",
channel_id: 999, topic_id: topic.id params: {
} channel_id: 999,
topic_id: topic.id,
}
expect(response.status).to eq(422) expect(response.status).to eq(422)
end end
end end
end end
describe 'viewing channels' do describe "viewing channels" do
include_examples 'admin constraints', 'get', '/admin/plugins/chat-integration/channels.json' include_examples "admin constraints", "get", "/admin/plugins/chat-integration/channels.json"
context 'when signed in as an admin' do context "when signed in as an admin" do
before do before { sign_in(admin) }
sign_in(admin)
end
it 'should return the right response' do it "should return the right response" do
rule = DiscourseChatIntegration::Rule.create( rule =
channel: channel, DiscourseChatIntegration::Rule.create(
filter: 'follow', channel: channel,
category_id: category.id, filter: "follow",
tags: [tag.name] category_id: category.id,
) tags: [tag.name],
)
get '/admin/plugins/chat-integration/channels.json', params: { provider: 'dummy' } get "/admin/plugins/chat-integration/channels.json", params: { provider: "dummy" }
expect(response.status).to eq(200) expect(response.status).to eq(200)
channels = response.parsed_body['channels'] channels = response.parsed_body["channels"]
expect(channels.count).to eq(1) expect(channels.count).to eq(1)
expect(channels.first).to eq( expect(channels.first).to eq(
"id" => channel.id, "id" => channel.id,
"provider" => 'dummy', "provider" => "dummy",
"data" => {}, "data" => {
},
"error_key" => nil, "error_key" => nil,
"error_info" => nil, "error_info" => nil,
"rules" => [{ "id" => rule.id, "type" => 'normal', "group_name" => nil, "group_id" => nil, "filter" => "follow", "channel_id" => channel.id, "category_id" => category.id, "tags" => [tag.name] }] "rules" => [
{
"id" => rule.id,
"type" => "normal",
"group_name" => nil,
"group_id" => nil,
"filter" => "follow",
"channel_id" => channel.id,
"category_id" => category.id,
"tags" => [tag.name],
},
],
) )
end end
it 'should fail for invalid provider' do it "should fail for invalid provider" do
get '/admin/plugins/chat-integration/channels.json', params: { provider: 'someprovider' } get "/admin/plugins/chat-integration/channels.json", params: { provider: "someprovider" }
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
end end
describe 'adding a channel' do describe "adding a channel" do
include_examples 'admin constraints', 'post', '/admin/plugins/chat-integration/channels.json' include_examples "admin constraints", "post", "/admin/plugins/chat-integration/channels.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able to add a new channel" do
sign_in(admin) post "/admin/plugins/chat-integration/channels.json",
end params: {
channel: {
it 'should be able to add a new channel' do provider: "dummy",
post '/admin/plugins/chat-integration/channels.json', params: { data: {
channel: { },
provider: 'dummy', },
data: {} }
}
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
channel = DiscourseChatIntegration::Channel.all.last channel = DiscourseChatIntegration::Channel.all.last
expect(channel.provider).to eq('dummy') expect(channel.provider).to eq("dummy")
end end
it 'should fail for invalid params' do it "should fail for invalid params" do
post '/admin/plugins/chat-integration/channels.json', params: { post "/admin/plugins/chat-integration/channels.json",
channel: { params: {
provider: 'dummy2', channel: {
data: { val: 'something with whitespace' } provider: "dummy2",
} data: {
} val: "something with whitespace",
},
},
}
expect(response.status).to eq(422) expect(response.status).to eq(422)
end end
end end
end end
describe 'updating a channel' do describe "updating a channel" do
let(:channel) { DiscourseChatIntegration::Channel.create(provider: 'dummy2', data: { val: "something" }) } let(:channel) do
DiscourseChatIntegration::Channel.create(provider: "dummy2", data: { val: "something" })
end
include_examples 'admin constraints', 'put', "/admin/plugins/chat-integration/channels/1.json" include_examples "admin constraints", "put", "/admin/plugins/chat-integration/channels/1.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able update a channel" do
sign_in(admin) put "/admin/plugins/chat-integration/channels/#{channel.id}.json",
end params: {
channel: {
it 'should be able update a channel' do data: {
put "/admin/plugins/chat-integration/channels/#{channel.id}.json", params: { val: "something-else",
channel: { },
data: { val: "something-else" } },
} }
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -191,30 +202,32 @@ describe 'Chat Controller', type: :request do
expect(channel.data).to eq("val" => "something-else") expect(channel.data).to eq("val" => "something-else")
end end
it 'should fail for invalid params' do it "should fail for invalid params" do
put "/admin/plugins/chat-integration/channels/#{channel.id}.json", params: { put "/admin/plugins/chat-integration/channels/#{channel.id}.json",
channel: { params: {
data: { val: "something with whitespace" } channel: {
} data: {
} val: "something with whitespace",
},
},
}
expect(response.status).to eq(422) expect(response.status).to eq(422)
end end
end end
end end
describe 'deleting a channel' do describe "deleting a channel" do
let(:channel) { DiscourseChatIntegration::Channel.create(provider: 'dummy', data: {}) } let(:channel) { DiscourseChatIntegration::Channel.create(provider: "dummy", data: {}) }
include_examples 'admin constraints', 'delete', "/admin/plugins/chat-integration/channels/1.json" include_examples "admin constraints",
"delete",
"/admin/plugins/chat-integration/channels/1.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able delete a channel" do
sign_in(admin)
end
it 'should be able delete a channel' do
delete "/admin/plugins/chat-integration/channels/#{channel.id}.json" delete "/admin/plugins/chat-integration/channels/#{channel.id}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -223,24 +236,22 @@ describe 'Chat Controller', type: :request do
end end
end end
describe 'adding a rule' do describe "adding a rule" do
include_examples 'admin constraints', 'put', '/admin/plugins/chat-integration/rules.json' include_examples "admin constraints", "put", "/admin/plugins/chat-integration/rules.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able to add a new rule" do
sign_in(admin) post "/admin/plugins/chat-integration/rules.json",
end params: {
rule: {
it 'should be able to add a new rule' do channel_id: channel.id,
post '/admin/plugins/chat-integration/rules.json', params: { category_id: category.id,
rule: { filter: "watch",
channel_id: channel.id, tags: [tag.name],
category_id: category.id, },
filter: 'watch', }
tags: [tag.name]
}
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -248,46 +259,51 @@ describe 'Chat Controller', type: :request do
expect(rule.channel_id).to eq(channel.id) expect(rule.channel_id).to eq(channel.id)
expect(rule.category_id).to eq(category.id) expect(rule.category_id).to eq(category.id)
expect(rule.filter).to eq('watch') expect(rule.filter).to eq("watch")
expect(rule.tags).to eq([tag.name]) expect(rule.tags).to eq([tag.name])
end end
it 'should fail for invalid params' do it "should fail for invalid params" do
post '/admin/plugins/chat-integration/rules.json', params: { post "/admin/plugins/chat-integration/rules.json",
rule: { params: {
channel_id: channel.id, rule: {
category_id: category.id, channel_id: channel.id,
filter: 'watch', category_id: category.id,
tags: ['somenonexistanttag'] filter: "watch",
} tags: ["somenonexistanttag"],
} },
}
expect(response.status).to eq(422) expect(response.status).to eq(422)
end end
end end
end end
describe 'updating a rule' do describe "updating a rule" do
let(:rule) { DiscourseChatIntegration::Rule.create(channel: channel, filter: 'follow', category_id: category.id, tags: [tag.name]) } let(:rule) do
DiscourseChatIntegration::Rule.create(
channel: channel,
filter: "follow",
category_id: category.id,
tags: [tag.name],
)
end
include_examples 'admin constraints', 'put', "/admin/plugins/chat-integration/rules/1.json" include_examples "admin constraints", "put", "/admin/plugins/chat-integration/rules/1.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able update a rule" do
sign_in(admin) put "/admin/plugins/chat-integration/rules/#{rule.id}.json",
end params: {
rule: {
it 'should be able update a rule' do channel_id: channel.id,
put "/admin/plugins/chat-integration/rules/#{rule.id}.json", params: { category_id: category2.id,
rule: { filter: rule.filter,
channel_id: channel.id, tags: rule.tags,
category_id: category2.id, },
filter: rule.filter, }
tags: rule.tags
}
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -295,40 +311,38 @@ describe 'Chat Controller', type: :request do
expect(rule.category_id).to eq(category2.id) expect(rule.category_id).to eq(category2.id)
end end
it 'should fail for invalid params' do it "should fail for invalid params" do
put "/admin/plugins/chat-integration/rules/#{rule.id}.json", params: { put "/admin/plugins/chat-integration/rules/#{rule.id}.json",
rule: { params: {
channel_id: channel.id, rule: {
category_id: category.id, channel_id: channel.id,
filter: 'watch', category_id: category.id,
tags: ['somenonexistanttag'] filter: "watch",
} tags: ["somenonexistanttag"],
} },
}
expect(response.status).to eq(422) expect(response.status).to eq(422)
end end
end end
end end
describe 'deleting a rule' do describe "deleting a rule" do
let(:rule) do let(:rule) do
DiscourseChatIntegration::Rule.create!( DiscourseChatIntegration::Rule.create!(
channel_id: channel.id, channel_id: channel.id,
filter: 'follow', filter: "follow",
category_id: category.id, category_id: category.id,
tags: [tag.name] tags: [tag.name],
) )
end end
include_examples 'admin constraints', 'delete', "/admin/plugins/chat-integration/rules/1.json" include_examples "admin constraints", "delete", "/admin/plugins/chat-integration/rules/1.json"
context 'as an admin' do context "as an admin" do
before { sign_in(admin) }
before do it "should be able delete a rule" do
sign_in(admin)
end
it 'should be able delete a rule' do
delete "/admin/plugins/chat-integration/rules/#{rule.id}.json" delete "/admin/plugins/chat-integration/rules/#{rule.id}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -336,5 +350,4 @@ describe 'Chat Controller', type: :request do
end end
end end
end end
end end

View File

@ -1,16 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe 'Public Controller', type: :request do describe "Public Controller", type: :request do
before { SiteSetting.chat_integration_enabled = true }
before do describe "loading a transcript" do
SiteSetting.chat_integration_enabled = true it "should be able to load a transcript" do
end
describe 'loading a transcript' do
it 'should be able to load a transcript' do
key = DiscourseChatIntegration::Helper.save_transcript("Some content here") key = DiscourseChatIntegration::Helper.save_transcript("Some content here")
get "/chat-transcript/#{key}.json" get "/chat-transcript/#{key}.json"
@ -20,13 +16,11 @@ describe 'Public Controller', type: :request do
expect(response.body).to eq('{"content":"Some content here"}') expect(response.body).to eq('{"content":"Some content here"}')
end end
it 'should 404 for non-existant transcript' do it "should 404 for non-existant transcript" do
key = 'abcdefghijk' key = "abcdefghijk"
get "/chat-transcript/#{key}.json" get "/chat-transcript/#{key}.json"
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
end end

View File

@ -1,11 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_dependency 'post_creator' require_dependency "post_creator"
require_relative '../dummy_provider' require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Manager do RSpec.describe DiscourseChatIntegration::Manager do
let(:manager) { ::DiscourseChatIntegration::Manager } let(:manager) { ::DiscourseChatIntegration::Manager }
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:group) { Fabricate(:group) } let(:group) { Fabricate(:group) }
@ -14,31 +13,37 @@ RSpec.describe DiscourseChatIntegration::Manager do
let(:first_post) { Fabricate(:post, topic: topic) } let(:first_post) { Fabricate(:post, topic: topic) }
let(:second_post) { Fabricate(:post, topic: topic, post_number: 2) } let(:second_post) { Fabricate(:post, topic: topic, post_number: 2) }
describe '.trigger_notifications' do describe ".trigger_notifications" do
include_context "with dummy provider" include_context "with dummy provider"
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') } let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') } let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan3) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') } let(:chan3) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
before do before { SiteSetting.chat_integration_enabled = true }
SiteSetting.chat_integration_enabled = true
end
it "should fail gracefully when a provider throws an exception" do it "should fail gracefully when a provider throws an exception" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id) DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
category_id: category.id,
)
# Triggering a ProviderError should set the error_key to the error message # Triggering a ProviderError should set the error_key to the error message
provider.set_raise_exception(DiscourseChatIntegration::ProviderError.new info: { error_key: "hello" }) provider.set_raise_exception(
DiscourseChatIntegration::ProviderError.new info: { error_key: "hello" }
)
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq('hello') expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq("hello")
# Triggering a different error should set the error_key to a generic message # Triggering a different error should set the error_key to a generic message
provider.set_raise_exception(StandardError.new "hello") provider.set_raise_exception(StandardError.new "hello")
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq('chat_integration.channel_exception') expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq(
"chat_integration.channel_exception",
)
provider.set_raise_exception(nil) provider.set_raise_exception(nil)
@ -48,17 +53,33 @@ RSpec.describe DiscourseChatIntegration::Manager do
it "should not send notifications when provider is disabled" do it "should not send notifications when provider is disabled" do
SiteSetting.chat_integration_enabled = false SiteSetting.chat_integration_enabled = false
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id) DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
category_id: category.id,
)
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
end end
it "should send a notification to watched and following channels for new topic" do it "should send a notification to watched and following channels for new topic" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id) DiscourseChatIntegration::Rule.create!(
DiscourseChatIntegration::Rule.create!(channel: chan2, filter: 'follow', category_id: category.id) channel: chan1,
DiscourseChatIntegration::Rule.create!(channel: chan3, filter: 'mute', category_id: category.id) filter: "watch",
category_id: category.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan2,
filter: "follow",
category_id: category.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan3,
filter: "mute",
category_id: category.id,
)
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
@ -66,9 +87,21 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should send a notification only to watched for reply" do it "should send a notification only to watched for reply" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id) DiscourseChatIntegration::Rule.create!(
DiscourseChatIntegration::Rule.create!(channel: chan2, filter: 'follow', category_id: category.id) channel: chan1,
DiscourseChatIntegration::Rule.create!(channel: chan3, filter: 'mute', category_id: category.id) filter: "watch",
category_id: category.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan2,
filter: "follow",
category_id: category.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan3,
filter: "mute",
category_id: category.id,
)
manager.trigger_notifications(second_post.id) manager.trigger_notifications(second_post.id)
@ -76,7 +109,7 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should respect wildcard category settings" do it "should respect wildcard category settings" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: nil) DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch", category_id: nil)
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
@ -84,17 +117,25 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should respect mute over watch" do it "should respect mute over watch" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: nil) # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch", category_id: nil) # Wildcard watch
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'mute', category_id: category.id) # Specific mute DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "mute",
category_id: category.id,
) # Specific mute
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
end end
it "should respect watch over follow" do it "should respect watch over follow" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil) # Wildcard follow DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "follow", category_id: nil) # Wildcard follow
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id) # Specific watch DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
category_id: category.id,
) # Specific watch
manager.trigger_notifications(second_post.id) manager.trigger_notifications(second_post.id)
@ -102,8 +143,12 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should respect thread over watch" do it "should respect thread over watch" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: nil) # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch", category_id: nil) # Wildcard watch
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'thread', category_id: category.id) # Specific thread DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "thread",
category_id: category.id,
) # Specific thread
manager.trigger_notifications(second_post.id) manager.trigger_notifications(second_post.id)
@ -111,18 +156,23 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should not notify about private messages" do it "should not notify about private messages" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil) # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "follow", category_id: nil) # Wildcard watch
private_post = Fabricate(:private_message_post) private_post = Fabricate(:private_message_post)
manager.trigger_notifications(private_post.id) manager.trigger_notifications(private_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
end end
it "should work for group pms" do it "should work for group pms" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch') # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch") # Wildcard watch
DiscourseChatIntegration::Rule.create!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group.id) # Group watch DiscourseChatIntegration::Rule.create!(
channel: chan2,
type: "group_message",
filter: "watch",
group_id: group.id,
) # Group watch
private_post = Fabricate(:private_message_post) private_post = Fabricate(:private_message_post)
private_post.topic.invite_group(Fabricate(:user), group) private_post.topic.invite_group(Fabricate(:user), group)
@ -133,8 +183,18 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should work for pms with multiple groups" do it "should work for pms with multiple groups" do
DiscourseChatIntegration::Rule.create!(channel: chan1, type: 'group_message', filter: 'watch', group_id: group.id) DiscourseChatIntegration::Rule.create!(
DiscourseChatIntegration::Rule.create!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group2.id) channel: chan1,
type: "group_message",
filter: "watch",
group_id: group.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan2,
type: "group_message",
filter: "watch",
group_id: group2.id,
)
private_post = Fabricate(:private_message_post) private_post = Fabricate(:private_message_post)
private_post.topic.invite_group(Fabricate(:user), group) private_post.topic.invite_group(Fabricate(:user), group)
@ -146,40 +206,77 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should work for group mentions" do it "should work for group mentions" do
third_post = Fabricate(:post, topic: topic, post_number: 3, raw: "let's mention @#{group.name}") third_post =
Fabricate(:post, topic: topic, post_number: 3, raw: "let's mention @#{group.name}")
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch') # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch") # Wildcard watch
DiscourseChatIntegration::Rule.create!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group.id) DiscourseChatIntegration::Rule.create!(
DiscourseChatIntegration::Rule.create!(channel: chan3, type: 'group_mention', filter: 'watch', group_id: group.id) channel: chan2,
type: "group_message",
filter: "watch",
group_id: group.id,
)
DiscourseChatIntegration::Rule.create!(
channel: chan3,
type: "group_mention",
filter: "watch",
group_id: group.id,
)
manager.trigger_notifications(third_post.id) manager.trigger_notifications(third_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id, chan3.id) expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id, chan3.id)
end end
it "should give group rule precedence over normal rules" do it "should give group rule precedence over normal rules" do
third_post = Fabricate(:post, topic: topic, post_number: 3, raw: "let's mention @#{group.name}") third_post =
Fabricate(:post, topic: topic, post_number: 3, raw: "let's mention @#{group.name}")
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'mute', category_id: category.id) # Mute category DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "mute",
category_id: category.id,
) # Mute category
manager.trigger_notifications(third_post.id) manager.trigger_notifications(third_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', type: 'group_mention', group_id: group.id) # Watch mentions DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
type: "group_mention",
group_id: group.id,
) # Watch mentions
manager.trigger_notifications(third_post.id) manager.trigger_notifications(third_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end end
it "should not notify about mentions in private messages" do it "should not notify about mentions in private messages" do
# Group 1 watching for messages on channel 1 # Group 1 watching for messages on channel 1
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', type: 'group_message', group_id: group.id) DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
type: "group_message",
group_id: group.id,
)
# Group 2 watching for mentions on channel 2 # Group 2 watching for mentions on channel 2
DiscourseChatIntegration::Rule.create!(channel: chan2, filter: 'watch', type: 'group_mention', group_id: group2.id) DiscourseChatIntegration::Rule.create!(
channel: chan2,
filter: "watch",
type: "group_mention",
group_id: group2.id,
)
# Make a private message only accessible to group 1 # Make a private message only accessible to group 1
private_message = Fabricate(:private_message_post) private_message = Fabricate(:private_message_post)
private_message.topic.invite_group(Fabricate(:user), group) private_message.topic.invite_group(Fabricate(:user), group)
# Mention group 2 in the message # Mention group 2 in the message
mention_post = Fabricate(:post, topic: private_message.topic, post_number: 2, raw: "let's mention @#{group2.name}") mention_post =
Fabricate(
:post,
topic: private_message.topic,
post_number: 2,
raw: "let's mention @#{group2.name}",
)
# We expect that only group 1 receives a notification # We expect that only group 1 receives a notification
manager.trigger_notifications(mention_post.id) manager.trigger_notifications(mention_post.id)
@ -187,15 +284,15 @@ RSpec.describe DiscourseChatIntegration::Manager do
end end
it "should not notify about posts the chat_user cannot see" do it "should not notify about posts the chat_user cannot see" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil) # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "follow", category_id: nil) # Wildcard watch
# Create a group & user # Create a group & user
group = Fabricate(:group, name: "friends") group = Fabricate(:group, name: "friends")
user = Fabricate(:user, username: 'david') user = Fabricate(:user, username: "david")
group.add(user) group.add(user)
# Set the chat_user to the newly created non-admin user # Set the chat_user to the newly created non-admin user
SiteSetting.chat_integration_discourse_username = 'david' SiteSetting.chat_integration_discourse_username = "david"
# Create a category # Create a category
category = Fabricate(:category, name: "Test category") category = Fabricate(:category, name: "Test category")
@ -208,7 +305,7 @@ RSpec.describe DiscourseChatIntegration::Manager do
# Check no notification sent # Check no notification sent
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly() expect(provider.sent_to_channel_ids).to contain_exactly
# Now expose category to new user # Now expose category to new user
category.set_permissions(Group[:friends] => :full) category.set_permissions(Group[:friends] => :full)
@ -217,20 +314,17 @@ RSpec.describe DiscourseChatIntegration::Manager do
# Check notification sent # Check notification sent
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end end
describe 'with tags enabled' do describe "with tags enabled" do
let(:tag) { Fabricate(:tag, name: 'gsoc') } let(:tag) { Fabricate(:tag, name: "gsoc") }
let(:tagged_topic) { Fabricate(:topic, category_id: category.id, tags: [tag]) } let(:tagged_topic) { Fabricate(:topic, category_id: category.id, tags: [tag]) }
let(:tagged_first_post) { Fabricate(:post, topic: tagged_topic) } let(:tagged_first_post) { Fabricate(:post, topic: tagged_topic) }
before(:each) do before(:each) { SiteSetting.tagging_enabled = true }
SiteSetting.tagging_enabled = true
end
it 'should still work for rules without any tags specified' do it "should still work for rules without any tags specified" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil) # Wildcard watch DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "follow", category_id: nil) # Wildcard watch
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
manager.trigger_notifications(tagged_first_post.id) manager.trigger_notifications(tagged_first_post.id)
@ -238,16 +332,19 @@ RSpec.describe DiscourseChatIntegration::Manager do
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id, chan1.id) expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id, chan1.id)
end end
it 'should only match tagged topics when rule has tags' do it "should only match tagged topics when rule has tags" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: category.id, tags: [tag.name]) DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "follow",
category_id: category.id,
tags: [tag.name],
)
manager.trigger_notifications(first_post.id) manager.trigger_notifications(first_post.id)
manager.trigger_notifications(tagged_first_post.id) manager.trigger_notifications(tagged_first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end end
end end
end end
end end