Repository: zulip/zulip

Issue:
widgets: Prevent edits to /poll and /todo messages.
See #14229 for a more ambitious plan here, but in the short term, we want to prevent users from editing messages that are widgetized, such as "/poll" and "/todo" messages.  You can detect widgets by the presence of `submessage` rows attached to the `message`, although there may be something more explicit.

We want to either just remove all the UI ways to perform an edit, or better, have a little popup when users try to edit that says "Sorry you can not edit this message, because it's a widget."  We can work on the wording.

We should continue to allow admins to fully delete a widgetized message.


Discussion:
@zulipbot claim
Topic editing should still be allowed as normal. These messages should behave like messages sent by someone else, where you’re allowed to View source / Edit topic.
Ah, good point on topic edits.  So I think the only difference from the "View Source" experience should be that we change the messaging when you hover over the `?` icon next to "Topic editing only". 
Hello @zulip/server-integrations, @zulip/server-message-view members, this issue was labeled with the "area: integrations", "area: message-editing" labels, so you may want to check it out!

<!-- areaLabelAddition -->

Resolving patch:
diff --git a/zerver/lib/widget.py b/zerver/lib/widget.py
--- a/zerver/lib/widget.py
+++ b/zerver/lib/widget.py
@@ -3,7 +3,7 @@
 from typing import Any, Optional, Tuple
 
 from zerver.lib.message import SendMessageRequest
-from zerver.models import SubMessage
+from zerver.models import Message, SubMessage
 
 
 def get_widget_data(content: str) -> Tuple[Optional[str], Optional[str]]:
@@ -77,3 +77,8 @@ def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
         )
         submessage.save()
         send_request.submessages = SubMessage.get_raw_db_rows([message_id])
+
+
+def is_widget_message(message: Message) -> bool:
+    # Right now all messages that are widgetized use submessage, and vice versa.
+    return message.submessage_set.exists()
diff --git a/zerver/views/message_edit.py b/zerver/views/message_edit.py
--- a/zerver/views/message_edit.py
+++ b/zerver/views/message_edit.py
@@ -24,6 +24,7 @@
 from zerver.lib.timestamp import datetime_to_timestamp
 from zerver.lib.topic import LEGACY_PREV_TOPIC, REQ_topic
 from zerver.lib.validator import check_bool, check_string_in, to_non_negative_int
+from zerver.lib.widget import is_widget_message
 from zerver.models import Message, Realm, UserProfile
 
 
@@ -139,6 +140,10 @@ def update_message_backend(
     else:
         raise JsonableError(_("You don't have permission to edit this message"))
 
+    # Right now, we prevent users from editing widgets.
+    if content is not None and is_widget_message(message):
+        return json_error(_("Widgets cannot be edited."))
+
     # If there is a change to the content, check that it hasn't been too long
     # Allow an extra 20 seconds since we potentially allow editing 15 seconds
     # past the limit, and in case there are network issues, etc. The 15 comes


Test patch:
diff --git a/frontend_tests/node_tests/message_edit.js b/frontend_tests/node_tests/message_edit.js
--- a/frontend_tests/node_tests/message_edit.js
+++ b/frontend_tests/node_tests/message_edit.js
@@ -68,6 +68,10 @@ run_test("get_editability", () => {
     // is true, we can edit the topic if there is one.
     message.type = "stream";
     assert.equal(get_editability(message, 45), editability_types.TOPIC_ONLY);
+    // Right now, we prevent users from editing widgets.
+    message.submessages = ["/poll"];
+    assert.equal(get_editability(message, 45), editability_types.TOPIC_ONLY);
+    delete message.submessages;
     message.type = "private";
     assert.equal(get_editability(message, 45), editability_types.NO_LONGER);
     // If we don't pass a second argument, treat it as 0
diff --git a/zerver/tests/test_message_edit.py b/zerver/tests/test_message_edit.py
--- a/zerver/tests/test_message_edit.py
+++ b/zerver/tests/test_message_edit.py
@@ -187,6 +187,24 @@ def test_fetch_raw_message_private_stream(self) -> None:
         result = self.client_get("/json/messages/" + str(msg_id))
         self.assert_json_error(result, "Invalid message(s)")
 
+    # Right now, we prevent users from editing widgets.
+    def test_edit_submessage(self) -> None:
+        self.login("hamlet")
+        msg_id = self.send_stream_message(
+            self.example_user("hamlet"),
+            "Scotland",
+            topic_name="editing",
+            content="/poll Games?\nYES\nNO",
+        )
+        result = self.client_patch(
+            "/json/messages/" + str(msg_id),
+            {
+                "message_id": msg_id,
+                "content": "/poll Games?\nYES\nNO\nMaybe",
+            },
+        )
+        self.assert_json_error(result, "Widgets cannot be edited.")
+
     def test_edit_message_no_permission(self) -> None:
         self.login("hamlet")
         msg_id = self.send_stream_message(