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
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: yarn
- name: Yarn install
@ -33,15 +33,15 @@ jobs:
bundler-cache: true
- name: ESLint
if: ${{ always() }}
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts
if: ${{ !cancelled() }}
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets,admin/assets}/javascripts
- name: Prettier
if: ${{ always() }}
if: ${{ !cancelled() }}
shell: bash
run: |
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}"
fi
if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
@ -49,9 +49,18 @@ jobs:
fi
- name: Ember template lint
if: ${{ always() }}
run: yarn ember-template-lint --no-error-on-unmatched-pattern assets/javascripts
if: ${{ !cancelled() }}
run: yarn ember-template-lint --no-error-on-unmatched-pattern assets/javascripts admin/assets/javascripts
- name: Rubocop
if: ${{ always() }}
if: ${{ !cancelled() }}
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
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash
run: |
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
- name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash
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
echo "::set-output name=files_exist::true"
echo "files_exist=true" >> $GITHUB_OUTPUT
fi
- name: Plugin QUnit

View File

@ -1,2 +1,2 @@
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
source 'https://rubygems.org'
source "https://rubygems.org"
group :development do
gem 'translations-manager', git: 'https://github.com/discourse/translations-manager.git'
gem 'rubocop-discourse'
gem "translations-manager", git: "https://github.com/discourse/translations-manager.git"
gem "rubocop-discourse"
gem "syntax_tree"
end

View File

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

View File

@ -8,15 +8,16 @@ class DiscourseChatIntegration::ChatController < ApplicationController
end
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,
channel_parameters: (defined? x::CHANNEL_PARAMETERS) ? x::CHANNEL_PARAMETERS : []
channel_parameters: (defined?(x::CHANNEL_PARAMETERS)) ? x::CHANNEL_PARAMETERS : [],
}
end
render json: providers, root: 'providers'
render json: providers, root: "providers"
end
def test
@ -28,9 +29,7 @@ class DiscourseChatIntegration::ChatController < ApplicationController
provider = ::DiscourseChatIntegration::Provider.get_by_name(channel.provider)
if !DiscourseChatIntegration::Provider.is_enabled(provider)
raise Discourse::NotFound
end
raise Discourse::NotFound if !DiscourseChatIntegration::Provider.is_enabled(provider)
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)
channels = DiscourseChatIntegration::Channel.with_provider(requested_provider)
render_serialized channels, DiscourseChatIntegration::ChannelSerializer, root: 'channels'
render_serialized channels, DiscourseChatIntegration::ChannelSerializer, root: "channels"
end
def create_channel
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])
raise Discourse::InvalidParameters, 'Provider is not valid'
raise Discourse::InvalidParameters, "Provider is not valid"
end
requested_provider = params[:channel][:provider]
if !providers.include?(requested_provider)
raise Discourse::InvalidParameters, 'Provider is not valid'
raise Discourse::InvalidParameters, "Provider is not valid"
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)
channel = DiscourseChatIntegration::Channel.new(hash)
if !channel.save
raise Discourse::InvalidParameters, 'Channel is not valid'
end
raise Discourse::InvalidParameters, "Channel is not valid" if !channel.save
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: 'channel'
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: "channel"
rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422
end
@ -94,15 +95,16 @@ class DiscourseChatIntegration::ChatController < ApplicationController
channel = DiscourseChatIntegration::Channel.find(params[:id].to_i)
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)
if !channel.update(hash)
raise Discourse::InvalidParameters, 'Channel is not valid'
end
raise Discourse::InvalidParameters, "Channel is not valid" if !channel.update(hash)
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: 'channel'
render_serialized channel, DiscourseChatIntegration::ChannelSerializer, root: "channel"
rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422
end
@ -118,14 +120,13 @@ class DiscourseChatIntegration::ChatController < ApplicationController
def create_rule
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)
if !rule.save
raise Discourse::InvalidParameters, 'Rule is not valid'
end
raise Discourse::InvalidParameters, "Rule is not valid" if !rule.save
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: 'rule'
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: "rule"
rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422
end
@ -136,11 +137,9 @@ class DiscourseChatIntegration::ChatController < ApplicationController
rule = DiscourseChatIntegration::Rule.find(params[:id].to_i)
hash = params.require(:rule).permit(:type, :filter, :group_id, :category_id, tags: [])
if !rule.update(hash)
raise Discourse::InvalidParameters, 'Rule is not valid'
end
raise Discourse::InvalidParameters, "Rule is not valid" if !rule.update(hash)
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: 'rule'
render_serialized rule, DiscourseChatIntegration::RuleSerializer, root: "rule"
rescue Discourse::InvalidParameters => e
render json: { errors: [e.message] }, status: 422
end

View File

@ -2,7 +2,6 @@
module DiscourseChatIntegration
module Helper
def self.process_command(channel, tokens)
guardian = DiscourseChatIntegration::Manager.guardian
@ -16,13 +15,19 @@ module DiscourseChatIntegration
when "thread", "watch", "follow", "mute"
return error_text if tokens.empty?
# 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
category = Category.find_by(slug: category_name)
unless category
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)
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,
)
)
end
else
category = nil # All categories
@ -32,8 +37,8 @@ module DiscourseChatIntegration
# Every remaining token must be a tag. If not, abort and send help text
while tokens.size > 0
token = tokens.shift
if token.start_with?('tag:')
tag_name = token.sub(/^tag:/, '')
if token.start_with?("tag:")
tag_name = token.sub(/^tag:/, "")
else
return error_text
end
@ -47,7 +52,12 @@ module DiscourseChatIntegration
end
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
I18n.t("chat_integration.provider.#{provider}.create.created")
when :updated
@ -107,23 +117,25 @@ module DiscourseChatIntegration
end
end
text << I18n.t("chat_integration.provider.#{provider}.status.rule_string",
text << I18n.t(
"chat_integration.provider.#{provider}.status.rule_string",
index: i,
filter: rule.filter,
category: category_name
category: category_name,
)
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
text << "\n"
i += 1
end
if rules.size == 0
text << I18n.t("chat_integration.provider.#{provider}.status.no_rules")
end
text << I18n.t("chat_integration.provider.#{provider}.status.no_rules") if rules.size == 0
text
end
@ -144,12 +156,15 @@ module DiscourseChatIntegration
# :created if a new rule has been created
# false if there was an error
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
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
# These rules have exactly the same criteria as what we're trying to create
@ -185,7 +200,9 @@ module DiscourseChatIntegration
end
# 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
end
@ -197,13 +214,13 @@ module DiscourseChatIntegration
end
def self.formatted_display_name(user)
if !SiteSetting.enable_names || user.name.blank?
return "@#{user.username}"
end
return "@#{user.username}" if !SiteSetting.enable_names || user.name.blank?
full_name = user.name
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?
"@#{user.username}"
elsif similar

View File

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

View File

@ -3,10 +3,10 @@
module Jobs
class DiscourseChatMigrateFromSlackOfficial < ::Jobs::Onceoff
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
already_setup_rules = DiscourseChatIntegration::Channel.with_provider('slack').exists?
already_setup_rules = DiscourseChatIntegration::Channel.with_provider("slack").exists?
already_setup_sitesettings =
SiteSetting.chat_integration_slack_enabled ||
@ -18,11 +18,11 @@ module Jobs
ActiveRecord::Base.transaction do
migrate_settings
migrate_data
is_slack_enabled = site_settings_value('slack_enabled')
is_slack_enabled = site_settings_value("slack_enabled")
if is_slack_enabled
slack_enabled = SiteSetting.find_by(name: 'slack_enabled')
slack_enabled.update!(value: 'f')
slack_enabled = SiteSetting.find_by(name: "slack_enabled")
slack_enabled.update!(value: "f")
SiteSetting.chat_integration_slack_enabled = true
SiteSetting.chat_integration_enabled = true
@ -30,21 +30,22 @@ module Jobs
end
end
end
end
def migrate_data
rows = []
PluginStoreRow.where(plugin_name: 'discourse-slack-official')
PluginStoreRow
.where(plugin_name: "discourse-slack-official")
.where("key ~* :pat", pat: "^category_.*")
.each do |row|
PluginStore.cast_value(row.type_name, row.value).each do |rule|
PluginStore
.cast_value(row.type_name, row.value)
.each do |rule|
category_id =
if row.key == 'category_*'
if row.key == "category_*"
nil
else
row.key.gsub!('category_', '')
row.key.gsub!("category_", "")
row.key.to_i
end
@ -57,7 +58,7 @@ module Jobs
category_id: category_id,
channel: rule[:channel],
filter: rule[:filter],
tags: valid_tags
tags: valid_tags,
}
end
end
@ -65,9 +66,19 @@ module Jobs
rows.each do |row|
# Load an existing channel with this identifier. If none, create it
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
channel = DiscourseChatIntegration::Channel.create(provider: 'slack', data: { identifier: row[:channel] })
channel =
DiscourseChatIntegration::Channel.create(
provider: "slack",
data: {
identifier: row[:channel],
},
)
if !channel.id
Rails.logger.warn("Error creating channel for #{row}")
next
@ -75,50 +86,58 @@ module Jobs
end
# 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
private
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
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
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
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
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
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
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
SiteSetting.chat_integration_discourse_username = (username || Discourse.system_user.username)
SiteSetting.chat_integration_discourse_username =
(username || Discourse.system_user.username)
end
end
def site_settings_value(name)
value = SiteSetting.find_by(name: name)&.value
if value == 't'
if value == "t"
value = true
elsif value == 'f'
elsif value == "f"
value = false
end

View File

@ -2,10 +2,11 @@
class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
# 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_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_destroy :destroy_rules
@ -13,7 +14,7 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
validate :provider_valid?, :data_valid?
def self.key_prefix
'channel:'.freeze
"channel:".freeze
end
def rules
@ -52,9 +53,7 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
data.each do |key, value|
regex_string = params.find { |p| p[:key] == key }[:regex]
if !Regexp.new(regex_string).match(value)
errors.add(:data, "data.#{key} is invalid")
end
errors.add(:data, "data.#{key} is invalid") if !Regexp.new(regex_string).match(value)
unique = params.find { |p| p[:key] == key }[:unique]
if unique
@ -63,8 +62,6 @@ class DiscourseChatIntegration::Channel < DiscourseChatIntegration::PluginModel
end
end
if check_unique && matching_channels.exists?
errors.add(:data, "matches an existing channel")
end
errors.add(:data, "matches an existing channel") if check_unique && matching_channels.exists?
end
end

