diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3f3604d..3a2e426 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -202,3 +202,17 @@ en: errors: not_found: "The path you attempted to post your message to was not found. Check the Bot ID in Site Settings." instance_names_issue: "instance names incorrectly formatted or not provided" + ############################################ + ######### MICROSOFT TEAMS STRINGS ########## + ############################################ + teams: + title: "Microsoft Teams" + param: + name: + title: "Name" + help: "A Teams channel name e.g. discourse" + webhook_url: + title: "Webhook URL" + help: "The URL provided when you create a new incomming webhook" + errors: + invalid_channel: "That channel does not exist on Microsoft Teams" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bcc9098..373a8b5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -86,6 +86,12 @@ en: chat_integration_groupme_bot_ids: "*required* Bot IDs, seperated by ',' if there are multiple" chat_integration_groupme_instance_names: " *required* Name of the GroupMe chat, seperated by ',' if there are multiple (same order as Bot IDs)" + ########################################### + ######## MICROSOFT TEAMS SETTINGS ######### + ########################################### + chat_integration_teams_enabled: "Enable the Microsoft Teams chat integration provider" + chat_integration_teams_excerpt_length: "Microsoft Team post excerpt length" + chat_integration: all_categories: "(all categories)" diff --git a/config/settings.yml b/config/settings.yml index 41f4c0a..98ccac5 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -129,4 +129,11 @@ chat_integration: chat_integration_groupme_bot_ids: default: '' chat_integration_groupme_instance_names: - default: '' \ No newline at end of file + default: '' +########################################### +######## MICROSOFT TEAMS SETTINGS ######### +########################################### + chat_integration_teams_enabled: + default: false + chat_integration_teams_excerpt_length: + default: 400 \ No newline at end of file diff --git a/lib/discourse_chat/provider.rb b/lib/discourse_chat/provider.rb index 9a38050..87c4a21 100644 --- a/lib/discourse_chat/provider.rb +++ b/lib/discourse_chat/provider.rb @@ -101,3 +101,4 @@ require_relative "provider/rocketchat/rocketchat_provider" require_relative "provider/gitter/gitter_provider" require_relative "provider/flowdock/flowdock_provider" require_relative "provider/groupme/groupme_provider" +require_relative "provider/teams/teams_provider" diff --git a/lib/discourse_chat/provider/teams/teams_provider.rb b/lib/discourse_chat/provider/teams/teams_provider.rb new file mode 100644 index 0000000..26525ce --- /dev/null +++ b/lib/discourse_chat/provider/teams/teams_provider.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module DiscourseChat::Provider::TeamsProvider + PROVIDER_NAME = "teams".freeze + PROVIDER_ENABLED_SETTING = :chat_integration_teams_enabled + CHANNEL_PARAMETERS = [ + { key: "name", regex: '^\S+$', unique: true }, + { key: "webhook_url", regex: '^https:\/\/outlook\.office\.com\/webhook\/[A-Za-z0-9\-@\/]+\S+$', unique: true, hidden: true } + ] + + def self.trigger_notification(post, channel, rule) + message = get_message(post) + uri = URI(channel.data['webhook_url']) + + http = Net::HTTP.new(uri.host, uri.port) + 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?('Invalid webhook URL') + error_key = 'chat_integration.provider.teams.errors.invalid_channel' + else + error_key = nil + end + raise ::DiscourseChat::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) + display_name = "@#{post.user.username}" + full_name = post.user.name || "" + + topic = post.topic + + category = '' + if topic.category&.uncategorized? + 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}]" + 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 + }], + } + + message + end + +end diff --git a/spec/lib/discourse_chat/provider/teams/teams_provider_spec.rb b/spec/lib/discourse_chat/provider/teams/teams_provider_spec.rb new file mode 100644 index 0000000..3501540 --- /dev/null +++ b/spec/lib/discourse_chat/provider/teams/teams_provider_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe DiscourseChat::Provider::TeamsProvider do + let(:post) { Fabricate(:post) } + + describe '.trigger_notifications' do + before do + SiteSetting.chat_integration_teams_enabled = true + end + + let(:chan1) { DiscourseChat::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") + 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: "{}") + expect(stub1).to have_been_requested.times(0) + expect { described_class.trigger_notification(post, chan1, nil) }.to raise_exception(::DiscourseChat::ProviderError) + expect(stub1).to have_been_requested.once + end + + end + +end