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 @@
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),