View File

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

View File

@ -2,26 +2,30 @@
class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
# 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_channel, ->(channel) { with_channel_id(channel.id) }
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,
->(category_id) {
if category_id.nil?
where("(value::json->'category_id') IS NULL OR json_typeof(value::json->'category_id')='null'")
where(
"(value::json->'category_id') IS NULL OR json_typeof(value::json->'category_id')='null'",
)
else
where("value::json->>'category_id'=?", category_id.to_s)
end
end
}
scope :with_group_ids, ->(group_id) do
where("value::json->>'group_id' IN (?)", group_id.map!(&:to_s))
end
scope :with_group_ids,
->(group_id) { where("value::json->>'group_id' IN (?)", group_id.map!(&:to_s)) }
scope :order_by_precedence, -> {
order("
scope :order_by_precedence,
-> {
order(
"
CASE
WHEN value::json->>'type' = 'group_mention' THEN 1
WHEN value::json->>'type' = 'group_message' THEN 2
@ -35,21 +39,28 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
WHEN value::json->>'filter' = 'watch' THEN 3
WHEN value::json->>'filter' = 'follow' THEN 4
END
")
",
)
}
after_initialize :init_filter
validates :filter, inclusion: { in: %w(thread watch follow mute),
message: "%{value} is not a valid filter" }
validates :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),
message: "%{value} is not a valid filter" }
validates :type,
inclusion: {
in: %w[normal group_message group_mention],
message: "%{value} is not a valid filter",
}
validate :channel_valid?, :category_valid?, :group_valid?, :tags_valid?
def self.key_prefix
'rule:'.freeze
"rule:".freeze
end
# We never want an empty array, set it to nil instead
@ -62,7 +73,7 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end
# 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|
if val.nil? || val.blank?
super(nil)
@ -91,11 +102,11 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end
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")
end
return unless type == 'normal'
return unless type == "normal"
if !(category_id.nil? || Category.where(id: category_id).exists?)
errors.add(:category_id, "#{category_id} is not a valid category id")
@ -103,11 +114,11 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel
end
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")
end
return if type == 'normal'
return if type == "normal"
if !Group.where(id: group_id).exists?
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?
tags.each do |tag|
if !Tag.where(name: tag).exists?
errors.add(:tags, "#{tag} is not a valid tag")
end
errors.add(:tags, "#{tag} is not a valid tag") if !Tag.where(name: tag).exists?
end
end
def init_filter
self.filter ||= 'watch'
self.type ||= 'normal'
self.filter ||= "watch"
self.type ||= "normal"
end
end

View File

@ -1,10 +1,13 @@
# frozen_string_literal: true
Discourse::Application.routes.append do
mount ::DiscourseChatIntegration::AdminEngine, at: '/admin/plugins/chat-integration', constraints: AdminConstraint.new
mount ::DiscourseChatIntegration::PublicEngine, at: '/chat-transcript/', as: 'chat-transcript'
mount ::DiscourseChatIntegration::Provider::HookEngine, at: '/chat-integration/'
mount ::DiscourseChatIntegration::AdminEngine,
at: "/admin/plugins/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
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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
module DiscourseChatIntegration
module Manager
def self.guardian
Guardian.new(User.find_by(username: SiteSetting.chat_integration_discourse_username))
end
@ -23,19 +22,26 @@ module DiscourseChatIntegration
if topic.archetype == Archetype.private_message
group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id)
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
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
matching_rules += DiscourseChatIntegration::Rule.with_type('normal').with_category_id(nil)
matching_rules += DiscourseChatIntegration::Rule.with_type("normal").with_category_id(nil)
end
# If groups are mentioned, check for any matching rules and append them
mentions = post.raw_mentions
if mentions && mentions.length > 0
groups = Group.where('LOWER(name) IN (?)', mentions)
groups = Group.where("LOWER(name) IN (?)", mentions)
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
@ -43,7 +49,8 @@ module DiscourseChatIntegration
# If tagging is enabled, thow away rules that don't apply to this topic
if SiteSetting.tagging_enabled
topic_tags = topic.tags.present? ? topic.tags.pluck(:name) : []
matching_rules = matching_rules.select do |rule|
matching_rules =
matching_rules.select do |rule|
next true if rule.tags.nil? || rule.tags.empty? # Filter has no tags specified
any_tags_match = !((rule.tags & topic_tags).empty?)
next any_tags_match # If any tags match, keep this filter, otherwise throw away
@ -51,9 +58,10 @@ module DiscourseChatIntegration
end
# Sort by order of precedence
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)
sort_func = proc { |a, b| [t_prec[a.type], f_prec[a.filter]] <=> [t_prec[b.type], f_prec[b.filter]] }
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)
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)
# Take the first rule for each channel
@ -81,14 +89,15 @@ module DiscourseChatIntegration
begin
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
if e.class == (DiscourseChatIntegration::ProviderError) && e.info.key?(:error_key) && !e.info[:error_key].nil?
channel.update_attribute('error_key', e.info[:error_key])
if e.class == (DiscourseChatIntegration::ProviderError) && e.info.key?(:error_key) &&
!e.info[:error_key].nil?
channel.update_attribute("error_key", e.info[:error_key])
else
channel.update_attribute('error_key', 'chat_integration.channel_exception')
channel.update_attribute("error_key", "chat_integration.channel_exception")
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
# Discourse.handle_job_exception(e,
@ -99,10 +108,7 @@ module DiscourseChatIntegration
# error_info: e.class == DiscourseChatIntegration::ProviderError ? e.info : nil }
# )
end
end
end
end
end

View File

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

View File

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

View File

@ -8,7 +8,12 @@ module DiscourseChatIntegration
CHANNEL_PARAMETERS = [
{ 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
def self.send_message(url, message)
@ -17,7 +22,7 @@ module DiscourseChatIntegration
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
response = http.request(req)
@ -25,7 +30,7 @@ module DiscourseChatIntegration
end
def self.ensure_protocol(url)
return url if !url.start_with?('//')
return url if !url.start_with?("//")
"http:#{url}"
end
@ -34,24 +39,40 @@ module DiscourseChatIntegration
topic = post.topic
category = ''
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
message = {
content: SiteSetting.chat_integration_discord_message_content,
embeds: [{
title: "#{topic.title} #{(category == '[uncategorized]') ? '' : category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}",
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,
description: post.excerpt(SiteSetting.chat_integration_discord_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true),
description:
post.excerpt(
SiteSetting.chat_integration_discord_excerpt_length,
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)
}
}]
icon_url: ensure_protocol(post.user.small_avatar_url),
},
},
],
}
message
@ -59,17 +80,20 @@ module DiscourseChatIntegration
def self.trigger_notification(post, channel, rule)
# 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)
response = send_message(webhook_url, message)
if !response.kind_of?(Net::HTTPSuccess)
raise ::DiscourseChatIntegration::ProviderError.new(info: {
error_key: nil, message: message, response_body: response.body
})
raise ::DiscourseChatIntegration::ProviderError.new(
info: {
error_key: nil,
message: message,
response_body: response.body,
},
)
end
end
end
end
end

View File

@ -1,12 +1,9 @@
# frozen_string_literal: true
module DiscourseChatIntegration::Provider::FlowdockProvider
PROVIDER_NAME = "flowdock".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_flowdock_enabled
CHANNEL_PARAMETERS = [
{ key: "flow_token", regex: '^\S+', unique: true, hidden: true },
]
CHANNEL_PARAMETERS = [{ key: "flow_token", regex: '^\S+', unique: true, hidden: true }]
def self.send_message(url, message)
uri = URI(url)
@ -14,7 +11,7 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
http = FinalDestination::HTTP.new(uri.host, uri.port)
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
response = http.request(req)
@ -29,15 +26,21 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
event: "discussion",
author: {
name: display_name,
avatar: post.user.small_avatar_url
avatar: post.user.small_avatar_url,
},
title: I18n.t("chat_integration.provider.flowdock.message_title"),
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: {
title: post.topic.title,
external_url: post.full_url
}
external_url: post.full_url,
},
}
message
@ -50,7 +53,11 @@ module DiscourseChatIntegration::Provider::FlowdockProvider
unless response.kind_of?(Net::HTTPSuccess)
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

View File

@ -3,19 +3,28 @@
module DiscourseChatIntegration
module Provider
module GitterProvider
PROVIDER_NAME = 'gitter'.freeze
PROVIDER_NAME = "gitter".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_gitter_enabled
CHANNEL_PARAMETERS = [
{ 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)
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
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
@ -23,7 +32,14 @@ module DiscourseChatIntegration
display_name = post.user.username
topic = post.topic
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})"
end

View File

@ -3,26 +3,35 @@
module DiscourseChatIntegration
module Provider
module GoogleProvider
PROVIDER_NAME = 'google'.freeze
PROVIDER_NAME = "google".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_google_enabled
CHANNEL_PARAMETERS = [
{ 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)
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.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
response = http.request(req)
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
@ -35,49 +44,67 @@ module DiscourseChatIntegration
widgets: [
{
keyValue: {
"topLabel": I18n.t("chat_integration.provider.google.new_#{post.is_first_post? ? "topic" : "post"}", site_title: SiteSetting.title),
"content": post.topic.title,
"contentMultiline": "false",
"bottomLabel": I18n.t("chat_integration.provider.google.author", username: post.user.username),
"onClick": {
"openLink": {
"url": post.full_url
}
}
}
topLabel:
I18n.t(
"chat_integration.provider.google.new_#{post.is_first_post? ? "topic" : "post"}",
site_title: SiteSetting.title,
),
content: post.topic.title,
contentMultiline: "false",
bottomLabel:
I18n.t(
"chat_integration.provider.google.author",
username: post.user.username,
),
onClick: {
openLink: {
url: post.full_url,
},
]
},
},
},
],
},
{
widgets: [
{
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: [
{
buttons: [
{
"textButton": {
"text": I18n.t("chat_integration.provider.google.link", site_title: SiteSetting.title),
"onClick": {
"openLink": {
"url": post.full_url
}
}
}
textButton: {
text:
I18n.t(
"chat_integration.provider.google.link",
site_title: SiteSetting.title,
),
onClick: {
openLink: {
url: post.full_url,
},
},
},
]
}
]
},
],
}
]
},
],
},
],
},
],
}
end
end

