When creating a new Discourse post from slack with the `post` feature, record the slack `ts` thread ID for the resulting topic post using an HTML comment to pass the `ts` through. When notifying slack of new Discourse posts, record the slack `ts` thread ID in the post's topic if it has not yet been recorded. (Normally, this will be done for the topic post, except where notifications are being posted for old topics before this feature was created.) Add a new rule filter `thread` which posts threaded responses to slack if there is a `ts` recorded for the post topic. Modify the `trigger_notifications` interface to enable other integrations to implement similar functionality. Present the `thread` rule in the help text and admin UI only for the slack providers. https://meta.discourse.org/t/optionally-threading-posts-to-parent-topic-in-slack-integration/150759
338 lines
12 KiB
Ruby
338 lines
12 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
require 'rails_helper'
|
||
|
||
RSpec.describe DiscourseChat::Provider::SlackProvider::SlackTranscript do
|
||
|
||
let(:messages_fixture) {
|
||
[
|
||
{
|
||
"type": "message",
|
||
"user": "U6JSSESES",
|
||
"text": "Yeah, should make posting slack transcripts much easier",
|
||
"ts": "1501801665.062694"
|
||
},
|
||
{
|
||
"type": "message",
|
||
"user": "U5Z773QLZ",
|
||
"text": "Oooh a new discourse plugin <@U5Z773QLS> ???",
|
||
"ts": "1501801643.056375"
|
||
},
|
||
{
|
||
"type": "message",
|
||
"user": "U6E2W7R8C",
|
||
"text": "Which one?",
|
||
"ts": "1501801635.053761"
|
||
},
|
||
{
|
||
"type": "message",
|
||
"user": "U6JSSESES",
|
||
"text": "So, who's interested in the new <https://meta.discourse.org|discourse plugin>?",
|
||
"ts": "1501801629.052212"
|
||
},
|
||
{
|
||
"type": "message",
|
||
"user": "U820GH3LA",
|
||
"text": "I'm interested!!",
|
||
"ts": "1501801634.053761",
|
||
"thread_ts": "1501801629.052212"
|
||
},
|
||
{
|
||
"text": "Check this out!",
|
||
"username": "Test Community",
|
||
"bot_id": "B6C6JNUDN",
|
||
"attachments": [
|
||
{
|
||
"author_name": "@david",
|
||
"fallback": "Discourse can now be integrated with Mattermost! - @david",
|
||
"text": "Hey <http://localhost/groups/team|@team>, what do you think about this?",
|
||
"title": "Discourse can now be integrated with Mattermost! [Announcements] ",
|
||
"id": 1,
|
||
"title_link": "http://localhost:3000/t/discourse-can-now-be-integrated-with-mattermost/51/4",
|
||
"color": "283890",
|
||
"mrkdwn_in": [
|
||
"text"
|
||
]
|
||
}
|
||
],
|
||
"type": "message",
|
||
"subtype": "bot_message",
|
||
"ts": "1501615820.949638"
|
||
},
|
||
{
|
||
"type": "message",
|
||
"user": "U5Z773QLS",
|
||
"text": "Let’s try some *bold text* <@U5Z773QLZ> <@someotheruser>",
|
||
"ts": "1501093331.439776"
|
||
},
|
||
]
|
||
}
|
||
|
||
let(:users_fixture) {
|
||
[
|
||
{
|
||
id: "U6JSSESES",
|
||
name: "threader",
|
||
profile: {
|
||
image_24: "https://example.com/avatar",
|
||
display_name: "Threader",
|
||
real_name: "A. Threader"
|
||
}
|
||
},
|
||
{
|
||
id: "U820GH3LA",
|
||
name: "responder",
|
||
profile: {
|
||
image_24: "https://example.com/avatar",
|
||
display_name: "Responder",
|
||
real_name: "A. Responder"
|
||
}
|
||
},
|
||
{
|
||
id: "U5Z773QLS",
|
||
name: "awesomeguyemail",
|
||
profile: {
|
||
image_24: "https://example.com/avatar",
|
||
display_name: "awesomeguy",
|
||
real_name: "actually just a guy"
|
||
}
|
||
},
|
||
{
|
||
id: "U5Z773QLZ",
|
||
name: "otherguyemail",
|
||
profile: {
|
||
image_24: "https://example.com/avatar",
|
||
display_name: "",
|
||
real_name: "another guy"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
let(:transcript) { described_class.new(channel_name: "#general", channel_id: "G1234") }
|
||
before do
|
||
SiteSetting.chat_integration_slack_access_token = "abcde"
|
||
end
|
||
|
||
it "doesn't raise an error when there are no messages to guess" do
|
||
transcript.instance_variable_set(:@messages, [])
|
||
expect(transcript.guess_first_message(skip_messages: 1)).to eq(false)
|
||
end
|
||
|
||
describe 'loading users' do
|
||
it 'loads users correctly' do
|
||
stub_request(:post, "https://slack.com/api/users.list")
|
||
.with(body: { token: "abcde", "cursor": nil, "limit": "200" })
|
||
.to_return(status: 200, body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json)
|
||
|
||
expect(transcript.load_user_data).to be_truthy
|
||
end
|
||
|
||
it 'handles failed connection' do
|
||
stub_request(:post, "https://slack.com/api/users.list")
|
||
.to_return(status: 500, body: '')
|
||
|
||
expect(transcript.load_user_data).to be_falsey
|
||
end
|
||
|
||
it 'handles slack failure' do
|
||
stub_request(:post, "https://slack.com/api/users.list")
|
||
.to_return(status: 200, body: { ok: false }.to_json)
|
||
|
||
expect(transcript.load_user_data).to be_falsey
|
||
end
|
||
end
|
||
|
||
context 'with loaded users' do
|
||
before do
|
||
stub_request(:post, "https://slack.com/api/users.list")
|
||
.to_return(status: 200, body: { ok: true, members: users_fixture, response_metadata: { next_cursor: "" } }.to_json)
|
||
transcript.load_user_data
|
||
end
|
||
|
||
describe 'loading history' do
|
||
it 'loads messages correctly' do
|
||
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)
|
||
|
||
expect(transcript.load_chat_history).to be_truthy
|
||
end
|
||
|
||
it 'handles failed connection' do
|
||
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/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
|
||
thread_transcript.load_user_data
|
||
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[3..4] }.to_json)
|
||
thread_transcript.load_chat_history
|
||
end
|
||
|
||
it 'includes messages in a thread' do
|
||
expect(thread_transcript.messages.length).to eq(2)
|
||
end
|
||
|
||
it 'loads in chronological order' do # replies API presents messages in actual chronological order
|
||
expect(thread_transcript.messages.first.ts).to eq('1501801629.052212')
|
||
end
|
||
|
||
it 'includes slack thread identifiers in body' do
|
||
text = thread_transcript.build_transcript
|
||
expect(text).to include("<!--SLACK_CHANNEL_ID=G1234;SLACK_TS=1501801629.052212-->")
|
||
end
|
||
|
||
end
|
||
|
||
context 'with loaded messages' do
|
||
before do
|
||
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
|
||
end
|
||
|
||
it 'ignores messages in a thread' do
|
||
expect(transcript.messages.length).to eq(6)
|
||
end
|
||
|
||
it 'loads in chronological order' do # API presents in reverse chronological
|
||
expect(transcript.messages.first.ts).to eq('1501093331.439776')
|
||
end
|
||
|
||
it 'handles bold text' do
|
||
expect(transcript.messages.first.text).to start_with("Let’s try some **bold text** ")
|
||
end
|
||
|
||
it 'handles links' do
|
||
expect(transcript.messages[2].text).to eq("So, who's interested in the new [discourse plugin](https://meta.discourse.org)?")
|
||
end
|
||
|
||
it 'includes attachments' do
|
||
expect(transcript.messages[1].attachments.first).to eq("Discourse can now be integrated with Mattermost! - @david")
|
||
end
|
||
|
||
it 'can generate URL' do
|
||
expect(transcript.messages.first.url).to eq("https://slack.com/archives/G1234/p1501093331439776")
|
||
end
|
||
|
||
it 'includes attachments in raw text' do
|
||
transcript.set_first_message_by_ts('1501615820.949638')
|
||
expect(transcript.first_message.raw_text).to eq("Check this out!\n - Discourse can now be integrated with Mattermost! - @david\n")
|
||
end
|
||
|
||
it 'gives correct first and last messages' do
|
||
expect(transcript.first_message_number).to eq(0)
|
||
expect(transcript.last_message_number).to eq(transcript.messages.length - 1)
|
||
|
||
expect(transcript.first_message.ts).to eq('1501093331.439776')
|
||
expect(transcript.last_message.ts).to eq('1501801665.062694')
|
||
end
|
||
|
||
it 'can change first and last messages by index' do
|
||
expect(transcript.set_first_message_by_index(999)).to be_falsey
|
||
expect(transcript.set_first_message_by_index(1)).to be_truthy
|
||
|
||
expect(transcript.set_last_message_by_index(-2)).to be_truthy
|
||
|
||
expect(transcript.first_message.ts).to eq('1501615820.949638')
|
||
expect(transcript.last_message.ts).to eq('1501801643.056375')
|
||
end
|
||
|
||
it 'can change first and last messages by ts' do
|
||
expect(transcript.set_first_message_by_ts('blah')).to be_falsey
|
||
expect(transcript.set_first_message_by_ts('1501615820.949638')).to be_truthy
|
||
|
||
expect(transcript.set_last_message_by_ts('1501801629.052212')).to be_truthy
|
||
|
||
expect(transcript.first_message_number).to eq(1)
|
||
expect(transcript.last_message_number).to eq(2)
|
||
end
|
||
|
||
it 'can guess the first message' do
|
||
expect(transcript.guess_first_message(skip_messages: 1)).to eq(true)
|
||
expect(transcript.first_message.ts).to eq('1501801629.052212')
|
||
end
|
||
|
||
it 'handles usernames correctly' do
|
||
expect(transcript.first_message.username).to eq('awesomeguy') # Normal user
|
||
expect(transcript.messages[1].username).to eq('Test_Community') # Bot user
|
||
expect(transcript.messages[3].username).to eq(nil) # Unknown normal user
|
||
# Normal user, display_name not set (fall back to real_name)
|
||
expect(transcript.messages[4].username).to eq('another_guy')
|
||
end
|
||
|
||
it 'handles user mentions correctly' do
|
||
# User with display_name not set, unrecognized user
|
||
expect(transcript.first_message.text).to \
|
||
eq('Let’s try some **bold text** @another_guy @someotheruser')
|
||
# Normal user
|
||
expect(transcript.messages[4].text).to \
|
||
eq('Oooh a new discourse plugin @awesomeguy ???')
|
||
end
|
||
|
||
it 'handles avatars correctly' do
|
||
expect(transcript.first_message.avatar).to eq("https://example.com/avatar") # Normal user
|
||
expect(transcript.messages[1].avatar).to eq(nil) # Bot user
|
||
end
|
||
|
||
it 'creates a transcript correctly' do
|
||
transcript.set_last_message_by_index(1)
|
||
|
||
text = transcript.build_transcript
|
||
|
||
expected = <<~END
|
||
[quote]
|
||
[**View in #general on Slack**](https://slack.com/archives/G1234/p1501093331439776)
|
||
|
||
![awesomeguy] **@awesomeguy:** Let’s try some **bold text** @another_guy @someotheruser
|
||
|
||
**@Test_Community:** Check this out!
|
||
> Discourse can now be integrated with Mattermost! - @david
|
||
|
||
[/quote]
|
||
|
||
[awesomeguy]: https://example.com/avatar
|
||
END
|
||
|
||
expect(text).to eq(expected)
|
||
end
|
||
|
||
it 'creates the slack UI correctly' do
|
||
transcript.set_last_message_by_index(1)
|
||
ui = transcript.build_slack_ui
|
||
|
||
first_ui = ui[:attachments][0]
|
||
last_ui = ui[:attachments][1]
|
||
|
||
# The callback IDs are used to keep track of what the other option is
|
||
expect(first_ui[:callback_id]).to eq(transcript.last_message.ts)
|
||
expect(last_ui[:callback_id]).to eq(transcript.first_message.ts)
|
||
|
||
# The timestamps should match up to the actual messages
|
||
expect(first_ui[:ts]).to eq(transcript.first_message.ts)
|
||
expect(last_ui[:ts]).to eq(transcript.last_message.ts)
|
||
|
||
# Raw text should be used
|
||
expect(first_ui[:text]).to eq(transcript.first_message.raw_text)
|
||
end
|
||
end
|
||
end
|
||
end
|