diff --git a/.travis.yml b/.travis.yml index 6fecb4d..d787f9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ services: before_install: - plugin_name=${PWD##*/} && echo $plugin_name +install: true # Prevent travis doing bundle install + script: - > docker run diff --git a/lib/discourse_chat/provider/slack/slack_command_controller.rb b/lib/discourse_chat/provider/slack/slack_command_controller.rb index f36defd..a504359 100644 --- a/lib/discourse_chat/provider/slack/slack_command_controller.rb +++ b/lib/discourse_chat/provider/slack/slack_command_controller.rb @@ -26,8 +26,6 @@ module DiscourseChat::Provider::SlackProvider case params[:channel_name] when 'directmessage' "@#{params[:user_name]}" - when 'privategroup' - params[:channel_id] else "##{params[:channel_name]}" end @@ -58,9 +56,16 @@ module DiscourseChat::Provider::SlackProvider Scheduler::Defer.later "Processing slack transcript request" do requested_messages = nil 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*$/ slack_url_regex = /^https:\/\/\S+\.slack\.com\/archives\/\S+\/p([0-9]{16})\/?$/ - if tokens.size > 1 && match = slack_url_regex.match(tokens[1]) + + if tokens.size > 2 && tokens[1] == "thread" && match = slack_url_regex.match(tokens[2]) + 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, '.') elsif tokens.size > 1 begin @@ -72,7 +77,7 @@ module DiscourseChat::Provider::SlackProvider error_message = { text: I18n.t("chat_integration.provider.slack.transcript.error") } - return error_message unless transcript = SlackTranscript.new(channel_name: channel_name, channel_id: slack_channel_id) + return error_message unless transcript = SlackTranscript.new(channel_name: channel_name, channel_id: slack_channel_id, requested_thread_ts: requested_thread_ts) return error_message unless transcript.load_user_data return error_message unless transcript.load_chat_history diff --git a/lib/discourse_chat/provider/slack/slack_transcript.rb b/lib/discourse_chat/provider/slack/slack_transcript.rb index f24c23d..fe6d1e4 100644 --- a/lib/discourse_chat/provider/slack/slack_transcript.rb +++ b/lib/discourse_chat/provider/slack/slack_transcript.rb @@ -2,9 +2,10 @@ module DiscourseChat::Provider::SlackProvider class SlackTranscript attr_reader :users, :channel_id, :messages - def initialize(channel_name:, channel_id:) + def initialize(channel_name:, channel_id:, requested_thread_ts: nil) @channel_name = channel_name @channel_id = channel_id + @requested_thread_ts = requested_thread_ts @first_message_index = 0 @last_message_index = -1 # We can use negative array indicies to select the last message - fancy! @@ -30,6 +31,7 @@ module DiscourseChat::Provider::SlackProvider # Apply a heuristic to decide which is the first message in the current conversation def guess_first_message(skip_messages: 5) # Can skip the last n messages + return true if @requested_thread_ts # Always start thread on first message possible_first_messages = @messages[0..-skip_messages] @@ -121,6 +123,8 @@ module DiscourseChat::Provider::SlackProvider secret = DiscourseChat::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 + { text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>", attachments: [ @@ -201,29 +205,35 @@ module DiscourseChat::Provider::SlackProvider http = Net::HTTP.new("slack.com", 443) http.use_ssl = true - req = Net::HTTP::Post.new(URI('https://slack.com/api/channels.history')) + endpoint = @requested_thread_ts ? "replies" : "history" + + req = Net::HTTP::Post.new(URI("https://slack.com/api/conversations.#{endpoint}")) data = { token: SiteSetting.chat_integration_slack_access_token, channel: @channel_id, - count: count + limit: count } + data[:ts] = @requested_thread_ts if @requested_thread_ts + req.set_form_data(data) response = http.request(req) return false unless response.kind_of? Net::HTTPSuccess json = JSON.parse(response.body) return false unless json['ok'] - raw_messages = json['messages'].reverse + raw_messages = json['messages'] + raw_messages = raw_messages.reverse unless @requested_thread_ts # Build some message objects @messages = [] raw_messages.each_with_index do |message, index| # Only load messages next unless message["type"] == "message" - # Don't load responses to threads (if ts==thread_ts then it's the thread parent) - next if message["thread_ts"] && message["thread_ts"] != message["ts"] + + # 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"] this_message = SlackMessage.new(message, self) @messages << this_message 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 1c2def9..9b74604 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 @@ -196,7 +196,7 @@ describe 'Slack Command Controller', type: :request do 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","name":"david","profile":{"icon_24":"https://example.com/avatar"}}]}') - stub_request(:post, "https://slack.com/api/channels.history").to_return(body: { ok: true, messages: messages_fixture }.to_json) + 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 @@ -231,6 +231,46 @@ describe 'Slack Command Controller', type: :request do 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) + + 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 + } + + 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) + + 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 + } + + 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/) diff --git a/spec/lib/discourse_chat/provider/slack/slack_transcript_spec.rb b/spec/lib/discourse_chat/provider/slack/slack_transcript_spec.rb index 0cf262f..a764c70 100644 --- a/spec/lib/discourse_chat/provider/slack/slack_transcript_spec.rb +++ b/spec/lib/discourse_chat/provider/slack/slack_transcript_spec.rb @@ -105,7 +105,7 @@ RSpec.describe DiscourseChat::Provider::SlackProvider::SlackTranscript do describe 'loading history' do it 'loads messages correctly' do - stub_request(:post, "https://slack.com/api/channels.history") + 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) @@ -113,23 +113,43 @@ RSpec.describe DiscourseChat::Provider::SlackProvider::SlackTranscript do end it 'handles failed connection' do - stub_request(:post, "https://slack.com/api/channels.history") + 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/channels.history") + 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") } + + before do + 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 }.to_json) + thread_transcript.load_chat_history + end + + it 'includes messages in a thread' do + expect(thread_transcript.messages.length).to eq(7) + end + + it 'loads in chronological order' do # replies API presents messages in actual chronological order + expect(thread_transcript.messages.first.ts).to eq('1501801665.062694') + end + + end + context 'with loaded messages' do before do - stub_request(:post, "https://slack.com/api/channels.history") + 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