diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index a95eb65..0f5083c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -157,6 +157,10 @@ en: *Help:* `/discourse help` transcript: + modal_title: "Create Transcript" + modal_description: "All messages in the thread will be assembled into a single forum post. You will be given the opportunity to edit the transcript before posting." + transcript_ready: "Transcript ready" + continue_on_discourse: "Continue on Discourse" error: "Something went wrong when building the transcript, sorry!" post_to_discourse: "Click here to draft a post on Discourse with a transcript" api_required: "Sorry, this integration isn't setup to support posting transcripts." diff --git a/lib/discourse_chat/provider/slack/slack_command_controller.rb b/lib/discourse_chat/provider/slack/slack_command_controller.rb index 053e615..e0f8d88 100644 --- a/lib/discourse_chat/provider/slack/slack_command_controller.rb +++ b/lib/discourse_chat/provider/slack/slack_command_controller.rb @@ -22,8 +22,7 @@ module DiscourseChat::Provider::SlackProvider def interactive json = JSON.parse(params[:payload], symbolize_names: true) process_interactive(json) - - render json: { text: I18n.t("chat_integration.provider.slack.transcript.loading") } + head :ok end private @@ -68,8 +67,7 @@ module DiscourseChat::Provider::SlackProvider Scheduler::Defer.later "Processing slack transcript request" do response = build_post_request_response(channel, tokens, slack_channel_id, channel_name, response_url) - http = Net::HTTP.new("slack.com", 443) - http.use_ssl = true + http = DiscourseChat::Provider::SlackProvider.slack_api_http req = Net::HTTP::Post.new(URI(response_url), 'Content-Type' => 'application/json') req.body = response.to_json http.request(req) @@ -118,32 +116,114 @@ module DiscourseChat::Provider::SlackProvider end def process_interactive(json) - action_name = json[:actions][0][:name] - - 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 - - error_message = { text: I18n.t("chat_integration.provider.slack.transcript.error") } - Scheduler::Defer.later "Processing slack transcript update" do - break error_message unless transcript = SlackTranscript.new(channel_name: "##{json[:channel][:name]}", channel_id: json[:channel][:id]) - break error_message unless transcript.load_user_data - break error_message unless transcript.load_chat_history + http = DiscourseChat::Provider::SlackProvider.slack_api_http - break error_message unless transcript.set_first_message_by_ts(first_message) - break error_message unless transcript.set_last_message_by_ts(last_message) + if json[:type] == "block_actions" && json[:actions][0][:action_id] == "null_action" + # Do nothing + elsif json[:type] == "message_action" && json[:message][:thread_ts] + # Context menu used on a threaded message + transcript = SlackTranscript.new( + channel_name: "##{json[:channel][:name]}", + channel_id: json[:channel][:id], + requested_thread_ts: json[:message][:thread_ts] + ) - http = Net::HTTP.new("slack.com", 443) - http.use_ssl = true - req = Net::HTTP::Post.new(URI(json[:response_url]), 'Content-Type' => 'application/json') - req.body = transcript.build_slack_ui.to_json - response = http.request(req) + # Send a loading modal within 3 seconds: + req = Net::HTTP::Post.new( + "https://slack.com/api/views.open", + '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 + response = http.request(req) + view_id = JSON.parse(response.body).dig("view", "id") + + # Now load the transcript + error_view = generate_error_view("users") unless transcript.load_user_data + error_view = generate_error_view("history") unless transcript.load_chat_history + + # Then update the modal with the transcript link: + req = Net::HTTP::Post.new( + "https://slack.com/api/views.update", + '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 + 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.body = build_interactive_response(json).to_json + response = http.request(req) + end end end + def build_interactive_response(json) + requested_thread = first_message = last_message = nil + + if json[:type] == "message_action" # Slack "Shortcut" (for non-threaded messages) + first_message = json[:message][:ts] + else # Clicking buttons in our transcript UI message + action_name = json[:actions][0][:name] + + 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 + end + + error_key = "chat_integration.provider.slack.transcript.error" + + return { text: I18n.t(error_key) } unless transcript = SlackTranscript.new( + channel_name: "##{json[:channel][:name]}", + channel_id: json[:channel][:id], + requested_thread_ts: requested_thread + ) + 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) + end + + if last_message + return { text: I18n.t("#{error_key}_ts") } unless transcript.set_last_message_by_ts(last_message) + end + + transcript.build_slack_ui + end + + def generate_error_view(type = nil) + error_key = "chat_integration.provider.slack.transcript.error" + error_key += "_#{type}" if type + + { + type: "modal", + title: { + type: "plain_text", + text: I18n.t("chat_integration.provider.slack.transcript.modal_title") + }, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":warning: *#{I18n.t(error_key)}*" + } + } + ] + } + end + def slack_token_valid? params.require(:token) diff --git a/lib/discourse_chat/provider/slack/slack_provider.rb b/lib/discourse_chat/provider/slack/slack_provider.rb index 93bf9fd..8b451fd 100644 --- a/lib/discourse_chat/provider/slack/slack_provider.rb +++ b/lib/discourse_chat/provider/slack/slack_provider.rb @@ -91,8 +91,7 @@ module DiscourseChat::Provider::SlackProvider end def self.send_via_api(post, channel, message) - http = Net::HTTP.new("slack.com", 443) - http.use_ssl = true + http = slack_api_http response = nil uri = "" @@ -177,6 +176,13 @@ module DiscourseChat::Provider::SlackProvider end end + + def self.slack_api_http + http = Net::HTTP.new("slack.com", 443) + http.use_ssl = true + http.read_timeout = 5 # seconds + http + end end require_relative "slack_message_formatter" diff --git a/lib/discourse_chat/provider/slack/slack_transcript.rb b/lib/discourse_chat/provider/slack/slack_transcript.rb index 803a01f..beff4e9 100644 --- a/lib/discourse_chat/provider/slack/slack_transcript.rb +++ b/lib/discourse_chat/provider/slack/slack_transcript.rb @@ -125,6 +125,60 @@ module DiscourseChat::Provider::SlackProvider post_content end + def build_modal_ui + data = { + type: "modal", + title: { + type: "plain_text", + 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") + } + } + ] + } + + if @messages + post_content = build_transcript + secret = DiscourseChat::Helper.save_transcript(post_content) + 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")}*" + }, + "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" + } + } + else + data[:blocks] << { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":writing_hand: #{I18n.t("chat_integration.provider.slack.transcript.loading")}" + } + } + end + + data + end + def build_slack_ui post_content = build_transcript secret = DiscourseChat::Helper.save_transcript(post_content) @@ -196,8 +250,7 @@ module DiscourseChat::Provider::SlackProvider end def load_user_data - http = Net::HTTP.new("slack.com", 443) - http.use_ssl = true + http = ::DiscourseChat::Provider::SlackProvider.slack_api_http cursor = nil req = Net::HTTP::Post.new(URI('https://slack.com/api/users.list')) @@ -226,8 +279,7 @@ module DiscourseChat::Provider::SlackProvider end def load_chat_history(count: 500) - http = Net::HTTP.new("slack.com", 443) - http.use_ssl = true + http = DiscourseChat::Provider::SlackProvider.slack_api_http endpoint = @requested_thread_ts ? "replies" : "history" 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 003d3f0..b6e7247 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 @@ -304,6 +304,36 @@ describe 'Slack Command Controller', type: :request do 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) + + 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) + + post "/chat-integration/slack/interactive.json", params: { + payload: { + type: "message_action", + channel: { name: 'general', id: 'C6029G78F' }, + trigger_id: "TRIGGERID", + message: { thread_ts: "1501801629.052212" }, + token: token + }.to_json + } + + expect(response.status).to eq(200) + + 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