View File

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

View File

@ -8,26 +8,42 @@ module DiscourseChatIntegration
CHANNEL_PARAMETERS = [
{ 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
def self.trigger_notification(post, channel, rule)
webhook_url = channel.data['webhook_url']
webhook_url = channel.data["webhook_url"]
message = generate_guilded_message(post)
response = send_message(webhook_url, message)
if !response.kind_of?(Net::HTTPSuccess)
raise ::DiscourseChatIntegration::ProviderError.new(info: {
error_key: nil, message: message, response_body: response.body
})
raise ::DiscourseChatIntegration::ProviderError.new(
info: {
error_key: nil,
message: message,
response_body: response.body,
},
)
end
end
def self.generate_guilded_message(post)
topic = post.topic
category = ''
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
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
@ -37,15 +53,24 @@ module DiscourseChatIntegration
end
message = {
embeds: [{
title: "#{topic.title} #{(category == '[uncategorized]') ? '' : category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}",
embeds: [
{
title:
"#{topic.title} #{(category == "[uncategorized]") ? "" : category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
url: post.full_url,
description: post.excerpt(SiteSetting.chat_integration_guilded_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true),
description:
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}"
}
}]
text: "#{display_name} | #{post.created_at}",
},
},
],
}
message
@ -54,9 +79,9 @@ module DiscourseChatIntegration
def self.send_message(url, message)
uri = URI(url)
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
response = http.request(req)
@ -64,10 +89,9 @@ module DiscourseChatIntegration
end
def self.ensure_protocol(url)
return url if !url.start_with?('//')
return url if !url.start_with?("//")
"http:#{url}"
end
end
end
end

View File

@ -7,24 +7,26 @@ module DiscourseChatIntegration
PROVIDER_ENABLED_SETTING = :chat_integration_matrix_enabled
CHANNEL_PARAMETERS = [
{ 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)
homeserver = SiteSetting.chat_integration_matrix_homeserver
event_type = 'm.room.message'
event_type = "m.room.message"
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.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
response = http.request(req)
@ -35,16 +37,29 @@ module DiscourseChatIntegration
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
message = {
msgtype: SiteSetting.chat_integration_matrix_use_notice ? 'm.notice' : 'm.text',
body: I18n.t('chat_integration.provider.matrix.text_message', user: display_name,
post_url: post.full_url,
title: post.topic.title),
format: 'org.matrix.custom.html',
formatted_body: I18n.t('chat_integration.provider.matrix.formatted_message', user: display_name,
msgtype: SiteSetting.chat_integration_matrix_use_notice ? "m.notice" : "m.text",
body:
I18n.t(
"chat_integration.provider.matrix.text_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))
),
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
@ -53,24 +68,26 @@ module DiscourseChatIntegration
def self.trigger_notification(post, channel, rule)
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)
error_key = nil
begin
responseData = JSON.parse(response.body)
if responseData['errcode'] == "M_UNKNOWN_TOKEN"
error_key = 'chat_integration.provider.matrix.errors.unknown_token'
elsif responseData['errcode'] == "M_UNKNOWN"
error_key = 'chat_integration.provider.matrix.errors.unknown_room'
if responseData["errcode"] == "M_UNKNOWN_TOKEN"
error_key = "chat_integration.provider.matrix.errors.unknown_token"
elsif responseData["errcode"] == "M_UNKNOWN"
error_key = "chat_integration.provider.matrix.errors.unknown_room"
end
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

View File

@ -15,22 +15,18 @@ module DiscourseChatIntegration::Provider::MattermostProvider
def command
text = process_command(params)
render json: {
response_type: 'ephemeral',
text: text
}
render json: { response_type: "ephemeral", text: text }
end
def process_command(params)
tokens = params[:text].split(" ")
# channel name fix
channel_id =
case params[:channel_name]
when 'directmessage'
when "directmessage"
"@#{params[:user_name]}"
when 'privategroup'
when "privategroup"
params[:channel_id]
else
"##{params[:channel_name]}"
@ -38,13 +34,22 @@ module DiscourseChatIntegration::Provider::MattermostProvider
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
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)
end
def mattermost_token_valid?
@ -52,7 +57,6 @@ module DiscourseChatIntegration::Provider::MattermostProvider
if SiteSetting.chat_integration_mattermost_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_mattermost_incoming_webhook_token != params[:token]
raise Discourse::InvalidAccess.new
end
end
@ -63,8 +67,5 @@ module DiscourseChatIntegration::Provider::MattermostProvider
isolate_namespace DiscourseChatIntegration::Provider::MattermostProvider
end
MattermostEngine.routes.draw do
post "command" => "mattermost_command#command"
end
MattermostEngine.routes.draw { post "command" => "mattermost_command#command" }
end

View File

@ -5,29 +5,30 @@ module DiscourseChatIntegration
module MattermostProvider
PROVIDER_NAME = "mattermost".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_mattermost_enabled
CHANNEL_PARAMETERS = [
{ key: "identifier", regex: '^[@#]\S*$', unique: true }
]
CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
def self.send_via_webhook(message)
uri = URI(SiteSetting.chat_integration_mattermost_webhook_url)
http = FinalDestination::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
http.use_ssl = (uri.scheme == "https")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = message.to_json
response = http.request(req)
unless response.kind_of? Net::HTTPSuccess
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
error_key = nil
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
def self.mattermost_message(post, channel)
@ -35,17 +36,26 @@ module DiscourseChatIntegration
topic = post.topic
category = ''
category = ""
if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]"
category = "[#{I18n.t("uncategorized_category_name")}]"
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
icon_url =
if SiteSetting.chat_integration_mattermost_icon_url.present?
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)
end
@ -53,7 +63,7 @@ module DiscourseChatIntegration
channel: channel,
username: SiteSetting.title || "Discourse",
icon_url: icon_url,
attachments: []
attachments: [],
}
summary = {
@ -61,8 +71,15 @@ module DiscourseChatIntegration
author_name: display_name,
author_icon: post.user.small_avatar_url,
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),
title: "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}",
text:
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,
}
@ -71,12 +88,11 @@ module DiscourseChatIntegration
end
def self.trigger_notification(post, channel, rule)
channel_id = channel.data['identifier']
channel_id = channel.data["identifier"]
message = mattermost_message(post, channel_id)
self.send_via_webhook(message)
end
end
end
end

View File

@ -5,36 +5,45 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
PROVIDER_ENABLED_SETTING = :chat_integration_rocketchat_enabled
CHANNEL_PARAMETERS = [
{ key: "identifier", regex: '^[@#]\S*$', unique: true }
]
CHANNEL_PARAMETERS = [{ key: "identifier", regex: '^[@#]\S*$', unique: true }]
def self.rocketchat_message(post, channel)
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
topic = post.topic
category = ''
category = ""
if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]"
category = "[#{I18n.t("uncategorized_category_name")}]"
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
message = {
channel: channel,
attachments: []
}
message = { channel: channel, attachments: [] }
summary = {
fallback: "#{topic.title} - #{display_name}",
author_name: display_name,
author_icon: post.user.small_avatar_url,
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"],
title: "#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(', ') : ''}",
title_link: post.full_url
title:
"#{topic.title} #{category} #{topic.tags.present? ? topic.tags.map(&:name).join(", ") : ""}",
title_link: post.full_url,
}
message[:attachments].push(summary)
@ -46,25 +55,29 @@ module DiscourseChatIntegration::Provider::RocketchatProvider
uri = URI(SiteSetting.chat_integration_rocketchat_webhook_url)
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
response = http.request(req)
unless response.kind_of? Net::HTTPSuccess
if response.body.include?('invalid-channel')
error_key = 'chat_integration.provider.rocketchat.errors.invalid_channel'
if response.body.include?("invalid-channel")
error_key = "chat_integration.provider.rocketchat.errors.invalid_channel"
else
error_key = nil
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
def self.trigger_notification(post, channel, rule)
channel_id = channel.data['identifier']
channel_id = channel.data["identifier"]
message = rocketchat_message(post, channel_id)
self.send_via_webhook(message)

View File

