From 33c0c00cd626edeffc39523497b102acb7cc3746 Mon Sep 17 00:00:00 2001 From: vaibhav Date: Sat, 10 Jun 2017 22:13:31 +0530 Subject: [PATCH] Outgoing webhook System: first Iteration of outgoing webhook UI. --- static/js/settings_bots.js | 25 +++++++++++++++ static/styles/settings.css | 4 +++ .../settings/bot-settings.handlebars | 7 +++++ tools/linter_lib/custom_check.py | 2 ++ zerver/models.py | 2 ++ zerver/tests/test_bots.py | 31 ++++++++++++++++++- zerver/views/users.py | 25 +++++++++++++-- 7 files changed, 92 insertions(+), 4 deletions(-) diff --git a/static/js/settings_bots.js b/static/js/settings_bots.js index 3720380799..09b61af643 100644 --- a/static/js/settings_bots.js +++ b/static/js/settings_bots.js @@ -75,6 +75,8 @@ exports.set_up = function () { $("#get_api_key_box").hide(); $("#show_api_key_box").hide(); $("#api_key_button_box").show(); + $('#payload_url_inputbox').hide(); + $('#create_payload_url').val(''); $('#api_key_button').click(function () { if (page_params.realm_password_auth_enabled !== false) { @@ -119,6 +121,8 @@ exports.set_up = function () { var create_avatar_widget = avatar.build_bot_create_widget(); + var OUTGOING_WEBHOOK_BOT_TYPE = '3'; + var GENERIC_BOT_TYPE = '1'; $('#create_bot_form').validate({ errorClass: 'text-error', @@ -129,12 +133,18 @@ exports.set_up = function () { var bot_type = $('#create_bot_type :selected').val(); var full_name = $('#create_bot_name').val(); var short_name = $('#create_bot_short_name').val() || $('#create_bot_short_name').text(); + var payload_url = $('#create_payload_url').val(); var formData = new FormData(); formData.append('csrfmiddlewaretoken', csrf_token); formData.append('bot_type', bot_type); formData.append('full_name', full_name); formData.append('short_name', short_name); + + // If the selected bot_type is Outgoing webhook + if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { + formData.append('payload_url', JSON.stringify(payload_url)); + } jQuery.each($('#bot_avatar_file_input')[0].files, function (i, file) { formData.append('file-'+i, file); }); @@ -149,6 +159,9 @@ exports.set_up = function () { $('#bot_table_error').hide(); $('#create_bot_name').val(''); $('#create_bot_short_name').val(''); + $('#create_payload_url').val(''); + $('#payload_url_inputbox').hide(); + $('#create_bot_type').val(GENERIC_BOT_TYPE); $('#create_bot_button').show(); create_avatar_widget.clear(); }, @@ -162,6 +175,18 @@ exports.set_up = function () { }, }); + $("#create_bot_type").on("change", function () { + var bot_type = $('#create_bot_type :selected').val(); + // If the selected bot_type is Outgoing webhook + if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { + $('#payload_url_inputbox').show(); + $('#create_payload_url').addClass('required'); + } else { + $('#payload_url_inputbox').hide(); + $('#create_payload_url').removeClass('required'); + } + }); + $("#active_bots_list").on("click", "button.delete_bot", function (e) { var email = $(e.currentTarget).data('email'); channel.del({ diff --git a/static/styles/settings.css b/static/styles/settings.css index 9dd5b87cdd..2de2fea4f8 100644 --- a/static/styles/settings.css +++ b/static/styles/settings.css @@ -1212,3 +1212,7 @@ thead .actions { .required-text.thick:empty::after { width: 200%; } + +#payload_url_inputbox input[type=text] { + width: 340px; +} diff --git a/static/templates/settings/bot-settings.handlebars b/static/templates/settings/bot-settings.handlebars index 0aa44ccd5e..a3a6b3e722 100644 --- a/static/templates/settings/bot-settings.handlebars +++ b/static/templates/settings/bot-settings.handlebars @@ -33,6 +33,7 @@
@@ -50,6 +51,12 @@
+
+ + +
+
diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index 37d8b0ea6a..4858e8be22 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -157,6 +157,8 @@ def build_custom_checkers(by_lang): "return json_error(_(\"Email '%(email)s' not allowed for realm '%(realm)s'\") %"), ('zproject/settings.py', "'format': '%(asctime)s %(levelname)-8s %(message)s'"), + ('static/templates/settings/bot-settings.handlebars', + "'https://hostname.example.com/bots/followup'"), ]), 'description': 'Missing space around "%"'}, # This rule is constructed with + to avoid triggering on itself diff --git a/zerver/models.py b/zerver/models.py index ed4b8ebfc2..0c881e9f43 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -528,6 +528,7 @@ class UserProfile(ModelReprMixin, AbstractBaseUser, PermissionsMixin): since they can't be used to read messages. """ INCOMING_WEBHOOK_BOT = 2 + # This value is also being used in static/js/settings_bots.js. On updating it here, update it there as well. OUTGOING_WEBHOOK_BOT = 3 """ Embedded bots run within the Zulip server itself; events are added to the @@ -539,6 +540,7 @@ class UserProfile(ModelReprMixin, AbstractBaseUser, PermissionsMixin): ALLOWED_BOT_TYPES = [ DEFAULT_BOT, INCOMING_WEBHOOK_BOT, + OUTGOING_WEBHOOK_BOT, ] SERVICE_BOT_TYPES = [ diff --git a/zerver/tests/test_bots.py b/zerver/tests/test_bots.py index 8cf9c0b1f9..b97b23a307 100644 --- a/zerver/tests/test_bots.py +++ b/zerver/tests/test_bots.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List from zerver.lib.actions import do_change_stream_invite_only from zerver.models import get_realm, get_stream, \ - Realm, Stream, UserProfile, get_user + Realm, Stream, UserProfile, get_user, get_bot_services from zerver.lib.test_classes import ZulipTestCase, UploadSerializeMixin from zerver.lib.test_helpers import ( avatar_disk_path, get_test_image_file, tornado_redirected_to_list, @@ -911,3 +911,32 @@ class BotTest(ZulipTestCase, UploadSerializeMixin): result = self.client_patch("/json/bots/nonexistent-bot@zulip.com", bot_info) self.assert_json_error(result, 'No such user') self.assert_num_bots_equal(1) + + def test_create_outgoing_webhook_bot(self, **extras): + # type: (**Any) -> None + self.login(self.example_email('hamlet')) + bot_info = { + 'full_name': 'Outgoing Webhook test bot', + 'short_name': 'outgoingservicebot', + 'bot_type': UserProfile.OUTGOING_WEBHOOK_BOT, + 'payload_url': ujson.dumps('http://127.0.0.1:5002/bots/followup'), + } + bot_info.update(extras) + result = self.client_post("/json/bots", bot_info) + self.assert_json_success(result) + + bot_email = "outgoingservicebot-bot@zulip.testserver" + bot_realm = get_realm('zulip') + bot = get_user(bot_email, bot_realm) + services = get_bot_services(bot.id) + service = services[0] + + self.assertEqual(len(services), 1) + self.assertEqual(service.name, "outgoingservicebot") + self.assertEqual(service.base_url, "http://127.0.0.1:5002/bots/followup") + self.assertEqual(service.user_profile, bot) + + # invalid URL test case. + bot_info['payload_url'] = ujson.dumps('http://127.0.0.:5002/bots/followup') + result = self.client_post("/json/bots", bot_info) + self.assert_json_error(result, "Enter a valid URL.") diff --git a/zerver/views/users.py b/zerver/views/users.py index 63ef8fc3a2..360ac0f57a 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -22,11 +22,12 @@ from zerver.lib.avatar import avatar_url, get_avatar_url from zerver.lib.response import json_error, json_success from zerver.lib.streams import access_stream_by_name from zerver.lib.upload import upload_avatar_image -from zerver.lib.validator import check_bool, check_string, check_int +from zerver.lib.validator import check_bool, check_string, check_int, check_url from zerver.lib.users import check_valid_bot_type, check_change_full_name, check_full_name from zerver.lib.utils import generate_random_token from zerver.models import UserProfile, Stream, Realm, Message, get_user_profile_by_email, \ - email_allowed_for_realm, get_user_profile_by_id, get_user + email_allowed_for_realm, get_user_profile_by_id, get_user, Service +from zerver.lib.create_user import random_api_key def deactivate_user_backend(request, user_profile, email): @@ -229,13 +230,23 @@ def regenerate_bot_api_key(request, user_profile, email): ) return json_success(json_result) +def add_outgoing_webhook_service(name, user_profile, base_url, interface, token): + # type: (Text, UserProfile, Text, int, Text) -> None + Service.objects.create(name=name, + user_profile=user_profile, + base_url=base_url, + interface=interface, + token=token) + @has_request_variables def add_bot_backend(request, user_profile, full_name_raw=REQ("full_name"), short_name=REQ(), bot_type=REQ(validator=check_int, default=UserProfile.DEFAULT_BOT), + payload_url=REQ(validator=check_url, default=None), default_sending_stream_name=REQ('default_sending_stream', default=None), default_events_register_stream_name=REQ('default_events_register_stream', default=None), default_all_public_streams=REQ(validator=check_bool, default=None)): - # type: (HttpRequest, UserProfile, Text, Text, int, Optional[Text], Optional[Text], Optional[bool]) -> HttpResponse + # type: (HttpRequest, UserProfile, Text, Text, int, Optional[Text], Optional[Text], Optional[Text], Optional[bool]) -> HttpResponse + service_name = short_name short_name += "-bot" full_name = check_full_name(full_name_raw) email = '%s@%s' % (short_name, user_profile.realm.get_bot_domain()) @@ -279,6 +290,14 @@ def add_bot_backend(request, user_profile, full_name_raw=REQ("full_name"), short if len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot_profile) + + if bot_type == UserProfile.OUTGOING_WEBHOOK_BOT: + add_outgoing_webhook_service(name=service_name, + user_profile=bot_profile, + base_url=payload_url, + interface=1, + token=random_api_key()) + json_result = dict( api_key=bot_profile.api_key, avatar_url=avatar_url(bot_profile),