diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c04bad2..c9cb581 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -105,10 +105,16 @@ en: Create a draft topic on discourse containing the last `n` posts in this channel *Help:* `/discourse help` - transcript_error: "Something went wrong when building the transcript, sorry!" - post_to_discourse: "Click here to post on Discourse" - api_required: "Sorry, this integration isn't setup to support posting transcripts." - view_on_slack: "View on Slack" + transcript: + 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." + view_on_slack: "View in %{name} on Slack" + first_message_pretext: "starting %{n} messages ago:" + last_message_pretext: "and ending %{n} messages ago:" + posted_in: "posted in %{name}" + change_first_message: "Change first message..." + change_last_message: "Change last message..." ####################################### ########## TELEGRAM STRINGS ########### diff --git a/lib/discourse_chat/provider/slack/slack_command_controller.rb b/lib/discourse_chat/provider/slack/slack_command_controller.rb index 479b063..59a2932 100644 --- a/lib/discourse_chat/provider/slack/slack_command_controller.rb +++ b/lib/discourse_chat/provider/slack/slack_command_controller.rb @@ -3,12 +3,13 @@ module DiscourseChat::Provider::SlackProvider requires_provider ::DiscourseChat::Provider::SlackProvider::PROVIDER_NAME before_filter :slack_token_valid?, only: :command + before_filter :slack_payload_token_valid?, only: :interactive skip_before_filter :check_xhr, :preload_json, :verify_authenticity_token, :redirect_to_login_if_required, - only: :command + only: [:command, :interactive] def command message = process_command(params) @@ -39,29 +40,31 @@ module DiscourseChat::Provider::SlackProvider channel ||= DiscourseChat::Channel.create!(provider: provider, data: { identifier: channel_id }) if tokens[0] == 'post' - return process_post_request(channel, tokens, params[:channel_id]) + return process_post_request(channel, tokens, params[:channel_id], channel_id) end return { text: ::DiscourseChat::Helper.process_command(channel, tokens) } end - def process_post_request(channel, tokens, slack_channel_id) + def process_post_request(channel, tokens, slack_channel_id, channel_name) if SiteSetting.chat_integration_slack_access_token.empty? return I18n.t("chat_integration.provider.slack.api_required") end - messages_to_load = 10 + requested_messages = 10 if tokens.size > 1 begin - messages_to_load = Integer(tokens[1], 10) + requested_messages = Integer(tokens[1], 10) rescue ArgumentError return { text: I18n.t("chat_integration.provider.slack.parse_error") } end end - transcript = SlackTranscript.load_transcript(slack_channel_id, messages_to_load) + transcript = SlackTranscript.load_transcript(slack_channel_id: slack_channel_id, + channel_name: channel_name, + requested_messages: requested_messages) return { text: I18n.t("chat_integration.provider.slack.transcript_error") } unless transcript @@ -69,6 +72,33 @@ module DiscourseChat::Provider::SlackProvider end + def interactive + json = JSON.parse(params[:payload], symbolize_names: true) + + render json: process_interactive(json) + 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 + + transcript = SlackTranscript.load_transcript(slack_channel_id: json[:channel][:id], + channel_name: "##{json[:channel][:name]}", + first_message_ts: first_message, + last_message_ts: last_message) + + return { text: I18n.t("chat_integration.provider.slack.transcript_error") } unless transcript + + message = transcript.build_slack_ui + + return message + end + def slack_token_valid? params.require(:token) @@ -78,6 +108,18 @@ module DiscourseChat::Provider::SlackProvider raise Discourse::InvalidAccess.new end end + + def slack_payload_token_valid? + params.require(:payload) + + json = JSON.parse(params[:payload], symbolize_names: true) + + if SiteSetting.chat_integration_slack_incoming_webhook_token.blank? || + SiteSetting.chat_integration_slack_incoming_webhook_token != json[:token] + + raise Discourse::InvalidAccess.new + end + end end class SlackEngine < ::Rails::Engine @@ -87,6 +129,7 @@ module DiscourseChat::Provider::SlackProvider SlackEngine.routes.draw do post "command" => "slack_command#command" + post "interactive" => "slack_command#interactive" end end diff --git a/lib/discourse_chat/provider/slack/slack_transcript_helper.rb b/lib/discourse_chat/provider/slack/slack_transcript_helper.rb index 4f5c439..17f15b2 100644 --- a/lib/discourse_chat/provider/slack/slack_transcript_helper.rb +++ b/lib/discourse_chat/provider/slack/slack_transcript_helper.rb @@ -29,22 +29,32 @@ module DiscourseChat::Provider::SlackProvider text = @raw["text"] # Format links (don't worry about special cases @ # !) - text.gsub!(/<(.*?)>/) do |match| + text = text.gsub(/<(.*?)>/) do |match| group = $1 parts = group.split('|') link = parts[0].start_with?('@', '#', '!') ? '' : parts[0] text = parts.length > 1 ? parts[1] : parts[0] + + if parts[0].start_with?('@') + user = @transcript.users.find { |u| u["id"] == parts[0].gsub('@', '') } + next "@#{user['name']}" + end + "[#{text}](#{link})" end # Add an extra * to each side for bold - text.gsub!(/\*(.*?)\*/) do |match| + text = text.gsub(/\*(.*?)\*/) do |match| "*#{match}*" end return text end + def raw_text + @raw['text'] + end + def attachments attachments = [] @@ -58,6 +68,10 @@ module DiscourseChat::Provider::SlackProvider return attachments end + def ts + @raw["ts"] + end + private def user return nil unless user_id = @raw["user"] @@ -70,27 +84,47 @@ module DiscourseChat::Provider::SlackProvider class SlackTranscript attr_reader :users, :channel_id - def initialize(raw_history, raw_users, channel_id) + def initialize(raw_history:, raw_users:, channel_id:, channel_name:, requested_messages: nil, first_message_ts: nil, last_message_ts: nil) + + requested_messages ||= 10 + + raw_messages = raw_history['messages'].reverse # Build some message objects @messages = [] - raw_history['messages'].reverse.each do |message| + raw_messages.each_with_index do |message, index| next unless message["type"] == "message" - @messages << SlackMessage.new(message, self) + this_message = SlackMessage.new(message, self) + @messages << this_message + + # Auto set first and last based on requested_messages + @first_message = this_message if index == raw_messages.length - requested_messages + @last_message = this_message if index == raw_messages.length - 1 end + if first_message_ts && last_message_ts + @first_message = @messages.find { |m| m.ts == first_message_ts } + @last_message = @messages.find { |m| m.ts == last_message_ts } + end + + @first_message_index = @messages.index(@first_message) + @last_message_index = @messages.index(@last_message) + @users = raw_users['members'] @channel_id = channel_id + @channel_name = channel_name end def build_transcript post_content = "[quote]\n" - post_content << "[**#{I18n.t('chat_integration.provider.slack.view_on_slack')}**](#{@messages.first.url})\n" + post_content << "[**#{I18n.t('chat_integration.provider.slack.transcript.view_on_slack', name: @channel_name)}**](#{@first_message.url})\n" all_avatars = {} last_username = '' - @messages.each do |m| + transcript_messages = @messages[@first_message_index..@last_message_index] + + transcript_messages.each do |m| same_user = m.username == last_username last_username = m.username @@ -127,7 +161,50 @@ 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.post_to_discourse")}>", + return { text: "<#{link}|#{I18n.t("chat_integration.provider.slack.transcript.post_to_discourse")}>", + attachments: [ + { + pretext: I18n.t("chat_integration.provider.slack.transcript.first_message_pretext", n: @messages.length - @first_message_index), + fallback: "#{@first_message.username} - #{@first_message.raw_text}", + color: "#007AB8", + author_name: @first_message.username, + author_icon: @first_message.avatar, + text: @first_message.raw_text, + footer: I18n.t("chat_integration.provider.slack.transcript.posted_in", name: @channel_name), + ts: @first_message.ts, + callback_id: @last_message.ts, + actions: [ + { + name: "first_message", + text: I18n.t("chat_integration.provider.slack.transcript.change_first_message"), + type: "select", + options: first_message_options = @messages[ [(@first_message_index - 20), 0].max .. @last_message_index] + .map { |m| { text: "#{m.username}: #{m.text}", value: m.ts } } + } + ], + }, + { + pretext: I18n.t("chat_integration.provider.slack.transcript.last_message_pretext", n: @messages.length - @last_message_index), + fallback: "#{@last_message.username} - #{@last_message.raw_text}", + color: "#007AB8", + author_name: @last_message.username, + author_icon: @last_message.avatar, + text: @last_message.raw_text, + footer: I18n.t("chat_integration.provider.slack.transcript.posted_in", name: @channel_name), + ts: @last_message.ts, + callback_id: @first_message.ts, + actions: [ + { + name: "last_message", + text: I18n.t("chat_integration.provider.slack.transcript.change_last_message"), + type: "select", + options: @messages[@first_message_index..(@last_message_index + 20)] + .map { |m| { text: "#{m.username}: #{m.text}", value: m.ts } } + } + ], + } + + ] } end @@ -144,7 +221,7 @@ module DiscourseChat::Provider::SlackProvider return json end - def self.load_chat_history(slack_channel_id, messages_to_load) + def self.load_chat_history(slack_channel_id:, count: 500) http = Net::HTTP.new("slack.com", 443) http.use_ssl = true @@ -153,7 +230,7 @@ module DiscourseChat::Provider::SlackProvider data = { token: SiteSetting.chat_integration_slack_access_token, channel: slack_channel_id, - count: messages_to_load + count: count } req.set_form_data(data) @@ -164,11 +241,18 @@ module DiscourseChat::Provider::SlackProvider return json end - def self.load_transcript(slack_channel_id, messages_to_load) + def self.load_transcript(slack_channel_id:, channel_name:, requested_messages: nil, first_message_ts: nil, last_message_ts: nil) return false unless raw_users = self.load_user_data - return false unless raw_history = self.load_chat_history(slack_channel_id, messages_to_load) - self.new(raw_history, raw_users, slack_channel_id) + return false unless raw_history = self.load_chat_history(slack_channel_id: slack_channel_id) + + self.new(raw_history: raw_history, + raw_users: raw_users, + channel_id: slack_channel_id, + channel_name: channel_name, + requested_messages: requested_messages, + first_message_ts: first_message_ts, + last_message_ts: last_message_ts) end end