diff --git a/app/controllers/chat_controller.rb b/app/controllers/chat_controller.rb index 63e9b9e..42879f8 100644 --- a/app/controllers/chat_controller.rb +++ b/app/controllers/chat_controller.rb @@ -36,6 +36,7 @@ class DiscourseChat::ChatController < ApplicationController rescue Discourse::InvalidParameters, ActiveRecord::RecordNotFound => e render json: {errors: [e.message]}, status: 422 rescue DiscourseChat::ProviderError => e + Rails.logger.error("Test provider failed #{e.info}") if e.info.key?(:error_key) and !e.info[:error_key].nil? render json: {error_key: e.info[:error_key]}, status: 422 else diff --git a/app/helpers/helper.rb b/app/helpers/helper.rb index 50c0479..d27d587 100644 --- a/app/helpers/helper.rb +++ b/app/helpers/helper.rb @@ -1,6 +1,77 @@ module DiscourseChat module Helper + def self.process_command(channel, tokens) + guardian = DiscourseChat::Manager.guardian + + provider = channel.provider + + cmd = tokens.shift if tokens.size >= 1 + + error_text = I18n.t("chat_integration.provider.#{provider}.parse_error") + + case cmd + when "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 + + 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) + end + else + category = nil # All categories + end + + tags = [] + # 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:/, '') + else + return error_text # Abort and send help text + end + + tag = Tag.find_by(name: tag_name) + unless tag # If tag doesn't exist, abort + return I18n.t("chat_integration.provider.#{provider}.not_found.tag", name: tag_name) + end + tags.push(tag.name) + end + + category_id = category.nil? ? nil : category.id + case DiscourseChat::Helper.smart_create_rule(channel:channel, filter:cmd, category_id: category_id, tags:tags) + when :created + return I18n.t("chat_integration.provider.#{provider}.create.created") + when :updated + return I18n.t("chat_integration.provider.#{provider}.create.updated") + else + return I18n.t("chat_integration.provider.#{provider}.create.error") + end + when "remove" + return error_text unless tokens.size == 1 + + rule_number = tokens[0].to_i + return error_text unless rule_number.to_s == tokens[0] # Check we were given a number + + if DiscourseChat::Helper.delete_by_index(channel, rule_number) + return I18n.t("chat_integration.provider.#{provider}.delete.success") + else + return I18n.t("chat_integration.provider.#{provider}.delete.error") + end + when "status" + return DiscourseChat::Helper.status_for_channel(channel) + when "help" + return I18n.t("chat_integration.provider.#{provider}.help") + else + return error_text + end + end + # Produce a string with a list of all rules associated with a channel def self.status_for_channel(channel) rules = channel.rules.order_by_precedence diff --git a/app/models/channel.rb b/app/models/channel.rb index 2337ade..ca97194 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -50,6 +50,6 @@ class DiscourseChat::Channel < DiscourseChat::PluginModel scope :with_provider, ->(provider) { where("value::json->>'provider'=?", provider)} - scope :with_data_value, ->(key, value) { where("(value::json->>'data')::json->>?=?", key, value)} + scope :with_data_value, ->(key, value) { where("(value::json->>'data')::json->>?=?", key.to_s, value.to_s)} end \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 63ac6ce..c6158ab 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -63,4 +63,13 @@ en: ####################################### telegram: title: "Telegram" - channel_instructions: "Enter a chat_id. You will need to add the bot to the chat first." \ No newline at end of file + param: + name: + title: "Name" + help: "A name to describe the channel. It is not used for the connection for telegram." + chat_id: + title: Chat ID + help: A number given to you by the bot, or a broadcast channel identifier in the form @channelname + errors: + channel_not_found: "The specified channel does not exist on Telegram" + forbidden: "The bot does not have permission to post to this channel" \ No newline at end of file diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 26b32d0..acf7fdf 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -10,7 +10,7 @@ en: ####################################### chat_integration_slack_enabled: 'Enable the slack chat-integration provider' chat_integration_slack_outbound_webhook_url: 'URL for outbound slack requests' - chat_integration_slack_excerpt_length: 'Post excerpt length' + chat_integration_slack_excerpt_length: 'Slack post excerpt length' chat_integration_slack_icon_url: 'Icon to post to slack with (defaults to forum logo)' chat_integration_slack_access_token: 'Token if you are using the Web API instead of webhooks' @@ -20,7 +20,10 @@ en: ####################################### ######### TELEGRAM SETTINGS ########### ####################################### - chat_integration_telegram_enabled: "Enable the telegram chat-integration provider" + chat_integration_telegram_enabled: "Enable the Telegram chat-integration provider" + chat_integration_telegram_access_token: "Your bot's access token from the Telegram botfather" + chat_integration_telegram_excerpt_length: "Telegram post excerpt length" + chat_integration_telegram_enable_slash_commands: "Allow telegram subscriptions to be managed using 'slash commands'" chat_integration: @@ -47,7 +50,6 @@ en: not_found: tag: "The *%{name}* tag cannot be found." category: "The *%{name}* category cannot be found. Available categories: *%{list}*" - help: | *New rule:* `/discourse [watch|follow|mute] [category] [tag:name]` (you must specify a rule type and at least one category or tag) @@ -60,4 +62,44 @@ en: *List rules:* `/discourse status` - *Help:* `/discourse help` \ No newline at end of file + *Help:* `/discourse help` + + telegram: + unknown_chat: "This chat isn't setup on %{site_title}. Ask an administrator to add a channel with 'Chat ID' %{chat_id}." + known_chat: "This chat is setup on %{site_title}. Configure it in the admin panel. (Chat ID: %{chat_id})" + message: |- + %{user} posted in %{title} + +
%{post_excerpt}
+ + status: + header: | + Rules for this channel + (if multiple rules match a post, the topmost rule is executed) + no_rules: "There are no rules set up for this channel. Run /help for instructions." + rule_string: "%{index}) %{filter} posts in %{category}" + rule_string_tags_suffix: " with tags: %{tags}" + parse_error: "Sorry, I didn't understand that. Run /help for instructions." + create: + created: "Rule created successfully" + updated: "Rule updated successfully" + error: "Sorry, an error occured while creating that rule." + delete: + success: "Rule deleted successfully" + error: "Sorry, an error occured while deleting that rule. Run /status for a list of rules." + not_found: + tag: "The %{name} tag cannot be found." + category: "The %{name} category cannot be found. Available categories: %{list}" + help: | + New rule: /[watch|follow|mute] [category] [tag:name] + (you must specify a rule type and at least one category or tag) + - watch – notify this channel for new topics and new replies + - follow – notify this channel for new topics + - mute – block notifications to this channel + + Remove rule: /remove [rule number] + ([rule number] can be found by running /status) + + List rules: /status + + Help: /help \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index 61caed7..07b2cbd 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -30,3 +30,11 @@ plugins: ####################################### chat_integration_telegram_enabled: default: false + chat_integration_telegram_access_token: + default: '' + chat_integration_telegram_excerpt_length: + default: 400 + chat_integration_telegram_enable_slash_commands: + default: true + chat_integration_telegram_secret: + hidden: true \ No newline at end of file diff --git a/lib/discourse_chat/provider/slack/slack_command_controller.rb b/lib/discourse_chat/provider/slack/slack_command_controller.rb index 7eb8ca3..7410250 100644 --- a/lib/discourse_chat/provider/slack/slack_command_controller.rb +++ b/lib/discourse_chat/provider/slack/slack_command_controller.rb @@ -17,7 +17,6 @@ module DiscourseChat::Provider::SlackProvider end def process_command(params) - guardian = DiscourseChat::Manager.guardian tokens = params[:text].split(" ") @@ -39,70 +38,7 @@ module DiscourseChat::Provider::SlackProvider # Create channel if doesn't exist channel ||= DiscourseChat::Channel.create!(provider:provider, data:{identifier: channel_id}) - cmd = tokens.shift if tokens.size >= 1 - - error_text = I18n.t("chat_integration.provider.slack.parse_error") - - case cmd - when "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 - - 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.slack.not_found.category", name: category_name, list:cat_list) - end - else - category = nil # All categories - end - - tags = [] - # 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:/, '') - else - return error_text # Abort and send help text - end - - tag = Tag.find_by(name: tag_name) - unless tag # If tag doesn't exist, abort - return I18n.t("chat_integration.provider.slack.not_found.tag", name: tag_name) - end - tags.push(tag.name) - end - - category_id = category.nil? ? nil : category.id - case DiscourseChat::Helper.smart_create_rule(channel:channel, filter:cmd, category_id: category_id, tags:tags) - when :created - return I18n.t("chat_integration.provider.slack.create.created") - when :updated - return I18n.t("chat_integration.provider.slack.create.updated") - else - return I18n.t("chat_integration.provider.slack.create.error") - end - when "remove" - return error_text unless tokens.size == 1 - - rule_number = tokens[0].to_i - return error_text unless rule_number.to_s == tokens[0] # Check we were given a number - - if DiscourseChat::Helper.delete_by_index(channel, rule_number) - return I18n.t("chat_integration.provider.slack.delete.success") - else - return I18n.t("chat_integration.provider.slack.delete.error") - end - when "status" - return DiscourseChat::Helper.status_for_channel(channel) - when "help" - return I18n.t("chat_integration.provider.slack.help") - else - return error_text - end + return ::DiscourseChat::Helper.process_command(channel, tokens) end diff --git a/lib/discourse_chat/provider/telegram/telegram_command_controller.rb b/lib/discourse_chat/provider/telegram/telegram_command_controller.rb index c63c22e..f3aba2f 100644 --- a/lib/discourse_chat/provider/telegram/telegram_command_controller.rb +++ b/lib/discourse_chat/provider/telegram/telegram_command_controller.rb @@ -2,9 +2,79 @@ module DiscourseChat::Provider::TelegramProvider class TelegramCommandController < DiscourseChat::Provider::HookController requires_provider ::DiscourseChat::Provider::TelegramProvider::PROVIDER_NAME - def say_hello + before_filter :telegram_token_valid?, only: :command - render json: {hello: "from telegram"} + skip_before_filter :check_xhr, + :preload_json, + :verify_authenticity_token, + :redirect_to_login_if_required, + 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'] + + message_text = process_command(params['message']) + + message = { + chat_id: chat_id, + text: message_text, + parse_mode: "html", + disable_web_page_preview: true, + } + + DiscourseChat::Provider::TelegramProvider.sendMessage(message) + + end + + # Always give telegram a success message, otherwise we'll stop receiving webhooks + data = { + success: true + } + render json: data + end + + def process_command(message) + chat_id = params['message']['chat']['id'] + + provider = DiscourseChat::Provider::TelegramProvider::PROVIDER_NAME + + channel = DiscourseChat::Channel.with_provider(provider).with_data_value('chat_id',chat_id).first + + if channel.nil? + return I18n.t( + "chat_integration.provider.telegram.unknown_chat", + site_title: CGI::escapeHTML(SiteSetting.title), + chat_id: chat_id, + ) + end + + # If slash commands disabled, send a generic message + if !SiteSetting.chat_integration_telegram_enable_slash_commands + return I18n.t( + "chat_integration.provider.telegram.known_chat", + site_title: CGI::escapeHTML(SiteSetting.title), + chat_id: chat_id, + ) + end + + tokens = message['text'].split(" ") + + tokens[0][0] = '' # Remove the slash from the first token + + return ::DiscourseChat::Helper.process_command(channel, tokens) + end + + def telegram_token_valid? + params.require(:token) + + if SiteSetting.chat_integration_telegram_secret.blank? || + SiteSetting.chat_integration_telegram_secret != params[:token] + + raise Discourse::InvalidAccess.new + end end end @@ -14,6 +84,6 @@ module DiscourseChat::Provider::TelegramProvider end TelegramEngine.routes.draw do - get "command" => "telegram_command#say_hello" + post "command/:token" => "telegram_command#command" end end \ No newline at end of file diff --git a/lib/discourse_chat/provider/telegram/telegram_initializer.rb b/lib/discourse_chat/provider/telegram/telegram_initializer.rb new file mode 100644 index 0000000..cff63b4 --- /dev/null +++ b/lib/discourse_chat/provider/telegram/telegram_initializer.rb @@ -0,0 +1,13 @@ +DiscourseEvent.on(:site_setting_saved) do |sitesetting| + isEnabledSetting = sitesetting.name == 'chat_integration_telegram_enabled' + isAccessToken = sitesetting.name == 'chat_integration_telegram_access_token' + + if (isEnabledSetting or isAccessToken) + enabled = isEnabledSetting ? sitesetting.value == 't' : SiteSetting.chat_integration_telegram_enabled + if enabled + Scheduler::Defer.later("Setup Telegram Webhook") do + DiscourseChat::Provider::TelegramProvider.setup_webhook() + end + end + end +end \ No newline at end of file diff --git a/lib/discourse_chat/provider/telegram/telegram_provider.rb b/lib/discourse_chat/provider/telegram/telegram_provider.rb index a681603..2bf505a 100644 --- a/lib/discourse_chat/provider/telegram/telegram_provider.rb +++ b/lib/discourse_chat/provider/telegram/telegram_provider.rb @@ -3,10 +3,107 @@ module DiscourseChat module TelegramProvider PROVIDER_NAME = "telegram".freeze PROVIDER_ENABLED_SETTING = :chat_integration_telegram_enabled - CHANNEL_PARAMETERS = [] + CHANNEL_PARAMETERS = [ + {key: "name", regex: '^\S+'}, + {key: "chat_id", regex: '^(-?[0-9]+|@\S+)$'} + ] + + def self.setup_webhook + newSecret = SecureRandom.hex + SiteSetting.chat_integration_telegram_secret = newSecret + + message = { + url: Discourse.base_url+'/chat-integration/telegram/command/'+newSecret, + } + + response = self.do_api_request('setWebhook', message) + + if not 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) + end + + end + + def self.sendMessage(message) + return self.do_api_request('sendMessage', message) + end + + def self.do_api_request(methodName, message) + http = Net::HTTP.new("api.telegram.org", 443) + http.use_ssl = true + + access_token = SiteSetting.chat_integration_telegram_access_token + + uri = URI("https://api.telegram.org/bot#{access_token}/#{methodName}") + + req = Net::HTTP::Post.new(uri, 'Content-Type' =>'application/json') + req.body = message.to_json + response = http.request(req) + + responseData = JSON.parse(response.body) + + return responseData + end + + def self.message_text(post) + display_name = "@#{post.user.username}" + full_name = post.user.name || "" + + if !(full_name.strip.empty?) && (full_name.strip.gsub(' ', '_').casecmp(post.user.username) != 0) && (full_name.strip.gsub(' ', '').casecmp(post.user.username) != 0) + display_name = "#{full_name} @#{post.user.username}" + end + + topic = post.topic + + category = '' + if topic.category + category = (topic.category.parent_category) ? "[#{topic.category.parent_category.name}/#{topic.category.name}]": "[#{topic.category.name}]" + end + + tags = '' + if topic.tags.present? + tags = topic.tags.map(&:name).join(', ') + end + + return I18n.t( + "chat_integration.provider.telegram.message", + user: display_name, + post_url: "https://meta.discourse.org", + 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) + chat_id = channel.data['chat_id'] + + message = { + chat_id: chat_id, + text: message_text(post), + parse_mode: "html", + disable_web_page_preview: true, + } + + response = sendMessage(message) + + if not response['ok'] == true + error_key = nil + if response['description'].include? 'chat not found' + error_key = 'chat_integration.provider.telegram.channel_not_found' + elsif response['description'].include? 'Forbidden' + error_key = 'chat_integration.provider.telegram.forbidden' + end + raise ::DiscourseChat::ProviderError.new info: {error_key: error_key, message: message, response_body:response} + end + + end end end end -require_relative "telegram_command_controller.rb" \ No newline at end of file +require_relative "telegram_command_controller.rb" +require_relative "telegram_initializer.rb" \ No newline at end of file diff --git a/spec/helpers/helper_spec.rb b/spec/helpers/helper_spec.rb index d890b06..86cdb6e 100644 --- a/spec/helpers/helper_spec.rb +++ b/spec/helpers/helper_spec.rb @@ -14,6 +14,129 @@ RSpec.describe DiscourseChat::Manager do let(:tag2){Fabricate(:tag)} let(:tag3){Fabricate(:tag)} + 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 = DiscourseChat::Helper.process_command(chan1, ['watch',category.slug]) + + expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created")) + + rule = DiscourseChat::Rule.all.first + expect(rule.channel).to eq(chan1) + expect(rule.filter).to eq('watch') + expect(rule.category_id).to eq(category.id) + expect(rule.tags).to eq(nil) + end + + it 'should work with all three filter types' do + response = DiscourseChat::Helper.process_command(chan1, ['watch',category.slug]) + + rule = DiscourseChat::Rule.all.first + expect(rule.filter).to eq('watch') + + response = DiscourseChat::Helper.process_command(chan1, ['follow',category.slug]) + + rule = DiscourseChat::Rule.all.first + expect(rule.filter).to eq('follow') + + response = DiscourseChat::Helper.process_command(chan1, ['mute',category.slug]) + + rule = DiscourseChat::Rule.all.first + expect(rule.filter).to eq('mute') + end + + it 'errors on incorrect categories' do + response = DiscourseChat::Helper.process_command(chan1, ['watch','blah']) + + expect(response).to eq(I18n.t("chat_integration.provider.dummy.not_found.category", name:'blah', list:'uncategorized')) + end + + context 'with tags enabled' do + before do + SiteSetting.tagging_enabled = true + end + + it 'should add a new tag rule correctly' do + response = DiscourseChat::Helper.process_command(chan1, ['watch',"tag:#{tag1.name}"]) + + expect(response).to eq(I18n.t("chat_integration.provider.dummy.create.created")) + + rule = DiscourseChat::Rule.all.first + expect(rule.channel).to eq(chan1) + expect(rule.filter).to eq('watch') + expect(rule.category_id).to eq(nil) + expect(rule.tags).to eq([tag1.name]) + end + + it 'should work with a category and multiple tags' do + + response = DiscourseChat::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 = DiscourseChat::Rule.all.first + expect(rule.channel).to eq(chan1) + expect(rule.filter).to eq('watch') + expect(rule.category_id).to eq(category.id) + expect(rule.tags).to contain_exactly(tag1.name, tag2.name) + end + + it 'errors on incorrect tags' do + response = DiscourseChat::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 = DiscourseChat::Rule.create(channel: chan1, + filter: 'watch', + category_id: category.id, + tags: [tag1.name, tag2.name] + ) + + expect(DiscourseChat::Rule.all.size).to eq(1) + + response = DiscourseChat::Helper.process_command(chan1, ['remove','1']) + + expect(response).to eq(I18n.t("chat_integration.provider.dummy.delete.success")) + + expect(DiscourseChat::Rule.all.size).to eq(0) + end + + it 'errors correctly' do + response = DiscourseChat::Helper.process_command(chan1, ['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 + response = DiscourseChat::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 = DiscourseChat::Helper.process_command(chan1, ['status']) + expect(response).to eq(DiscourseChat::Helper.status_for_channel(chan1)) + end + end + + describe 'unknown command' do + it 'should return the right response' do + response = DiscourseChat::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 diff --git a/spec/lib/discourse_chat/provider/slack/slack_command_controller_spec.rb b/spec/lib/discourse_chat/provider/slack/slack_command_controller_spec.rb index bc4e9f7..33ed12d 100644 --- a/spec/lib/discourse_chat/provider/slack/slack_command_controller_spec.rb +++ b/spec/lib/discourse_chat/provider/slack/slack_command_controller_spec.rb @@ -62,14 +62,15 @@ describe 'Slack Command Controller', type: :request 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 describe 'add new rule' do - # Not testing how filters are merged here, that's done upstream in helper_spec - # We just want to make sure the slash commands are being interpretted correctly - + it 'should add a new rule correctly' do post "/chat-integration/slack/command.json", text: "watch #{category.slug}", @@ -87,100 +88,6 @@ describe 'Slack Command Controller', type: :request do expect(rule.tags).to eq(nil) end - it 'should work with all three filter types' do - post "/chat-integration/slack/command.json", - text: "watch #{category.slug}", - channel_name: 'welcome', - token: token - - rule = DiscourseChat::Rule.all.first - expect(rule.filter).to eq('watch') - - post "/chat-integration/slack/command.json", - text: "follow #{category.slug}", - channel_name: 'welcome', - token: token - - rule = DiscourseChat::Rule.all.first - expect(rule.filter).to eq('follow') - - post "/chat-integration/slack/command.json", - text: "mute #{category.slug}", - channel_name: 'welcome', - token: token - - rule = DiscourseChat::Rule.all.first - expect(rule.filter).to eq('mute') - end - - it 'errors on incorrect categories' do - post "/chat-integration/slack/command.json", - text: "watch blah", - channel_name: 'welcome', - token: token - - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.not_found.category", name:'blah', list:'uncategorized')) - end - - context 'with tags enabled' do - before do - SiteSetting.tagging_enabled = true - end - - it 'should add a new tag rule correctly' do - post "/chat-integration/slack/command.json", - text: "watch tag:#{tag.name}", - channel_name: 'welcome', - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.create.created")) - - rule = DiscourseChat::Rule.all.first - expect(rule.channel).to eq(chan1) - expect(rule.filter).to eq('watch') - expect(rule.category_id).to eq(nil) - expect(rule.tags).to eq([tag.name]) - end - - it 'should work with a category and multiple tags' do - post "/chat-integration/slack/command.json", - text: "watch #{category.slug} tag:#{tag.name} tag:#{tag2.name}", - channel_name: 'welcome', - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.create.created")) - - rule = DiscourseChat::Rule.all.first - expect(rule.channel).to eq(chan1) - expect(rule.filter).to eq('watch') - expect(rule.category_id).to eq(category.id) - expect(rule.tags).to contain_exactly(tag.name, tag2.name) - end - - it 'errors on incorrect tags' do - post "/chat-integration/slack/command.json", - text: "watch tag:blah", - channel_name: 'welcome', - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.not_found.tag", name:"blah")) - end - end - context 'from an unknown channel' do it 'creates the channel' do post "/chat-integration/slack/command.json", @@ -202,92 +109,6 @@ describe 'Slack Command Controller', type: :request do end end end - - describe 'remove rule' do - it 'removes the rule' do - rule1 = DiscourseChat::Rule.create(channel: chan1, - filter: 'watch', - category_id: category.id, - tags: [tag.name, tag2.name] - ) - - expect(DiscourseChat::Rule.all.size).to eq(1) - post "/chat-integration/slack/command.json", - text: "remove 1", - channel_name: 'welcome', - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.delete.success")) - - expect(DiscourseChat::Rule.all.size).to eq(0) - end - - it 'errors correctly' do - post "/chat-integration/slack/command.json", - text: "remove 1", - channel_name: 'welcome', - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.delete.error")) - end - end - - describe 'help command' do - it 'should return the right response' do - post '/chat-integration/slack/command.json', text: "help", channel_name: "welcome", token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.help")) - end - end - - describe 'status command' do - # No need to test this with different combinations of rules - # That's done upstream in helper_spec - - it 'should return the right response' do - post '/chat-integration/slack/command.json', - text: "status", - channel_name: "welcome", - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(DiscourseChat::Helper.status_for_channel(chan1)) - end - end - - describe 'unknown command' do - # No need to test this with different combinations of rules - # That's done upstream in helper_spec - - it 'should return the right response' do - post '/chat-integration/slack/command.json', - text: "somerandomtext", - channel_name: "welcome", - token: token - - expect(response).to be_success - - json = JSON.parse(response.body) - - expect(json["text"]).to eq(I18n.t("chat_integration.provider.slack.parse_error")) - end - end - end end end diff --git a/spec/lib/discourse_chat/provider/telegram/telegram_command_controller_spec.rb b/spec/lib/discourse_chat/provider/telegram/telegram_command_controller_spec.rb new file mode 100644 index 0000000..d995a26 --- /dev/null +++ b/spec/lib/discourse_chat/provider/telegram/telegram_command_controller_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +describe 'Telegram Command Controller', type: :request do + let(:category) { Fabricate(:category) } + let!(:chan1){DiscourseChat::Channel.create!(provider:'telegram', data:{name: 'Amazing Channel', chat_id:'123'})} + + 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 + 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' + expect(response.status).to eq(404) + end + end + + describe 'slash commands endpoint' do + before do + SiteSetting.chat_integration_enabled = true + SiteSetting.chat_integration_telegram_secret = "shhh" + SiteSetting.chat_integration_telegram_access_token = "TOKEN" + SiteSetting.chat_integration_telegram_enabled = true + end + + let!(:stub){stub_request(:post, 'https://api.telegram.org/botTOKEN/sendMessage').to_return(body: "{\"ok\":true}")} + + 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', 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', 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 + SiteSetting.chat_integration_telegram_access_token = "" + post '/chat-integration/telegram/command/blah.json', message: {chat: {id:123}, text: '/help' } + + expect(response.status).to eq(403) + end + end + + describe 'when token is valid' do + let(:token) { "TOKEN" } + + before do + SiteSetting.chat_integration_telegram_enable_slash_commands = true + end + + describe 'add new rule' do + + it 'should add a new rule correctly' do + post '/chat-integration/telegram/command/shhh.json', message: {chat: {id:123}, text: "/watch #{category.slug}" } + + expect(response.status).to eq(200) + expect(stub).to have_been_requested.once + + rule = DiscourseChat::Rule.all.first + expect(rule.channel).to eq(chan1) + expect(rule.filter).to eq('watch') + expect(rule.category_id).to eq(category.id) + expect(rule.tags).to eq(nil) + end + + context 'from an unknown channel' do + it 'does nothing' do + post '/chat-integration/telegram/command/shhh.json', message: {chat: {id:456}, text: "/watch #{category.slug}" } + expect(DiscourseChat::Rule.all.size).to eq(0) + expect(DiscourseChat::Channel.all.size).to eq(1) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/discourse_chat/provider/telegram/telegram_provider_spec.rb b/spec/lib/discourse_chat/provider/telegram/telegram_provider_spec.rb new file mode 100644 index 0000000..38f4966 --- /dev/null +++ b/spec/lib/discourse_chat/provider/telegram/telegram_provider_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe DiscourseChat::Provider::TelegramProvider do + let(:post) { Fabricate(:post) } + + describe '.trigger_notifications' do + before do + SiteSetting.chat_integration_telegram_access_token = "TOKEN" + SiteSetting.chat_integration_telegram_secret = 'shhh' + SiteSetting.chat_integration_telegram_enabled = true + end + + let(:chan1){DiscourseChat::Channel.create!(provider:'telegram', data:{name: "Awesome Channel", chat_id: '123'})} + + 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) + 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\"}") + expect(stub1).to have_been_requested.times(0) + expect{described_class.trigger_notification(post, chan1)}.to raise_exception(::DiscourseChat::ProviderError) + expect(stub1).to have_been_requested.once + end + + end + +end