Merge pull request #20 from davidtaylorhq/private_slash_commands

Slash command and transcript improvements
This commit is contained in:
Sam 2018-05-18 10:47:37 +10:00 committed by GitHub
commit d481205999
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 15 deletions

View File

@ -6,6 +6,8 @@ services:
before_install: before_install:
- plugin_name=${PWD##*/} && echo $plugin_name - plugin_name=${PWD##*/} && echo $plugin_name
install: true # Prevent travis doing bundle install
script: script:
- > - >
docker run docker run

View File

@ -26,8 +26,6 @@ module DiscourseChat::Provider::SlackProvider
case params[:channel_name] case params[:channel_name]
when 'directmessage' when 'directmessage'
"@#{params[:user_name]}" "@#{params[:user_name]}"
when 'privategroup'
params[:channel_id]
else else
"##{params[:channel_name]}" "##{params[:channel_name]}"
end end
@ -58,9 +56,16 @@ module DiscourseChat::Provider::SlackProvider
Scheduler::Defer.later "Processing slack transcript request" do Scheduler::Defer.later "Processing slack transcript request" do
requested_messages = nil requested_messages = nil
first_message_ts = 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})\/?$/ 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, '.') first_message_ts = match.captures[0].insert(10, '.')
elsif tokens.size > 1 elsif tokens.size > 1
begin begin
@ -72,7 +77,7 @@ module DiscourseChat::Provider::SlackProvider
error_message = { text: I18n.t("chat_integration.provider.slack.transcript.error") } 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_user_data
return error_message unless transcript.load_chat_history return error_message unless transcript.load_chat_history

View File

@ -2,9 +2,10 @@ module DiscourseChat::Provider::SlackProvider
class SlackTranscript class SlackTranscript
attr_reader :users, :channel_id, :messages 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_name = channel_name
@channel_id = channel_id @channel_id = channel_id
@requested_thread_ts = requested_thread_ts
@first_message_index = 0 @first_message_index = 0
@last_message_index = -1 # We can use negative array indicies to select the last message - fancy! @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 # 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 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] possible_first_messages = @messages[0..-skip_messages]
@ -121,6 +123,8 @@ module DiscourseChat::Provider::SlackProvider
secret = DiscourseChat::Helper.save_transcript(post_content) secret = DiscourseChat::Helper.save_transcript(post_content)
link = "#{Discourse.base_url}/chat-transcript/#{secret}" 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")}>", text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>",
attachments: [ attachments: [
@ -201,29 +205,35 @@ module DiscourseChat::Provider::SlackProvider
http = Net::HTTP.new("slack.com", 443) http = Net::HTTP.new("slack.com", 443)
http.use_ssl = true 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 = { data = {
token: SiteSetting.chat_integration_slack_access_token, token: SiteSetting.chat_integration_slack_access_token,
channel: @channel_id, channel: @channel_id,
count: count limit: count
} }
data[:ts] = @requested_thread_ts if @requested_thread_ts
req.set_form_data(data) req.set_form_data(data)
response = http.request(req) response = http.request(req)
return false unless response.kind_of? Net::HTTPSuccess return false unless response.kind_of? Net::HTTPSuccess
json = JSON.parse(response.body) json = JSON.parse(response.body)
return false unless json['ok'] 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 # Build some message objects
@messages = [] @messages = []
raw_messages.each_with_index do |message, index| raw_messages.each_with_index do |message, index|
# Only load messages # Only load messages
next unless message["type"] == "message" 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) this_message = SlackMessage.new(message, self)
@messages << this_message @messages << this_message

View File

@ -196,7 +196,7 @@ describe 'Slack Command Controller', type: :request do
context "with valid slack responses" do context "with valid slack responses" do
before 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/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 end
it 'generates the transcript UI properly' do 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 expect(command_stub).to have_been_requested
end 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 it 'can select by count' do
command_stub = stub_request(:post, "https://slack.com/commands/1234") command_stub = stub_request(:post, "https://slack.com/commands/1234")
.with(body: /1501801629\.052212/) .with(body: /1501801629\.052212/)

View File

@ -105,7 +105,7 @@ RSpec.describe DiscourseChat::Provider::SlackProvider::SlackTranscript do
describe 'loading history' do describe 'loading history' do
it 'loads messages correctly' 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')) .with(body: hash_including(token: "abcde", channel: 'G1234'))
.to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json) .to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json)
@ -113,23 +113,43 @@ RSpec.describe DiscourseChat::Provider::SlackProvider::SlackTranscript do
end end
it 'handles failed connection' do 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) .to_return(status: 500, body: {}.to_json)
expect(transcript.load_chat_history).to be_falsey expect(transcript.load_chat_history).to be_falsey
end end
it 'handles slack failure' do 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) .to_return(status: 200, body: { ok: false }.to_json)
expect(transcript.load_chat_history).to be_falsey expect(transcript.load_chat_history).to be_falsey
end end
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 context 'with loaded messages' do
before 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')) .with(body: hash_including(token: "abcde", channel: 'G1234'))
.to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json) .to_return(status: 200, body: { ok: true, messages: messages_fixture }.to_json)
transcript.load_chat_history transcript.load_chat_history