@ -11,7 +11,7 @@ module DiscourseChatIntegration::Provider::SlackProvider
:preload_json,
:verify_authenticity_token,
:redirect_to_login_if_required,
only: [:command, :interactive]
only: %i[command interactive]
def command
message = process_command(params)
@ -28,15 +28,14 @@ module DiscourseChatIntegration::Provider::SlackProvider
private
def process_command(params)
tokens = params[:text].split(" ")
# channel name fix
channel_id =
case params[:channel_name]
when 'directmessage'
when "directmessage"
"@#{params[:user_name]}"
when 'privategroup'
when "privategroup"
params[:channel_id]
else
"##{params[:channel_name]}"
@ -44,17 +43,28 @@ module DiscourseChatIntegration::Provider::SlackProvider
provider = DiscourseChatIntegration::Provider::SlackProvider::PROVIDER_NAME
channel = DiscourseChatIntegration::Channel.with_provider(provider)
.with_data_value('identifier', channel_id)
channel =
DiscourseChatIntegration::Channel
.with_provider(provider)
.with_data_value("identifier", channel_id)
.first
channel ||= DiscourseChatIntegration::Channel.create!(
channel ||=
DiscourseChatIntegration::Channel.create!(
provider: provider,
data: { identifier: channel_id }
data: {
identifier: channel_id,
},
)
if tokens[0] == 'post'
process_post_request(channel, tokens, params[:channel_id], channel_id, params[:response_url])
if tokens[0] == "post"
process_post_request(
channel,
tokens,
params[:channel_id],
channel_id,
params[:response_url],
)
else
{ text: ::DiscourseChatIntegration::Helper.process_command(channel, tokens) }
end
@ -66,9 +76,10 @@ module DiscourseChatIntegration::Provider::SlackProvider
end
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
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
http.request(req)
end
@ -81,15 +92,16 @@ module DiscourseChatIntegration::Provider::SlackProvider
first_message_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})\/?$/
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])
requested_thread_ts = match.captures[0]
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
begin
requested_messages = Integer(tokens[1], 10)
@ -100,12 +112,21 @@ module DiscourseChatIntegration::Provider::SlackProvider
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}_history") } unless transcript.load_chat_history
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
transcript.set_first_message_by_index(-requested_messages)
else
@ -123,22 +144,21 @@ module DiscourseChatIntegration::Provider::SlackProvider
# Do nothing
elsif json[:type] == "message_action" && json[:message][:thread_ts]
# Context menu used on a threaded message
transcript = SlackTranscript.new(
transcript =
SlackTranscript.new(
channel_name: "##{json[:channel][:name]}",
channel_id: json[:channel][:id],
requested_thread_ts: json[:message][:thread_ts]
requested_thread_ts: json[:message][:thread_ts],
)
# Send a loading modal within 3 seconds:
req = Net::HTTP::Post.new(
req =
Net::HTTP::Post.new(
"https://slack.com/api/views.open",
'Content-Type' => 'application/json',
'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],
"view": transcript.build_modal_ui
}.to_json
req.body = { trigger_id: json[:trigger_id], view: transcript.build_modal_ui }.to_json
response = http.request(req)
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
# Then update the modal with the transcript link:
req = Net::HTTP::Post.new(
req =
Net::HTTP::Post.new(
"https://slack.com/api/views.update",
'Content-Type' => 'application/json',
'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,
"view": error_view || transcript.build_modal_ui
}.to_json
req.body = { view_id: view_id, view: error_view || transcript.build_modal_ui }.to_json
response = http.request(req)
else
# 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
response = http.request(req)
end
@ -177,26 +195,33 @@ module DiscourseChatIntegration::Provider::SlackProvider
constant_val = json[:callback_id]
changed_val = json[:actions][0][:selected_options][0][:value]
first_message = (action_name == 'first_message') ? changed_val : constant_val
last_message = (action_name == 'first_message') ? constant_val : changed_val
first_message = (action_name == "first_message") ? changed_val : constant_val
last_message = (action_name == "first_message") ? constant_val : changed_val
end
error_key = "chat_integration.provider.slack.transcript.error"
return { text: I18n.t(error_key) } unless transcript = SlackTranscript.new(
unless transcript =
SlackTranscript.new(
channel_name: "##{json[:channel][:name]}",
channel_id: json[:channel][:id],
requested_thread_ts: requested_thread
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}_history") } unless transcript.load_chat_history
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
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
transcript.build_slack_ui
@ -210,17 +235,11 @@ module DiscourseChatIntegration::Provider::SlackProvider
type: "modal",
title: {
type: "plain_text",
text: I18n.t("chat_integration.provider.slack.transcript.modal_title")
text: I18n.t("chat_integration.provider.slack.transcript.modal_title"),
},
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ":warning: *#{I18n.t(error_key)}*"
}
}
]
{ type: "section", text: { type: "mrkdwn", text: ":warning: *#{I18n.t(error_key)}*" } },
],
}
end
@ -229,7 +248,6 @@ module DiscourseChatIntegration::Provider::SlackProvider
if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_slack_incoming_webhook_token != params[:token]
raise Discourse::InvalidAccess.new
end
end
@ -241,7 +259,6 @@ module DiscourseChatIntegration::Provider::SlackProvider
if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? ||
SiteSetting.chat_integration_slack_incoming_webhook_token != json[:token]
raise Discourse::InvalidAccess.new
end
end
@ -256,5 +273,4 @@ module DiscourseChatIntegration::Provider::SlackProvider
post "command" => "slack_command#command"
post "interactive" => "slack_command#interactive"
end
end

View File

@ -6,13 +6,15 @@ class ChatIntegrationSlackEnabledSettingValidator
end
def valid_value?(val)
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?
return true if val == ("f") || val == (false)
if SiteSetting.chat_integration_slack_outbound_webhook_url.blank? &&
SiteSetting.chat_integration_slack_access_token.blank?
return false
end
true
end
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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,29 +5,33 @@ module DiscourseChatIntegration::Provider::TeamsProvider
PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled
CHANNEL_PARAMETERS = [
{ 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)
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.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
response = http.request(req)
unless response.kind_of? Net::HTTPSuccess
if response.body.include?('Invalid webhook URL')
error_key = 'chat_integration.provider.teams.errors.invalid_channel'
if response.body.include?("Invalid webhook URL")
error_key = "chat_integration.provider.teams.errors.invalid_channel"
else
error_key = nil
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
def self.get_message(post)
@ -41,29 +45,41 @@ module DiscourseChatIntegration::Provider::TeamsProvider
topic = post.topic
category = ''
category = ""
if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]"
category = "[#{I18n.t("uncategorized_category_name")}]"
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
message = {
"@type": "MessageCard",
"summary": topic.title,
"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),
"activityImage": post.user.small_avatar_url,
"facts": [{
"name": full_name,
"value": display_name
}],
"markdown": true
}],
summary: topic.title,
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,
),
activityImage: post.user.small_avatar_url,
facts: [{ name: full_name, value: display_name }],
markdown: true,
},
],
}
message
end
end

View File

