Sfoglia il codice sorgente

[slack] Add Share from Editor Section and Gist modal improvements (#2049)

Harsh Gupta 4 anni fa
parent
commit
d36e001078

+ 34 - 0
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1466,6 +1466,40 @@ class ApiHelper {
     });
   }
 
+  /**
+   *
+   * @param {Object} options
+   * @param {channel} options.channel
+   * @param {message} options.message
+   *
+   * @return {Promise<void>}
+   */
+  async sendSlackMessageAsync(options) {
+    const data = {
+      channel: options.channel,
+      message: options.message
+    };
+    return new Promise((resolve, reject) => {
+      simplePost(URLS.SEND_SLACK_MESSAGE, data, options).done(resolve).fail(reject);
+    });
+  }
+
+  /**
+   *
+   * @param {Object} options
+   *
+   * @return {Promise<Object>}
+   */
+  async getSlackChannelsAsync(options) {
+    return new Promise((resolve, reject) => {
+      simpleGet(URLS.GET_SLACK_CHANNELS, {}, options)
+        .done(response => {
+          resolve(response.channels);
+        })
+        .fail(reject);
+    });
+  }
+
   /**
    * Updates Navigator properties and custom metadata for the given entity
    *

+ 2 - 0
desktop/core/src/desktop/js/api/urls.js

@@ -35,6 +35,8 @@ export const DASHBOARD_TERMS_API = '/dashboard/get_terms';
 export const DASHBOARD_STATS_API = '/dashboard/get_stats';
 export const FORMAT_SQL_API = '/notebook/api/format';
 export const GIST_API = '/desktop/api2/gist/';
+export const GET_SLACK_CHANNELS = '/slack/api/channels/get';
+export const SEND_SLACK_MESSAGE = '/slack/api/message/send';
 export const TOPO_URL = '/desktop/topo/';
 
 export const SEARCH_API = '/desktop/api/search/entities';

+ 3 - 1
desktop/core/src/desktop/js/apps/editor/components/ko.snippetEditorActions.js

@@ -157,7 +157,9 @@ class SnippetEditorActions {
       description: ''
     });
 
-    huePubSub.publish(SHOW_GIST_MODAL_EVENT, { link: gistLink });
+    const slackChannels = await apiHelper.getSlackChannelsAsync();
+
+    huePubSub.publish(SHOW_GIST_MODAL_EVENT, { link: gistLink, channels: slackChannels });
   }
 
   format() {

+ 4 - 5
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.shareGistModal.test.js.snap

@@ -8,15 +8,14 @@ exports[`ko.shareGistModal.js should render component 1`] = `
   </div>
   <div class=\\"modal-body\\">
     <div class=\\"row-fluid\\">
-      <div class=\\"span12\\">
+      <div class=\\"input-append\\">
         <form autocomplete=\\"off\\">
-          <input id=\\"gistLink\\" class=\\"input-xxlarge\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" onfocus=\\"this.select()\\" data-bind=\\"value: link\\" type=\\"text\\" placeholder=\\"Link\\">
+          <input id=\\"gistLink\\" style=\\"width: 510px\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" onfocus=\\"this.select()\\" data-bind=\\"value: link\\" type=\\"text\\" placeholder=\\"Link\\">
+          <button class=\\"btn\\" type=\\"button\\" data-dismiss=\\"modal\\" data-clipboard-target=\\"#gistLink\\" data-bind=\\"clipboard\\"><i class=\\"fa fa-clipboard\\"></i></button>
         </form>
-        <button class=\\"btn\\" type=\\"button\\" data-dismiss=\\"modal\\" data-clipboard-target=\\"#gistLink\\" data-bind=\\"clipboard\\">
-          <i class=\\"fa fa-clipboard\\"></i> Copy
-        </button>
       </div>
     </div>
+    <!-- ko if: window.SHARE_TO_SLACK --><!-- /ko -->
   </div>
   <div class=\\"modal-footer\\">
     <a class=\\"btn\\" data-dismiss=\\"modal\\">Close</a>

+ 51 - 6
desktop/core/src/desktop/js/ko/components/ko.shareGistModal.js

@@ -17,9 +17,11 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
+import ApiHelper from '/api/apiHelper';
 import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
+import DisposableComponent from 'ko/components/DisposableComponent';
 
 export const SHOW_EVENT = 'show.share.gist.modal';
 export const SHOWN_EVENT = 'share.gist.modal.shown';
@@ -32,21 +34,64 @@ const TEMPLATE = `
   </div>
   <div class="modal-body">
     <div class="row-fluid">
-      <div class="span12">
+      <div class="input-append">
         <form autocomplete="off">
-          <input id="gistLink" class="input-xxlarge" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } onfocus="this.select()" data-bind="value: link" type="text" placeholder="${ I18n('Link') }"/>
+          <input id="gistLink" style="width: 510px" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } onfocus="this.select()" data-bind="value: link" type="text" placeholder="${ I18n('Link') }"/>
+          <button class="btn" type="button" data-dismiss="modal" data-clipboard-target="#gistLink" data-bind="clipboard"><i class="fa fa-clipboard"></i></button>
         </form>
-        <button class="btn" type="button" data-dismiss="modal" data-clipboard-target="#gistLink" data-bind="clipboard">
-          <i class="fa fa-clipboard"></i> ${ I18n('Copy') }
-        </button>
       </div>
     </div>
+    <!-- ko if: window.SHARE_TO_SLACK -->
+    <label class="checkbox"><input type="checkbox" data-bind="checked: showSlackSection"> ${ I18n('Share to Slack') } </input></label>
+    <!-- ko if: showSlackSection -->
+    <form class="form-horizontal">
+      <div class="control-group">
+        <label class="control-label">${ I18n('Message') }</label>
+        <div class="controls">
+          <input type="text" style="width: 245px" data-bind="value: messageDescription, valueUpdate:'afterkeydown'" placeholder="${ I18n('(Optional)') }"/>
+        </div>
+      </div>
+      <div class="control-group">
+        <label class="control-label">${ I18n('Channel') }</label>
+        <div class="controls">
+          <select class="input-xlarge" data-bind="options: channels, optionsCaption: 'Choose...', value: selectedChannel"></select>
+          <button class="btn" type="button" data-dismiss="modal" data-bind="click: postMessage">${ I18n('Send') }</button>
+        </div>
+      </div>
+    </form>
+  <!-- /ko -->
+  <!-- /ko -->
   </div>
   <div class="modal-footer">
     <a class="btn" data-dismiss="modal">${ I18n('Close') }</a>
   </div>
 `;
 
+class ShareGistModal extends DisposableComponent {
+  constructor(params) {
+    super();
+    this.showSlackSection = ko.observable(false);
+
+    this.link = ko.observable(params.link);
+    this.channels = ko.observableArray(params.channels);
+
+    this.selectedChannel = ko.observable('');
+    this.messageDescription = ko.observable('');
+  }
+
+  async postMessage() {
+    try {
+      await ApiHelper.sendSlackMessageAsync({
+        channel: this.selectedChannel,
+        message: this.messageDescription() + '\n' + this.link()
+      });
+    } catch (err) {
+      console.warn('Failed posting message in Slack channel');
+      console.error(err);
+    }
+  }
+}
+
 componentUtils.registerComponent('share-gist-modal', undefined, TEMPLATE).then(() => {
   huePubSub.subscribe(SHOW_EVENT, async params => {
     let $shareGistModal = $('#shareGistModal');
@@ -61,7 +106,7 @@ componentUtils.registerComponent('share-gist-modal', undefined, TEMPLATE).then((
     $('body').append($shareGistModal);
 
     const data = {
-      params: params,
+      params: new ShareGistModal(params),
       descendantsComplete: () => {
         huePubSub.publish(SHOWN_EVENT);
       }

+ 18 - 0
desktop/core/src/desktop/lib/botserver/api.py

@@ -20,6 +20,7 @@ import json
 import sys
 
 from desktop.lib.botserver.slack_client import slack_client
+from desktop.lib.exceptions_renderable import PopupException
 from desktop.decorators import api_error_handler
 from desktop.lib.django_util import JsonResponse
 
@@ -43,3 +44,20 @@ def get_channels(request):
   return JsonResponse({
     'channels': bot_channels,
   })
+
+@api_error_handler
+def send_message(request):
+  channel = request.POST.get('channel')
+  message = request.POST.get('message')
+
+  slack_response = _send_message(channel, message)
+
+  return JsonResponse({
+    'ok': slack_response.get('ok'),
+  })
+
+def _send_message(channel_info, message):
+  try:
+    return slack_client.chat_postMessage(channel=channel_info, text=message)
+  except Exception as e:
+    raise PopupException(_("Error posting message in channel"), detail=e)

+ 15 - 1
desktop/core/src/desktop/lib/botserver/api_tests.py

@@ -62,8 +62,22 @@ class TestApi(object):
         }
       }
 
-      response = self.client.post(reverse('botserver.api.get_channels'))
+      response = self.client.get(reverse('botserver.api.get_channels'))
       data = json.loads(response.content)
 
       assert_equal(200, response.status_code)
       assert_equal(['channel-1', 'channel-2'], data.get('channels'))
+
+  def test_send_message(self):
+    with patch('desktop.lib.botserver.api.slack_client.chat_postMessage') as chat_postMessage:
+
+      chat_postMessage.return_value = {
+         "ok": True,
+      }
+
+      response = self.client.post(reverse('botserver.api.send_message'), {'channel': 'channel-1', 'message': 'some message'})
+      data = json.loads(response.content)
+
+      assert_equal(200, response.status_code)
+      chat_postMessage.assert_called_with(channel='channel-1', text='some message')
+      assert_true(data.get('ok'))

+ 1 - 0
desktop/core/src/desktop/lib/botserver/urls.py

@@ -28,4 +28,5 @@ urlpatterns = [
   re_path(r'^events/', views.slack_events, name='desktop.lib.botserver.views.slack_events'),
 
   re_path(r'^api/channels/get/?$', api.get_channels, name='botserver.api.get_channels'),
+  re_path(r'^api/message/send/?$', api.send_message, name='botserver.api.send_message'),
 ]

+ 14 - 18
desktop/core/src/desktop/lib/botserver/views.py

@@ -22,6 +22,7 @@ from urllib.parse import urlsplit
 from tabulate import tabulate
 
 from desktop.lib.botserver.slack_client import slack_client, SLACK_VERIFICATION_TOKEN
+from desktop.lib.botserver.api import _send_message
 from desktop.lib.django_util import login_notrequired, JsonResponse
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.models import Document2, _get_gist_document
@@ -89,7 +90,8 @@ def handle_on_message(channel_id, bot_id, text, user_id):
   
   if slack_client is not None:
     if text and 'hello hue' in text.lower():
-      send_hi_user(channel_id, user_id)
+      bot_message = 'Hi <@{user}> :wave:'.format(user=user_id)
+      _send_message(channel_id, bot_message)
 
 
 def handle_on_link_shared(channel_id, message_ts, links, user_id):
@@ -113,7 +115,8 @@ def handle_on_link_shared(channel_id, message_ts, links, user_id):
 
     # Permission check for Slack user to be Hue user
     try:
-      user = User.objects.get(username=slack_email_prefix(user_id))
+      slack_user = slack_user_check(user_id)
+      user = User.objects.get(username=slack_user['user_email_prefix']) if not slack_user['is_bot'] else doc.owner
     except User.DoesNotExist:
       raise PopupException(_("Slack user does not have access to the query"))
 
@@ -134,14 +137,19 @@ def handle_on_link_shared(channel_id, message_ts, links, user_id):
       send_result_file(request, channel_id, message_ts, doc, 'xls')
 
 
-def slack_email_prefix(user_id):
+def slack_user_check(user_id):
   try:
     slack_user = slack_client.users_info(user=user_id)
   except Exception as e:
     raise PopupException(_("Cannot find query owner in Slack"), detail=e)
   
-  if slack_user['ok']:
-    return slack_user['user']['profile']['email'].split('@')[0]
+  response = {
+    'is_bot': slack_user['user']['is_bot'],
+  }
+  if not slack_user['user']['is_bot']:
+    response['user_email_prefix'] = slack_user['user']['profile']['email'].split('@')[0]
+
+  return response
 
 
 def send_result_file(request, channel_id, message_ts, doc, file_format):
@@ -269,16 +277,4 @@ def _make_unfurl_payload(request, url, id_type, doc, doc_type):
   if result_section is not None:
     payload[url]['blocks'].append(result_section)
 
-  return {'payload': payload, 'file_status': file_status}
-
-
-def send_hi_user(channel_id, user_id):
-  """
-  Sends Hi<user_id> message in a specific channel.
-
-  """
-  bot_message = 'Hi <@{user}> :wave:'.format(user=user_id)
-  try:
-    slack_client.api_call(api_method='chat.postMessage', json={'channel': channel_id, 'text': bot_message})
-  except Exception as e:
-    raise PopupException(_("Error posting message"), detail=e)
+  return {'payload': payload, 'file_status': file_status}

+ 20 - 16
desktop/core/src/desktop/lib/botserver/views_tests.py

@@ -55,32 +55,21 @@ class TestBotServer(unittest.TestCase):
     self.client_not_me = make_logged_in_client(username="test_not_me", groupname="default", recreate=True, is_superuser=False)
     self.user_not_me = User.objects.get(username="test_not_me")
 
-  def test_send_hi_user(self):
-    with patch('desktop.lib.botserver.views.slack_client.api_call') as api_call:
-      api_call.return_value = {
-        "ok": True
-      }
-      send_hi_user("channel", "user_id")
-      api_call.assert_called_with(api_method='chat.postMessage', json={'channel': 'channel', 'text': 'Hi <@user_id> :wave:'})
-
-      api_call.side_effect = PopupException('message')
-      assert_raises(PopupException, send_hi_user, "channel", "user_id")
-
   def test_handle_on_message(self):
-    with patch('desktop.lib.botserver.views.send_hi_user') as say_hi_user:
+    with patch('desktop.lib.botserver.views._send_message') as _send_message:
       
       response = handle_on_message("channel", "bot_id", "text", "user_id")
       assert_equal(response.status_code, 200)
-      assert_false(say_hi_user.called)
+      assert_false(_send_message.called)
       
       handle_on_message("channel", None, None, "user_id")
-      assert_false(say_hi_user.called)
+      assert_false(_send_message.called)
 
       handle_on_message("channel", None, "text", "user_id")
-      assert_false(say_hi_user.called)
+      assert_false(_send_message.called)
 
       handle_on_message("channel", None, "hello hue test", "user_id")
-      assert_true(say_hi_user.called)
+      assert_true(_send_message.called)
   
   def test_handle_query_history_link(self):
     with patch('desktop.lib.botserver.views.slack_client.chat_unfurl') as chat_unfurl:
@@ -108,6 +97,7 @@ class TestBotServer(unittest.TestCase):
                 users_info.return_value = {
                   "ok": True,
                   "user": {
+                    "is_bot": False,
                     "profile": {
                       "email": "test_not_me@example.com"
                     }
@@ -193,6 +183,7 @@ class TestBotServer(unittest.TestCase):
           users_info.return_value = {
             "ok": True,
             "user": {
+              "is_bot": False,
               "profile": {
                 "email": "test@example.com"
               }
@@ -228,6 +219,18 @@ class TestBotServer(unittest.TestCase):
           chat_unfurl.assert_called_with(channel=channel_id, ts=message_ts, unfurls=gist_preview)
           assert_false(send_result_file.called)
 
+          # Gist link sent directly from Hue to Slack via bot
+          users_info.return_value = {
+            "ok": True,
+            "user": {
+              "is_bot": True,
+            }
+          }
+          handle_on_link_shared(channel_id, message_ts, links, user_id)
+
+          chat_unfurl.assert_called_with(channel=channel_id, ts=message_ts, unfurls=gist_preview)
+          assert_false(send_result_file.called)
+
           # Gist document does not exist
           gist_url = "https://demo.gethue.com/hue/gist?uuid=6d1c407b-d999-4dfd-ad23-d3a46c19a427"
           assert_raises(PopupException, handle_on_link_shared, "channel", "12.1", [{"url": gist_url}], "<@user_id>")
@@ -248,6 +251,7 @@ class TestBotServer(unittest.TestCase):
         users_info.return_value = {
           "ok": True,
           "user": {
+            "is_bot": False,
             "profile": {
               "email": "test_user_not_exist@example.com"
             }

+ 1 - 0
desktop/core/src/desktop/templates/global_js_constants.mako

@@ -120,6 +120,7 @@
   window.AUTO_UPLOAD_OPTIMIZER_STATS = '${ OPTIMIZER.AUTO_UPLOAD_STATS.get() }' === 'True';
 
   window.HAS_GIST = '${ ENABLE_GIST.get() }' === 'True';
+  window.SHARE_TO_SLACK = '${ conf.SLACK.SHARE_FROM_EDITOR.get() }' === 'True';
   window.HAS_LINK_SHARING = '${ ENABLE_LINK_SHARING.get() }' === 'True';
   window.HAS_CONNECTORS = '${ has_connectors() }' === 'True';