Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[slack] Add Share from Editor Section and Gist modal improvements (#2049
)
  • Loading branch information
Harshg999 committed Apr 30, 2021
1 parent 4aa4b05 commit d36e001
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 47 deletions.
34 changes: 34 additions & 0 deletions desktop/core/src/desktop/js/api/apiHelper.js
Expand Up @@ -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
*
Expand Down
2 changes: 2 additions & 0 deletions desktop/core/src/desktop/js/api/urls.js
Expand Up @@ -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';
Expand Down
Expand Up @@ -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() {
Expand Down
Expand Up @@ -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>
Expand Down
57 changes: 51 additions & 6 deletions desktop/core/src/desktop/js/ko/components/ko.shareGistModal.js
Expand Up @@ -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';
Expand All @@ -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');
Expand All @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions desktop/core/src/desktop/lib/botserver/api.py
Expand Up @@ -20,6 +20,7 @@
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

Expand All @@ -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)
16 changes: 15 additions & 1 deletion desktop/core/src/desktop/lib/botserver/api_tests.py
Expand Up @@ -62,8 +62,22 @@ def test_get_channels(self):
}
}

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 change: 1 addition & 0 deletions desktop/core/src/desktop/lib/botserver/urls.py
Expand Up @@ -28,4 +28,5 @@
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'),
]
32 changes: 14 additions & 18 deletions desktop/core/src/desktop/lib/botserver/views.py
Expand Up @@ -22,6 +22,7 @@
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
Expand Down Expand Up @@ -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):
Expand All @@ -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"))

Expand All @@ -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):
Expand Down Expand Up @@ -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}
36 changes: 20 additions & 16 deletions desktop/core/src/desktop/lib/botserver/views_tests.py
Expand Up @@ -55,32 +55,21 @@ def setUp(self):
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:
Expand Down Expand Up @@ -108,6 +97,7 @@ def test_handle_query_history_link(self):
users_info.return_value = {
"ok": True,
"user": {
"is_bot": False,
"profile": {
"email": "test_not_me@example.com"
}
Expand Down Expand Up @@ -193,6 +183,7 @@ def test_handle_gist_link(self):
users_info.return_value = {
"ok": True,
"user": {
"is_bot": False,
"profile": {
"email": "test@example.com"
}
Expand Down Expand Up @@ -228,6 +219,18 @@ def test_handle_gist_link(self):
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>")
Expand All @@ -248,6 +251,7 @@ def test_slack_user_not_hue_user(self):
users_info.return_value = {
"ok": True,
"user": {
"is_bot": False,
"profile": {
"email": "test_user_not_exist@example.com"
}
Expand Down
Expand Up @@ -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';

Expand Down

0 comments on commit d36e001

Please sign in to comment.