diff --git a/.eslintrc.json b/.eslintrc.json index 82a5e9a873..3cd4207645 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -74,6 +74,7 @@ "typing_status": false, "sent_messages": false, "transmit": false, + "zcommand": false, "compose": false, "compose_actions": false, "compose_state": false, diff --git a/frontend_tests/node_tests/compose.js b/frontend_tests/node_tests/compose.js index f4edc87e37..ac0980a448 100644 --- a/frontend_tests/node_tests/compose.js +++ b/frontend_tests/node_tests/compose.js @@ -55,6 +55,7 @@ set_global('reminder', { global.document.location.protocol = 'https:'; global.document.location.host = 'foo.com'; +zrequire('zcommand'); zrequire('compose_ui'); zrequire('util'); zrequire('common'); diff --git a/static/js/compose.js b/static/js/compose.js index 3876ea4338..526e569753 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -299,11 +299,21 @@ exports.finish = function () { exports.clear_private_stream_alert(); notifications.clear_compose_notifications(); + var message_content = compose_state.message_content(); + + // Skip normal validation for zcommands, since they aren't + // actual messages with recipients; users only send them + // from the compose box for convenience sake. + if (zcommand.process(message_content)) { + exports.do_post_send_tasks(); + clear_compose_box(); + return; + } + if (! compose.validate()) { return false; } - var message_content = compose_state.message_content(); if (reminder.is_deferred_delivery(message_content)) { reminder.schedule_message(); } else { diff --git a/static/js/zcommand.js b/static/js/zcommand.js new file mode 100644 index 0000000000..8e7de6be0b --- /dev/null +++ b/static/js/zcommand.js @@ -0,0 +1,82 @@ +var zcommand = (function () { + +var exports = {}; + +/* + +What in the heck is a zcommand? + + A zcommand is basically a specific type of slash + command where the client does almost no work and + the server just does something pretty simple like + flip a setting. + + The first zcommand we wrote is for "/ping", and + the server just responds with a 200 for that. + + Not all slash commands use zcommand under the hood. + For more exotic things like /poll see submessage.js + and widgetize.js + +*/ + +exports.send = function (opts) { + var command = opts.command; + var on_success = opts.on_success; + var data = { + command: command, + }; + + channel.post({ + url: '/json/zcommand', + data: data, + success: function (data) { + on_success(data); + }, + error: function () { + exports.tell_user('server did not respond'); + }, + }); +}; + +exports.tell_user = function (msg) { + // This is a bit hacky, but we don't have a super easy API now + // for just telling users stuff. + $('#compose-send-status').removeClass(common.status_classes) + .addClass('alert-error') + .stop(true).fadeTo(0, 1); + $('#compose-error-msg').text(msg); +}; + +exports.process = function (message_content) { + + var content = message_content.trim(); + + if (content === '/ping') { + var start_time = new Date(); + + exports.send({ + command: 'ping', + on_success: function () { + var end_time = new Date(); + var diff = end_time - start_time; + diff = Math.round(diff); + var msg = "ping time: " + diff + "ms"; + exports.tell_user(msg); + }, + }); + return true; + } + + // It is incredibly important here to return false + // if we don't see an actual zcommand, so that compose.js + // knows this is a normal message. + return false; +}; + +return exports; +}()); + +if (typeof module !== 'undefined') { + module.exports = zcommand; +} diff --git a/zerver/tests/test_messages.py b/zerver/tests/test_messages.py index 6aafa5dd45..56db74f5bc 100644 --- a/zerver/tests/test_messages.py +++ b/zerver/tests/test_messages.py @@ -999,6 +999,17 @@ class SewMessageAndReactionTest(ZulipTestCase): class MessagePOSTTest(ZulipTestCase): + def test_zcommand(self) -> None: + self.login(self.example_email("hamlet")) + + payload = dict(command="boil-ocean") + result = self.client_post("/json/zcommand", payload) + self.assert_json_error(result, "No such command: boil-ocean") + + payload = dict(command="ping") + result = self.client_post("/json/zcommand", payload) + self.assert_json_success(result) + def test_message_to_self(self) -> None: """ Sending a message to a stream to which you are subscribed is diff --git a/zerver/views/messages.py b/zerver/views/messages.py index 057ba56cd3..c059987e61 100644 --- a/zerver/views/messages.py +++ b/zerver/views/messages.py @@ -681,6 +681,15 @@ def find_first_unread_anchor(sa_conn: Any, return anchor +@has_request_variables +def zcommand_backend(request: HttpRequest, user_profile: UserProfile, + command: str=REQ('command')) -> HttpResponse: + if command == 'ping': + ret = dict() # type: Dict[str, Any] + return json_success(ret) + + raise JsonableError(_('No such command: %s') % (command,)) + @has_request_variables def get_messages_backend(request: HttpRequest, user_profile: UserProfile, anchor: int=REQ(converter=int), diff --git a/zproject/settings.py b/zproject/settings.py index 6e60c686b7..d41c0e4364 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -968,6 +968,7 @@ JS_SPECS = { 'js/compose_state.js', 'js/compose_actions.js', 'js/transmit.js', + 'js/zcommand.js', 'js/compose.js', 'js/upload.js', 'js/stream_color.js', diff --git a/zproject/urls.py b/zproject/urls.py index cf311b3f4a..f28a0d73b1 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -162,6 +162,9 @@ v1_api_and_json_patterns = [ url(r'^mark_topic_as_read$', rest_dispatch, {'POST': 'zerver.views.messages.mark_topic_as_read'}), + url(r'^zcommand$', rest_dispatch, + {'POST': 'zerver.views.messages.zcommand_backend'}), + # messages -> zerver.views.messages # GET returns messages, possibly filtered, POST sends a message url(r'^messages$', rest_dispatch,