@ -13,12 +13,11 @@ module DiscourseChatIntegration::Provider::TelegramProvider
only: :command
def command
# If it's a new message (telegram also sends hooks for other reasons that we don't care about)
if params.key?('message')
chat_id = params['message']['chat']['id']
if params.key?("message")
chat_id = params["message"]["chat"]["id"]
message_text = process_command(params['message'])
message_text = process_command(params["message"])
if message_text.present?
message = {
@ -30,13 +29,13 @@ module DiscourseChatIntegration::Provider::TelegramProvider
DiscourseChatIntegration::Provider::TelegramProvider.sendMessage(message)
end
elsif params.dig("channel_post", "text")&.include?("/getchatid")
chat_id = params["channel_post"]["chat"]["id"]
elsif params.dig('channel_post', 'text')&.include?('/getchatid')
chat_id = params['channel_post']['chat']['id']
message_text = I18n.t(
message_text =
I18n.t(
"chat_integration.provider.telegram.unknown_chat",
site_title: CGI::escapeHTML(SiteSetting.title),
site_title: CGI.escapeHTML(SiteSetting.title),
chat_id: chat_id,
)
@ -51,24 +50,28 @@ module DiscourseChatIntegration::Provider::TelegramProvider
end
# Always give telegram a success message, otherwise we'll stop receiving webhooks
data = {
success: true
}
data = { success: true }
render json: data
end
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
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 =
if channel.nil?
"unknown_chat"
elsif !SiteSetting.chat_integration_telegram_enable_slash_commands || !message['text'].start_with?('/')
elsif !SiteSetting.chat_integration_telegram_enable_slash_commands ||
!message["text"].start_with?("/")
"silent"
else
""
@ -77,17 +80,19 @@ module DiscourseChatIntegration::Provider::TelegramProvider
return "" if text_key == "silent"
if text_key.present?
return I18n.t(
return(
I18n.t(
"chat_integration.provider.telegram.#{text_key}",
site_title: CGI::escapeHTML(SiteSetting.title),
site_title: CGI.escapeHTML(SiteSetting.title),
chat_id: chat_id,
)
)
end
tokens = message['text'].split(" ")
tokens = message["text"].split(" ")
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][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)
::DiscourseChatIntegration::Helper.process_command(channel, tokens)
end
@ -97,7 +102,6 @@ module DiscourseChatIntegration::Provider::TelegramProvider
if SiteSetting.chat_integration_telegram_secret.blank? ||
SiteSetting.chat_integration_telegram_secret != params[:token]
raise Discourse::InvalidAccess.new
end
end
@ -108,7 +112,5 @@ module DiscourseChatIntegration::Provider::TelegramProvider
isolate_namespace DiscourseChatIntegration::Provider::TelegramProvider
end
TelegramEngine.routes.draw do
post "command/:token" => "telegram_command#command"
end
TelegramEngine.routes.draw { post "command/:token" => "telegram_command#command" }
end

View File

@ -7,29 +7,29 @@ module DiscourseChatIntegration
PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled
CHANNEL_PARAMETERS = [
{ 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
newSecret = SecureRandom.hex
SiteSetting.chat_integration_telegram_secret = newSecret
message = {
url: Discourse.base_url + '/chat-integration/telegram/command/' + newSecret,
}
message = { 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
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
def self.sendMessage(message)
self.do_api_request('sendMessage', message)
self.do_api_request("sendMessage", message)
end
def self.do_api_request(methodName, message)
@ -40,7 +40,7 @@ module DiscourseChatIntegration
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
response = http.request(req)
@ -54,28 +54,38 @@ module DiscourseChatIntegration
topic = post.topic
category = ''
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
tags = ''
if topic.tags.present?
tags = topic.tags.map(&:name).join(', ')
end
tags = ""
tags = topic.tags.map(&:name).join(", ") if topic.tags.present?
I18n.t(
"chat_integration.provider.telegram.message",
user: display_name,
post_url: post.full_url,
title: CGI::escapeHTML(topic.title),
post_excerpt: post.excerpt(SiteSetting.chat_integration_telegram_excerpt_length, text_entities: true, strip_links: true, remap_emoji: true),
title: CGI.escapeHTML(topic.title),
post_excerpt:
post.excerpt(
SiteSetting.chat_integration_telegram_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
)
end
def self.trigger_notification(post, channel, rule)
chat_id = channel.data['chat_id']
chat_id = channel.data["chat_id"]
message = {
chat_id: chat_id,
@ -86,18 +96,20 @@ module DiscourseChatIntegration
response = sendMessage(message)
if response['ok'] != true
if response["ok"] != true
error_key = nil
if response['description'].include? 'chat not found'
error_key = 'chat_integration.provider.telegram.errors.channel_not_found'
elsif response['description'].include? 'Forbidden'
error_key = 'chat_integration.provider.telegram.errors.forbidden'
if response["description"].include? "chat not found"
error_key = "chat_integration.provider.telegram.errors.channel_not_found"
elsif response["description"].include? "Forbidden"
error_key = "chat_integration.provider.telegram.errors.forbidden"
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

View File

@ -5,35 +5,38 @@ module DiscourseChatIntegration::Provider::WebexProvider
PROVIDER_ENABLED_SETTING = :chat_integration_webex_enabled
CHANNEL_PARAMETERS = [
{ key: "name", regex: '^\S+$', unique: true },
{ key: "webhook_url",
{
key: "webhook_url",
regex: '^https:\/\/webexapis\.com\/v1\/webhooks\/incoming\/[A-Za-z0-9\-@\/]+\S+$',
unique: true,
hidden: true }
hidden: true,
},
]
def self.trigger_notification(post, channel, rule)
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.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
response = http.request(req)
unless response.kind_of? Net::HTTPSuccess
if response.body.include?('Invalid webhook URL')
error_key = 'chat_integration.provider.webex.errors.invalid_channel'
if response.body.include?("Invalid webhook URL")
error_key = "chat_integration.provider.webex.errors.invalid_channel"
else
error_key = nil
end
raise ::DiscourseChatIntegration::ProviderError.new info: { error_key: error_key,
raise ::DiscourseChatIntegration::ProviderError.new info: {
error_key: error_key,
request: req.body,
response_code: response.code,
response_body: response.body }
response_body: response.body,
}
end
end
def self.get_message(post)
@ -41,24 +44,31 @@ module DiscourseChatIntegration::Provider::WebexProvider
topic = post.topic
category = ''
category = ""
if topic.category&.uncategorized?
category = "[#{I18n.t('uncategorized_category_name')}]"
category = "[#{I18n.t("uncategorized_category_name")}]"
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
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.excerpt(SiteSetting.chat_integration_webex_excerpt_length,
markdown +=
post.excerpt(
SiteSetting.chat_integration_webex_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true
remap_emoji: true,
)
{ "markdown": markdown }
{ markdown: markdown }
end
end

View File

@ -14,10 +14,13 @@ module DiscourseChatIntegration
uri = URI("#{SiteSetting.chat_integration_zulip_server}/api/v1/messages")
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.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)
response = http.request(req)
@ -28,23 +31,27 @@ module DiscourseChatIntegration
def self.generate_zulip_message(post, stream, subject)
display_name = ::DiscourseChatIntegration::Helper.formatted_display_name(post.user)
message = I18n.t('chat_integration.provider.zulip.message', user: display_name,
message =
I18n.t(
"chat_integration.provider.zulip.message",
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))
excerpt:
post.excerpt(
SiteSetting.chat_integration_zulip_excerpt_length,
text_entities: true,
strip_links: true,
remap_emoji: true,
),
)
data = {
type: 'stream',
to: stream,
subject: subject,
content: message
}
data = { type: "stream", to: stream, subject: subject, content: message }
end
def self.trigger_notification(post, channel, rule)
stream = channel.data['stream']
subject = channel.data['subject']
stream = channel.data["stream"]
subject = channel.data["subject"]
message = self.generate_zulip_message(post, stream, subject)
@ -52,12 +59,18 @@ module DiscourseChatIntegration
if !response.kind_of?(Net::HTTPSuccess)
error_key = nil
error_key = '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 }
error_key =
"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

View File

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

View File

@ -11,9 +11,7 @@ RSpec.shared_context "with dummy provider" do
@@raise_exception = nil
def self.trigger_notification(post, channel, rule)
if @@raise_exception
raise @@raise_exception
end
raise @@raise_exception if @@raise_exception
@@sent_messages.push(post: post.id, channel: channel)
end
@ -32,9 +30,7 @@ RSpec.shared_context "with dummy provider" do
end
end
after(:each) do
::DiscourseChatIntegration::Provider.send(:remove_const, :DummyProvider)
end
after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :DummyProvider) }
let(:provider) { ::DiscourseChatIntegration::Provider::DummyProvider }
end
@ -44,9 +40,7 @@ RSpec.shared_context "with validated dummy provider" do
module ::DiscourseChatIntegration::Provider::Dummy2Provider
PROVIDER_NAME = "dummy2".freeze
PROVIDER_ENABLED_SETTING = :chat_integration_enabled # Tie to main plugin enabled setting
CHANNEL_PARAMETERS = [
{ key: "val", regex: '^\S+$', unique: true }
]
CHANNEL_PARAMETERS = [{ key: "val", regex: '^\S+$', unique: true }]
@@sent_messages = []
@ -58,10 +52,7 @@ RSpec.shared_context "with validated dummy provider" do
@@sent_messages
end
end
end
after(:each) do
::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider)
end
after(:each) { ::DiscourseChatIntegration::Provider.send(:remove_const, :Dummy2Provider) }
end

View File

@ -1,213 +1,258 @@
# frozen_string_literal: true
require 'rails_helper'
require_relative '../dummy_provider'
require "rails_helper"
require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Manager do
include_context "with dummy provider"
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') }
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:category) { Fabricate(:category) }
let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) }
let(:tag3) { Fabricate(:tag) }
describe '.process_command' do
describe 'add new rule' do
describe ".process_command" do
describe "add new rule" do
# 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
it 'should add a new rule correctly' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug])
it "should add a new rule correctly" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ["watch", category.slug])
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.filter).to eq("watch")
expect(rule.category_id).to eq(category.id)
expect(rule.tags).to eq(nil)
end
it 'should work with all four filter types' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['thread', category.slug])
it "should work with all four filter types" do
response =
DiscourseChatIntegration::Helper.process_command(chan1, ["thread", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('thread')
expect(rule.filter).to eq("thread")
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', category.slug])
response = DiscourseChatIntegration::Helper.process_command(chan1, ["watch", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('watch')
expect(rule.filter).to eq("watch")
response = DiscourseChatIntegration::Helper.process_command(chan1, ['follow', category.slug])
response =
DiscourseChatIntegration::Helper.process_command(chan1, ["follow", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('follow')
expect(rule.filter).to eq("follow")
response = DiscourseChatIntegration::Helper.process_command(chan1, ['mute', category.slug])
response = DiscourseChatIntegration::Helper.process_command(chan1, ["mute", category.slug])
rule = DiscourseChatIntegration::Rule.all.first
expect(rule.filter).to eq('mute')
expect(rule.filter).to eq("mute")
end
it 'errors on incorrect categories' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['watch', 'blah'])
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'))
expect(response).to eq(
I18n.t(
"chat_integration.provider.dummy.not_found.category",
name: "blah",
list: "uncategorized",
),
)
end
context 'with tags enabled' do
before do
SiteSetting.tagging_enabled = true
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}"])
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
expect(rule.channel).to eq(chan1)
expect(rule.filter).to eq('watch')
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}"])
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.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"))
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
describe 'remove rule' do
it 'removes the rule' do
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'watch',
describe "remove rule" do
it "removes the rule" do
rule1 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
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(DiscourseChatIntegration::Rule.all.size).to eq(0)
end
it 'errors correctly' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['remove', '1'])
it "errors correctly" do
response = DiscourseChatIntegration::Helper.process_command(chan1, %w[remove 1])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.error"))
end
end
describe 'help command' do
it 'should return the right response' do
describe "help command" do
it "should return the right response" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ["help"])
expect(response).to eq(I18n.t("chat_integration.provider.dummy.help"))
end
end
describe 'status command' do
it 'should return the right response' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['status'])
describe "status command" do
it "should return the right response" do
response = DiscourseChatIntegration::Helper.process_command(chan1, ["status"])
expect(response).to eq(DiscourseChatIntegration::Helper.status_for_channel(chan1))
end
end
describe 'unknown command' do
it 'should return the right response' do
response = DiscourseChatIntegration::Helper.process_command(chan1, ['somerandomtext'])
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
describe '.status_for_channel' do
context 'with no rules' do
it 'includes the heading' do
describe ".status_for_channel" do
context "with no rules" do
it "includes the heading" do
string = DiscourseChatIntegration::Helper.status_for_channel(chan1)
expect(string).to include('dummy.status.header')
expect(string).to include("dummy.status.header")
end
it 'includes the no_rules string' do
it "includes the no_rules string" do
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
context 'with some rules' do
context "with some rules" do
let(:group) { Fabricate(:group) }
before do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch', category_id: category.id, 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)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
category_id: category.id,
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
end
it 'displays the correct rules' do
it "displays the correct rules" do
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
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)
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
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
describe '.delete_by_index' do
describe ".delete_by_index" do
let(:category2) { Fabricate(:category) }
let(:category3) { Fabricate(:category) }
it 'deletes the correct rule' do
it "deletes the correct rule" do
# Three identical rules, with different filters
# Status will be sorted by precedence
# be in this order
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'mute',
rule1 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "mute",
category_id: category.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
rule2 = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'watch',
rule2 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "watch",
category_id: category2.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
rule3 = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'follow',
rule3 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "follow",
category_id: category3.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
expect(DiscourseChatIntegration::Rule.all.size).to eq(3)
@ -215,14 +260,19 @@ RSpec.describe DiscourseChatIntegration::Manager do
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 2)).to eq(:deleted)
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
it 'fails gracefully for out of range indexes' do
rule1 = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'watch',
it "fails gracefully for out of range indexes" do
rule1 =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, -1)).to eq(false)
@ -231,87 +281,100 @@ RSpec.describe DiscourseChatIntegration::Manager do
expect(DiscourseChatIntegration::Helper.delete_by_index(chan1, 1)).to eq(:deleted)
end
end
describe '.smart_create_rule' do
it 'creates a rule when there are none' do
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1,
filter: 'watch',
describe ".smart_create_rule" do
it "creates a rule when there are none" do
val =
DiscourseChatIntegration::Helper.smart_create_rule(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag1.name]
tags: [tag1.name],
)
expect(val).to eq(:created)
record = DiscourseChatIntegration::Rule.all.first
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.tags).to eq([tag1.name])
end
it 'updates a rule when it has the same category and tags' do
existing = DiscourseChatIntegration::Rule.create!(channel: chan1,
filter: 'watch',
it "updates a rule when it has the same category and tags" do
existing =
DiscourseChatIntegration::Rule.create!(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag2.name, tag1.name]
tags: [tag2.name, tag1.name],
)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1,
filter: 'mute',
val =
DiscourseChatIntegration::Helper.smart_create_rule(
channel: chan1,
filter: "mute",
category_id: category.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
expect(val).to eq(:updated)
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
it 'updates a rule when it has the same category and filter' do
existing = DiscourseChatIntegration::Rule.create(channel: chan1,
filter: 'watch',
it "updates a rule when it has the same category and filter" do
existing =
DiscourseChatIntegration::Rule.create(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag1.name, tag2.name]
tags: [tag1.name, tag2.name],
)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1,
filter: 'watch',
val =
DiscourseChatIntegration::Helper.smart_create_rule(
channel: chan1,
filter: "watch",
category_id: category.id,
tags: [tag1.name, tag3.name]
tags: [tag1.name, tag3.name],
)
expect(val).to eq(:updated)
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
it 'destroys duplicate rules on save' do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch')
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'watch')
it "destroys duplicate rules on save" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch")
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch")
expect(DiscourseChatIntegration::Rule.all.size).to eq(2)
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1,
filter: 'watch',
val =
DiscourseChatIntegration::Helper.smart_create_rule(
channel: chan1,
filter: "watch",
category_id: nil,
tags: nil
tags: nil,
)
expect(val).to eq(:updated)
expect(DiscourseChatIntegration::Rule.all.size).to eq(1)
end
it 'returns false on error' do
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, filter: 'blah')
it "returns false on error" do
val = DiscourseChatIntegration::Helper.smart_create_rule(channel: chan1, filter: "blah")
expect(val).to eq(false)
end
end
describe '.save_transcript' do
it 'saves a transcript to redis' do
describe ".save_transcript" do
it "saves a transcript to redis" do
key = DiscourseChatIntegration::Helper.save_transcript("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
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
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
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
it "only displays one when name/username are similar" do
user.update!(username: "john_smith")
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
expect(DiscourseChatIntegration::Helper.formatted_display_name(user)).to eq("John Smith")
end
@ -346,10 +415,13 @@ RSpec.describe DiscourseChatIntegration::Manager do
SiteSetting.enable_names = false
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
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,39 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GitterProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
before do
SiteSetting.chat_integration_gitter_enabled = true
describe ".trigger_notifications" do
before { 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
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)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 404, body: "{ \"error\": \"Not Found\"}")
it "handles errors correctly" do
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 { 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
end
end

View File

@ -1,30 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GoogleProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
before do
SiteSetting.chat_integration_google_enabled = true
describe ".trigger_notifications" do
before { 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
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)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 400, body: "{}")
it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(status: 400, body: "{}")
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
end
end
end

View File

@ -1,28 +1,41 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GroupmeProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
describe ".trigger_notifications" do
before do
SiteSetting.chat_integration_groupme_enabled = true
SiteSetting.chat_integration_groupme_bot_ids = '1a2b3c4d5e6f7g'
SiteSetting.chat_integration_groupme_bot_ids = "1a2b3c4d5e6f7g"
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)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, "https://api.groupme.com/v3/bots/post").to_return(status: 404, body: "{ \"error\": \"Not Found\"}")
it "handles errors correctly" do
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 { 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
end
end

View File

@ -1,30 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::GuildedProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
before do
SiteSetting.chat_integration_guilded_enabled = true
describe ".trigger_notifications" do
before { 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
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 = stub_request(:post, 'https://media.guilded.gg/webhooks/1234/abcd').to_return(status: 200)
it "sends a webhook request" do
stub1 =
stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 400)
it "handles errors correctly" do
stub1 =
stub_request(:post, "https://media.guilded.gg/webhooks/1234/abcd").to_return(status: 400)
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
end
end
end

View File

@ -1,31 +1,47 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::MatrixProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
describe ".trigger_notifications" do
before do
SiteSetting.chat_integration_matrix_enabled = true
SiteSetting.chat_integration_matrix_access_token = 'abcd'
SiteSetting.chat_integration_matrix_access_token = "abcd"
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
stub1 = stub_request(:put, %r{https://matrix.org/_matrix/client/r0/rooms/!blah:matrix.org/send/m.room.message/*}).to_return(status: 200)
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)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
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"}')
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"}')
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
end
end
end

View File

@ -1,87 +1,93 @@
# 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(:tag) { 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
it 'should return a 404' do
post '/chat-integration/mattermost/command.json'
describe "with plugin disabled" do
it "should return a 404" do
post "/chat-integration/mattermost/command.json"
expect(response.status).to eq(404)
end
end
describe 'with plugin enabled and provider disabled' do
describe "with plugin enabled and provider disabled" do
before do
SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_mattermost_enabled = false
end
it 'should return a 404' do
post '/chat-integration/mattermost/command.json'
it "should return a 404" do
post "/chat-integration/mattermost/command.json"
expect(response.status).to eq(404)
end
end
describe 'slash commands endpoint' do
describe "slash commands endpoint" do
before do
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
end
describe 'when forum is private' do
it 'should not redirect to login page' do
describe "when forum is private" do
it "should not redirect to login page" do
SiteSetting.login_required = true
token = 'sometoken'
token = "sometoken"
SiteSetting.chat_integration_mattermost_incoming_webhook_token = token
post '/chat-integration/mattermost/command.json', params: {
text: 'help', token: token
}
post "/chat-integration/mattermost/command.json", params: { text: "help", token: token }
expect(response.status).to eq(200)
end
end
describe 'when the token is invalid' do
it 'should raise the right error' do
post '/chat-integration/mattermost/command.json', params: { text: 'help' }
describe "when the token is invalid" do
it "should raise the right error" do
post "/chat-integration/mattermost/command.json", params: { text: "help" }
expect(response.status).to eq(400)
end
end
describe 'when incoming webhook token has not been set' do
it 'should raise the right error' do
post '/chat-integration/mattermost/command.json', params: {
text: 'help', token: 'some token'
describe "when incoming webhook token has not been set" do
it "should raise the right error" do
post "/chat-integration/mattermost/command.json",
params: {
text: "help",
token: "some token",
}
expect(response.status).to eq(403)
end
end
describe 'when token is valid' do
describe "when token is valid" do
let(:token) { "Secret Sauce" }
# No need to test every single command here, that's tested
# by helper_spec upstream
before do
SiteSetting.chat_integration_mattermost_incoming_webhook_token = token
end
before { SiteSetting.chat_integration_mattermost_incoming_webhook_token = token }
describe 'add new rule' do
it 'should add a new rule correctly' do
post "/chat-integration/mattermost/command.json", params: {
describe "add new rule" do
it "should add a new rule correctly" do
post "/chat-integration/mattermost/command.json",
params: {
text: "watch #{category.slug}",
channel_name: 'welcome',
token: token
channel_name: "welcome",
token: token,
}
json = response.parsed_body
@ -90,34 +96,40 @@ describe 'Mattermost Command Controller', type: :request do
rule = DiscourseChatIntegration::Rule.all.first
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.tags).to eq(nil)
end
describe 'from an unknown channel' do
it 'creates the channel' do
post "/chat-integration/mattermost/command.json", params: {
describe "from an unknown channel" do
it "creates the channel" do
post "/chat-integration/mattermost/command.json",
params: {
text: "watch #{category.slug}",
channel_name: 'general',
token: token
channel_name: "general",
token: token,
}
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
expect(chan.provider).to eq('mattermost')
chan =
DiscourseChatIntegration::Channel
.with_provider("mattermost")
.with_data_value("identifier", "#general")
.first
expect(chan.provider).to eq("mattermost")
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.tags).to eq(nil)
end
end
end
end
end
end

View File

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

View File

@ -1,31 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::RocketchatProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
describe ".trigger_notifications" do
before do
SiteSetting.chat_integration_rocketchat_enabled = true
SiteSetting.chat_integration_rocketchat_webhook_url = "https://example.com/abcd"
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
stub1 = stub_request(:post, 'https://example.com/abcd').to_return(body: "{\"success\":true}")
it "sends a webhook request" do
stub1 = stub_request(:post, "https://example.com/abcd").to_return(body: "{\"success\":true}")
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, 'https://example.com/abcd').to_return(status: 400, body: "{}")
it "handles errors correctly" do
stub1 = stub_request(:post, "https://example.com/abcd").to_return(status: 400, body: "{}")
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
end
end
end

View File

@ -1,105 +1,98 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe 'Slack Command Controller', type: :request do
before do
Discourse.cache.clear
end
describe "Slack Command Controller", type: :request do
before { Discourse.cache.clear }
let(:category) { Fabricate(:category) }
let(:tag) { 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
it 'should return a 404' do
post '/chat-integration/slack/command.json'
describe "with plugin disabled" do
it "should return a 404" do
post "/chat-integration/slack/command.json"
expect(response.status).to eq(404)
end
end
describe 'with plugin enabled and provider disabled' do
describe "with plugin enabled and provider disabled" do
before do
SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_slack_enabled = false
end
it 'should return a 404' do
post '/chat-integration/slack/command.json'
it "should return a 404" do
post "/chat-integration/slack/command.json"
expect(response.status).to eq(404)
end
end
describe 'slash commands endpoint' do
describe "slash commands endpoint" do
before do
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
end
describe 'when forum is private' do
it 'should not redirect to login page' do
describe "when forum is private" do
it "should not redirect to login page" do
SiteSetting.login_required = true
token = 'sometoken'
token = "sometoken"
SiteSetting.chat_integration_slack_incoming_webhook_token = token
post '/chat-integration/slack/command.json', params: {
text: 'help', token: token
}
post "/chat-integration/slack/command.json", params: { text: "help", token: token }
expect(response.status).to eq(200)
end
end
describe 'when the token is invalid' do
it 'should raise the right error' do
post '/chat-integration/slack/command.json', params: { text: 'help' }
describe "when the token is invalid" do
it "should raise the right error" do
post "/chat-integration/slack/command.json", params: { text: "help" }
expect(response.status).to eq(400)
end
end
describe 'backwards compatibility with discourse-slack-official' do
it 'should return the right response' do
token = 'secret sauce'
describe "backwards compatibility with discourse-slack-official" do
it "should return the right response" do
token = "secret sauce"
SiteSetting.chat_integration_slack_incoming_webhook_token = token
post '/slack/command.json', params: {
text: 'help', token: token
}
post "/slack/command.json", params: { text: "help", token: token }
expect(response.status).to eq(200)
expect(response.parsed_body["text"]).to be_present
end
end
describe 'when incoming webhook token has not been set' do
it 'should raise the right error' do
post '/chat-integration/slack/command.json', params: {
text: 'help', token: 'some token'
}
describe "when incoming webhook token has not been set" do
it "should raise the right error" do
post "/chat-integration/slack/command.json", params: { text: "help", token: "some token" }
expect(response.status).to eq(403)
end
end
describe 'when token is valid' do
describe "when token is valid" do
let(:token) { "Secret Sauce" }
# No need to test every single command here, that's tested
# by helper_spec upstream
before do
SiteSetting.chat_integration_slack_incoming_webhook_token = token
end
before { SiteSetting.chat_integration_slack_incoming_webhook_token = token }
describe 'add new rule' do
it 'should add a new rule correctly' do
post "/chat-integration/slack/command.json", params: {
describe "add new rule" do
it "should add a new rule correctly" do
post "/chat-integration/slack/command.json",
params: {
text: "watch #{category.slug}",
channel_name: 'welcome',
token: token
channel_name: "welcome",
token: token,
}
json = response.parsed_body
@ -108,228 +101,258 @@ describe 'Slack Command Controller', type: :request do
rule = DiscourseChatIntegration::Rule.all.first
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.tags).to eq(nil)
end
describe 'from an unknown channel' do
it 'creates the channel' do
post "/chat-integration/slack/command.json", params: {
describe "from an unknown channel" do
it "creates the channel" do
post "/chat-integration/slack/command.json",
params: {
text: "watch #{category.slug}",
channel_name: 'general',
token: token
channel_name: "general",
token: token,
}
json = response.parsed_body
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
expect(chan.provider).to eq('slack')
chan =
DiscourseChatIntegration::Channel
.with_provider("slack")
.with_data_value("identifier", "#general")
.first
expect(chan.provider).to eq("slack")
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.tags).to eq(nil)
end
end
end
describe 'post transcript' do
let(:messages_fixture) {
describe "post transcript" do
let(:messages_fixture) do
[
{
"type": "message",
"user": "U6JSSESES",
"text": "Yeah, should make posting slack transcripts much easier",
"ts": "1501801665.062694"
type: "message",
user: "U6JSSESES",
text: "Yeah, should make posting slack transcripts much easier",
ts: "1501801665.062694",
},
{
"type": "message",
"user": "U5Z773QLS",
"text": "Oooh a new discourse plugin???",
"ts": "1501801643.056375"
type: "message",
user: "U5Z773QLS",
text: "Oooh a new discourse plugin???",
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",
"user": "U6E2W7R8C",
"text": "Which one?",
"ts": "1501801634.053761"
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",
"user": "U6JSSESES",
"text": "So, who's interested in the new <https://meta.discourse.org|discourse plugin>?",
"ts": "1501801629.052212"
},
{
"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",
subtype: "bot_message",
ts: "1501615820.949638",
},
{
"type": "message",
"user": "U5Z773QLS",
"text": "Lets try some *bold text*",
"ts": "1501093331.439776"
type: "message",
user: "U5Z773QLS",
text: "Lets try some *bold text*",
ts: "1501093331.439776",
},
]
}
before do
SiteSetting.chat_integration_slack_access_token = 'abcde'
end
before { SiteSetting.chat_integration_slack_access_token = "abcde" }
context "with valid slack responses" 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/conversations.history").to_return(body: { ok: true, messages: messages_fixture }.to_json)
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/conversations.history").to_return(
body: { ok: true, messages: messages_fixture }.to_json,
)
end
it 'generates the transcript UI properly' do
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: /attachments/)
.to_return(body: { ok: true }.to_json)
it "generates the transcript UI properly" do
command_stub =
stub_request(:post, "https://slack.com/commands/1234").with(
body: /attachments/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: {
post "/chat-integration/slack/command.json",
params: {
text: "post",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
end
it 'can select by url' do
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: /1501801629\.052212/)
.to_return(body: { ok: true }.to_json)
it "can select by url" do
command_stub =
stub_request(:post, "https://slack.com/commands/1234").with(
body: /1501801629\.052212/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: {
text: "post https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
post "/chat-integration/slack/command.json",
params: {
text:
"post https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
end
it 'can select by url with thread parameter' do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies")
.with(body: /1501801629\.052212/)
.to_return(body: { ok: true, messages: messages_fixture }.to_json)
it "can select by url with thread parameter" do
replies_stub =
stub_request(:post, "https://slack.com/api/conversations.replies").with(
body: /1501801629\.052212/,
).to_return(body: { ok: true, messages: messages_fixture }.to_json)
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.to_return(body: { ok: true }.to_json)
command_stub =
stub_request(:post, "https://slack.com/commands/1234").to_return(
body: { ok: true }.to_json,
)
post "/chat-integration/slack/command.json", params: {
text: "post https://sometestslack.slack.com/archives/C6029G78F/p1501201669054212?thread_ts=1501801629.052212",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
post "/chat-integration/slack/command.json",
params: {
text:
"post https://sometestslack.slack.com/archives/C6029G78F/p1501201669054212?thread_ts=1501801629.052212",
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
expect(replies_stub).to have_been_requested
end
it 'can select by thread' do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies")
.with(body: /1501801629\.052212/)
.to_return(body: { ok: true, messages: messages_fixture }.to_json)
it "can select by thread" do
replies_stub =
stub_request(:post, "https://slack.com/api/conversations.replies").with(
body: /1501801629\.052212/,
).to_return(body: { ok: true, messages: messages_fixture }.to_json)
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.to_return(body: { ok: true }.to_json)
command_stub =
stub_request(:post, "https://slack.com/commands/1234").to_return(
body: { ok: true }.to_json,
)
post "/chat-integration/slack/command.json", params: {
text: "post thread https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
post "/chat-integration/slack/command.json",
params: {
text:
"post thread https://sometestslack.slack.com/archives/C6029G78F/p1501801629052212",
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
expect(replies_stub).to have_been_requested
end
it 'can select by count' do
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: /1501801629\.052212/)
.to_return(body: { ok: true }.to_json)
it "can select by count" do
command_stub =
stub_request(:post, "https://slack.com/commands/1234").with(
body: /1501801629\.052212/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: {
post "/chat-integration/slack/command.json",
params: {
text: "post 4",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
end
it 'can auto select' do
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: /1501615820\.949638/)
.to_return(body: { ok: true }.to_json)
it "can auto select" do
command_stub =
stub_request(:post, "https://slack.com/commands/1234").with(
body: /1501615820\.949638/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/command.json", params: {
post "/chat-integration/slack/command.json",
params: {
text: "post",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
expect(command_stub).to have_been_requested
end
it "supports using shortcuts to create a thread transcript" do
replies_stub = stub_request(:post, "https://slack.com/api/conversations.replies")
.with(body: /1501801629\.052212/)
.to_return(body: { ok: true, messages: messages_fixture }.to_json)
replies_stub =
stub_request(:post, "https://slack.com/api/conversations.replies").with(
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")
.with(body: /TRIGGERID/)
.to_return(body: { ok: true, view: { id: "VIEWID" } }.to_json)
view_open_stub =
stub_request(:post, "https://slack.com/api/views.open").with(
body: /TRIGGERID/,
).to_return(body: { ok: true, view: { id: "VIEWID" } }.to_json)
view_update_stub = stub_request(:post, "https://slack.com/api/views.update")
.with(body: /VIEWID/)
.to_return(body: { ok: true }.to_json)
view_update_stub =
stub_request(:post, "https://slack.com/api/views.update").with(
body: /VIEWID/,
).to_return(body: { ok: true }.to_json)
post "/chat-integration/slack/interactive.json", params: {
post "/chat-integration/slack/interactive.json",
params: {
payload: {
type: "message_action",
channel: { name: 'general', id: 'C6029G78F' },
channel: {
name: "general",
id: "C6029G78F",
},
trigger_id: "TRIGGERID",
message: { thread_ts: "1501801629.052212" },
token: token
}.to_json
message: {
thread_ts: "1501801629.052212",
},
token: token,
}.to_json,
}
expect(response.status).to eq(200)
@ -337,48 +360,55 @@ describe 'Slack Command Controller', type: :request do
expect(view_open_stub).to have_been_requested
expect(view_update_stub).to have_been_requested
end
end
it 'deals with failed API calls correctly' do
command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: { text: I18n.t("chat_integration.provider.slack.transcript.error_users") })
.to_return(body: { ok: true }.to_json)
it "deals with failed API calls correctly" do
command_stub =
stub_request(:post, "https://slack.com/commands/1234").with(
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)
post "/chat-integration/slack/command.json", params: {
post "/chat-integration/slack/command.json",
params: {
text: "post 2",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
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
end
it 'errors correctly if there is no api key' do
SiteSetting.chat_integration_slack_access_token = ''
it "errors correctly if there is no api key" do
SiteSetting.chat_integration_slack_access_token = ""
post "/chat-integration/slack/command.json", params: {
post "/chat-integration/slack/command.json",
params: {
text: "post 2",
response_url: 'https://hooks.slack.com/commands/1234',
channel_name: 'general',
channel_id: 'C6029G78F',
token: token
response_url: "https://hooks.slack.com/commands/1234",
channel_name: "general",
channel_id: "C6029G78F",
token: token,
}
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

View File

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

View File

@ -1,24 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
let(:post) { Fabricate(:post) }
describe '.excerpt' do
describe 'when post contains emoijs' do
before do
post.update!(raw: ':slight_smile: This is a test')
end
describe ".excerpt" do
describe "when post contains emoijs" do
before { post.update!(raw: ":slight_smile: This is a test") }
it 'should return the right excerpt' do
expect(described_class.excerpt(post)).to eq('🙂 This is a test')
it "should return the right excerpt" do
expect(described_class.excerpt(post)).to eq("🙂 This is a test")
end
end
describe 'when post contains onebox' do
it 'should return the right excerpt' do
post.update!(cooked: <<~COOKED
describe "when post contains onebox" do
it "should return the right excerpt" do
post.update!(cooked: <<~COOKED)
<aside class=\"onebox whitelistedgeneric\">
<header class=\"source\">
<a href=\"http://somesource.com\">
@ -45,59 +43,97 @@ RSpec.describe DiscourseChatIntegration::Provider::SlackProvider do
<div style=\"clear: both\"></div>
</aside>
COOKED
)
expect(described_class.excerpt(post))
.to eq('<http://somesource.com|meta.discourse.org>')
expect(described_class.excerpt(post)).to eq("<http://somesource.com|meta.discourse.org>")
end
end
describe 'when post contains an email' do
it 'should return the right excerpt' do
post.update!(cooked: <<~COOKED
describe "when post contains an email" do
it "should return the right excerpt" do
post.update!(cooked: <<~COOKED)
The address is <a href=\"mailto:someone@domain.com\">my email</a>
COOKED
expect(described_class.excerpt(post)).to eq(
"The address is <mailto:someone@domain.com|my email>",
)
expect(described_class.excerpt(post))
.to eq('The address is <mailto:someone@domain.com|my email>')
end
end
end
describe '.trigger_notifications' do
describe ".trigger_notifications" 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
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
stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(body: "success")
it "sends a webhook request" do
stub1 =
stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(
body: "success",
)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(status: 400, body: "error")
it "handles errors correctly" do
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 { 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
end
describe 'with api token' do
describe "with api token" do
before do
SiteSetting.chat_integration_slack_access_token = "magic"
@ts = "#{Time.now.to_i}.012345"
@ts2 = "#{Time.now.to_i}.012346"
@stub1 = stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(body: "success")
@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' })
@stub1 =
stub_request(:post, SiteSetting.chat_integration_slack_outbound_webhook_url).to_return(
body: "success",
)
@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
it 'sends an api request' do
it "sends an api request" do
expect(@stub2).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)
end
it 'sends thread id for thread' do
it "sends thread id for thread" do
expect(@thread_stub).to have_been_requested.times(0)
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
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)
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")
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)
end
it 'recognizes slack thread ts in comment' do
post.update!(cooked: "cooked", raw: <<~RAW
it "recognizes slack thread ts in comment" do
post.update!(cooked: "cooked", raw: <<~RAW)
My fingers are typing words that improve `raw_quality`
<!--SLACK_CHANNEL_ID=#general;SLACK_TS=#{@ts}-->
RAW
)
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)
end
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' })
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChatIntegration::ProviderError)
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",
},
)
expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(
::DiscourseChatIntegration::ProviderError,
)
expect(@stub2).to have_been_requested.once
end
end
end
end

View File

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

View File

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

View File

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

View File

@ -1,33 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::TelegramProvider do
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
SiteSetting.chat_integration_telegram_access_token = "TOKEN"
SiteSetting.chat_integration_telegram_enabled = true
SiteSetting.chat_integration_telegram_secret = 'shhh'
SiteSetting.chat_integration_telegram_secret = "shhh"
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
stub1 = stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":true}")
it "sends a webhook request" do
stub1 =
stub_request(:post, "https://api.telegram.org/botTOKEN/sendMessage").to_return(
body: "{\"ok\":true}",
)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":false, \"description\":\"chat not found\"}")
it "handles errors correctly" do
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 { 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
end
end
end

View File

@ -1,30 +1,37 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::WebexProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
before do
SiteSetting.chat_integration_webex_enabled = true
describe ".trigger_notifications" do
before { 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
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)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, chan1.data['webhook_url']).to_return(status: 400, body: "{}")
it "handles errors correctly" do
stub1 = stub_request(:post, chan1.data["webhook_url"]).to_return(status: 400, body: "{}")
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
end
end
end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe DiscourseChatIntegration::Provider::ZulipProvider do
let(:post) { Fabricate(:post) }
describe '.trigger_notifications' do
describe ".trigger_notifications" do
before do
SiteSetting.chat_integration_zulip_enabled = true
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"
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
stub1 = stub_request(:post, 'https://hello.world/api/v1/messages').to_return(status: 200)
it "sends a webhook request" do
stub1 = stub_request(:post, "https://hello.world/api/v1/messages").to_return(status: 200)
described_class.trigger_notification(post, chan1, nil)
expect(stub1).to have_been_requested.once
end
it 'handles errors correctly' do
stub1 = stub_request(:post, 'https://hello.world/api/v1/messages').to_return(status: 400, body: '{}')
it "handles errors correctly" do
stub1 =
stub_request(:post, "https://hello.world/api/v1/messages").to_return(
status: 400,
body: "{}",
)
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
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
# frozen_string_literal: true
require 'rails_helper'
require_dependency 'post_creator'
require_relative '../dummy_provider'
require "rails_helper"
require_dependency "post_creator"
require_relative "../dummy_provider"
RSpec.describe DiscourseChatIntegration::Manager do
let(:manager) { ::DiscourseChatIntegration::Manager }
let(:category) { Fabricate(:category) }
let(:group) { Fabricate(:group) }
@ -14,31 +13,37 @@ RSpec.describe DiscourseChatIntegration::Manager do
let(:first_post) { Fabricate(:post, topic: topic) }
let(:second_post) { Fabricate(:post, topic: topic, post_number: 2) }
describe '.trigger_notifications' do
describe ".trigger_notifications" do
include_context "with dummy provider"
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') }
let(:chan3) { DiscourseChatIntegration::Channel.create!(provider: 'dummy') }
let(:chan1) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan2) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
let(:chan3) { DiscourseChatIntegration::Channel.create!(provider: "dummy") }
before do
SiteSetting.chat_integration_enabled = true
end
before { SiteSetting.chat_integration_enabled = true }
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
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)
expect(provider.sent_to_channel_ids).to contain_exactly()
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq('hello')
expect(provider.sent_to_channel_ids).to contain_exactly
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq("hello")
# Triggering a different error should set the error_key to a generic message
provider.set_raise_exception(StandardError.new "hello")
manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly()
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq('chat_integration.channel_exception')
expect(provider.sent_to_channel_ids).to contain_exactly
expect(DiscourseChatIntegration::Channel.all.first.error_key).to eq(
"chat_integration.channel_exception",
)
provider.set_raise_exception(nil)
@ -48,17 +53,33 @@ RSpec.describe DiscourseChatIntegration::Manager do
it "should not send notifications when provider is disabled" do
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)
expect(provider.sent_to_channel_ids).to contain_exactly()
expect(provider.sent_to_channel_ids).to contain_exactly
end
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!(channel: chan2, filter: 'follow', category_id: category.id)
DiscourseChatIntegration::Rule.create!(channel: chan3, filter: 'mute', category_id: category.id)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
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)
@ -66,9 +87,21 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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!(channel: chan2, filter: 'follow', category_id: category.id)
DiscourseChatIntegration::Rule.create!(channel: chan3, filter: 'mute', category_id: category.id)
DiscourseChatIntegration::Rule.create!(
channel: chan1,
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)
@ -76,7 +109,7 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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)
@ -84,17 +117,25 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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: 'mute', category_id: category.id) # Specific mute
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
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
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: 'watch', category_id: category.id) # Specific watch
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
manager.trigger_notifications(second_post.id)
@ -102,8 +143,12 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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: 'thread', category_id: category.id) # Specific thread
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
manager.trigger_notifications(second_post.id)
@ -111,18 +156,23 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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)
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
it "should work for group pms" do
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: chan1, filter: "watch") # Wildcard 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.topic.invite_group(Fabricate(:user), group)
@ -133,8 +183,18 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group2.id)
DiscourseChatIntegration::Rule.create!(
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.topic.invite_group(Fabricate(:user), group)
@ -146,40 +206,77 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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: chan2, type: 'group_message', filter: 'watch', group_id: group.id)
DiscourseChatIntegration::Rule.create!(channel: chan3, type: 'group_mention', filter: 'watch', group_id: group.id)
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!(
channel: chan3,
type: "group_mention",
filter: "watch",
group_id: group.id,
)
manager.trigger_notifications(third_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id, chan3.id)
end
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)
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)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end
it "should not notify about mentions in private messages" do
# 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
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
private_message = Fabricate(:private_message_post)
private_message.topic.invite_group(Fabricate(:user), group)
# 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
manager.trigger_notifications(mention_post.id)
@ -187,15 +284,15 @@ RSpec.describe DiscourseChatIntegration::Manager do
end
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
group = Fabricate(:group, name: "friends")
user = Fabricate(:user, username: 'david')
user = Fabricate(:user, username: "david")
group.add(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
category = Fabricate(:category, name: "Test category")
@ -208,7 +305,7 @@ RSpec.describe DiscourseChatIntegration::Manager do
# Check no notification sent
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
category.set_permissions(Group[:friends] => :full)
@ -217,20 +314,17 @@ RSpec.describe DiscourseChatIntegration::Manager do
# Check notification sent
manager.trigger_notifications(first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end
describe 'with tags enabled' do
let(:tag) { Fabricate(:tag, name: 'gsoc') }
describe "with tags enabled" do
let(:tag) { Fabricate(:tag, name: "gsoc") }
let(:tagged_topic) { Fabricate(:topic, category_id: category.id, tags: [tag]) }
let(:tagged_first_post) { Fabricate(:post, topic: tagged_topic) }
before(:each) do
SiteSetting.tagging_enabled = true
end
before(:each) { SiteSetting.tagging_enabled = true }
it 'should still work for rules without any tags specified' do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: 'follow', category_id: nil) # Wildcard watch
it "should still work for rules without any tags specified" do
DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "follow", category_id: nil) # Wildcard watch
manager.trigger_notifications(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)
end
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])
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],
)
manager.trigger_notifications(first_post.id)
manager.trigger_notifications(tagged_first_post.id)
expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id)
end
end
end
end