api: Send full message in GET /messages/{message_id} response.

Previously, this URL just returned the `raw_content` field. It seems
cleanest to just make it a single-message variant of GET /messages,
deprecating the only format.
This commit is contained in:
Aman Agrawal 2022-03-02 04:33:20 +00:00 committed by Tim Abbott
parent b4675d978f
commit 82837304ec
6 changed files with 191 additions and 16 deletions

View File

@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 5.0 ## Changes in Zulip 5.0
**Feature level 120**
* [`GET /messages/{message_id}`](/api/get-message): This endpoint
now sends the full message details. Previously, it only returned
the message's raw Markdown content.
**Feature level 119** **Feature level 119**
* [`POST /register`](/api/register-queue): The `unread_msgs` section * [`POST /register`](/api/register-queue): The `unread_msgs` section

View File

@ -9,7 +9,7 @@
* [Add an emoji reaction](/api/add-reaction) * [Add an emoji reaction](/api/add-reaction)
* [Remove an emoji reaction](/api/remove-reaction) * [Remove an emoji reaction](/api/remove-reaction)
* [Render a message](/api/render-message) * [Render a message](/api/render-message)
* [Get a message's raw Markdown](/api/get-raw-message) * [Fetch a single message](/api/get-message)
* [Check if messages match narrow](/api/check-messages-match-narrow) * [Check if messages match narrow](/api/check-messages-match-narrow)
* [Get a message's edit history](/api/get-message-history) * [Get a message's edit history](/api/get-message-history)
* [Update personal message flags](/api/update-message-flags) * [Update personal message flags](/api/update-message-flags)

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 119 API_FEATURE_LEVEL = 120
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -5524,20 +5524,37 @@ paths:
} }
/messages/{message_id}: /messages/{message_id}:
get: get:
operationId: get-raw-message operationId: get-message
summary: Get a message's raw Markdown summary: Fetch a single message.
tags: ["messages"] tags: ["messages"]
description: | description: |
Get the raw content of a message. Given a message ID, return the message object.
`GET {{ api_url }}/v1/messages/{msg_id}` `GET {{ api_url }}/v1/messages/{msg_id}`
This is a rarely-used endpoint relevant for clients that primarily Additionally, a `raw_content` field is included. This field is
work with HTML-rendered messages but might need to occasionally fetch useful for clients that primarily work with HTML-rendered
the message's raw Markdown (e.g. for pre-filling a message-editing messages but might need to occasionally fetch the message's
UI). raw Markdown (e.g. for [view
source](/help/view-the-markdown-source-of-a-message) or
prefilling a message edit textarea).
**Changes**: Before Zulip 5.0 (feature level 120), this
endpoint only returned the `raw_content` field.
parameters: parameters:
- $ref: "#/components/parameters/MessageId" - $ref: "#/components/parameters/MessageId"
- name: apply_markdown
in: query
description: |
If `true`, message content is returned in the rendered HTML
format. If `false`, message content is returned in the raw
Markdown-format text that user entered.
**Changes**: New in Zulip 5.0 (feature level 120).
schema:
type: boolean
default: true
example: false
responses: responses:
"200": "200":
description: Success. description: Success.
@ -5553,13 +5570,99 @@ paths:
msg: {} msg: {}
raw_content: raw_content:
type: string type: string
deprecated: true
description: | description: |
The raw content of the message. The raw Markdown content of the message.
**Deprecated** and to be removed once no longer required for
legacy clients. Modern clients should prefer passing
`apply_markdown=false` to request raw message content.
message:
description: |
An object containing details of the message.
**Changes**: New in Zulip 5.0 (feature level 120).
allOf:
- $ref: "#/components/schemas/MessagesBase"
- additionalProperties: false
properties:
avatar_url:
nullable: true
client: {}
content: {}
content_type: {}
display_recipient: {}
edit_history: {}
id: {}
is_me_message: {}
last_edit_timestamp: {}
reactions: {}
recipient_id: {}
sender_email: {}
sender_full_name: {}
sender_id: {}
sender_realm_str: {}
stream_id: {}
subject: {}
submessages: {}
timestamp: {}
topic_links: {}
type: {}
flags:
type: array
description: |
The user's [message flags][message-flags] for the message.
[message-flags]: /api/update-message-flags#available-flags
items:
type: string
example: example:
{ {
"raw_content": "**Don't** forget your towel!", "raw_content": "**Don't** forget your towel!",
"result": "success", "result": "success",
"msg": "", "msg": "",
"message":
{
"subject": "",
"sender_realm_str": "zulip",
"type": "private",
"content": "<p>Security experts agree that relational algorithms are an interesting new topic in the field of networking, and scholars concur.</p>",
"flags": ["read"],
"id": 16,
"display_recipient":
[
{
"id": 4,
"is_mirror_dummy": false,
"email": "hamlet@zulip.com",
"full_name": "King Hamlet",
},
{
"id": 5,
"is_mirror_dummy": false,
"email": "iago@zulip.com",
"full_name": "Iago",
},
{
"id": 8,
"is_mirror_dummy": false,
"email": "prospero@zulip.com",
"full_name": "Prospero from The Tempest",
},
],
"content_type": "text/html",
"is_me_message": false,
"timestamp": 1527921326,
"sender_id": 4,
"sender_full_name": "King Hamlet",
"recipient_id": 27,
"topic_links": [],
"client": "populate_db",
"avatar_url": "https://secure.gravatar.com/avatar/6d8cad0fd00256e7b40691d27ddfd466?d=identicon&version=1",
"submessages": [],
"sender_email": "hamlet@zulip.com",
"reactions": [],
},
} }
"400": "400":
description: Bad request. description: Bad request.

View File

@ -304,18 +304,57 @@ class EditMessageTest(EditMessageTestCase):
self.assert_json_success(result) self.assert_json_success(result)
self.check_topic(msg_id, topic_name="edited") self.check_topic(msg_id, topic_name="edited")
def test_fetch_raw_message(self) -> None: def test_fetch_message_from_id(self) -> None:
self.login("hamlet") self.login("hamlet")
msg_id = self.send_personal_message( msg_id = self.send_personal_message(
from_user=self.example_user("hamlet"), from_user=self.example_user("hamlet"),
to_user=self.example_user("cordelia"), to_user=self.example_user("cordelia"),
content="**before** edit", content="Personal message",
) )
result = self.client_get(f"/json/messages/{msg_id}") result = self.client_get("/json/messages/" + str(msg_id))
self.assert_json_success(result) self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "**before** edit") self.assertEqual(result.json()["raw_content"], "Personal message")
self.assertEqual(result.json()["message"]["id"], msg_id)
# Send message to web public stream where hamlet is not subscribed.
# This will test case of user having no `UserMessage` but having access
# to message.
web_public_stream = self.make_stream("web-public-stream", is_web_public=True)
self.subscribe(self.example_user("cordelia"), web_public_stream.name)
web_public_stream_msg_id = self.send_stream_message(
self.example_user("cordelia"), web_public_stream.name, content="web-public message"
)
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
self.assertEqual(result.json()["message"]["id"], web_public_stream_msg_id)
# Spectator should be able to fetch message in web public stream.
self.logout()
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
self.assertEqual(result.json()["message"]["id"], web_public_stream_msg_id)
# Verify default is apply_markdown=True
self.assertEqual(result.json()["message"]["content"], "<p>web-public message</p>")
# Verify apply_markdown=False works correctly.
result = self.client_get(
"/json/messages/" + str(web_public_stream_msg_id), {"apply_markdown": "false"}
)
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
self.assertEqual(result.json()["message"]["content"], "web-public message")
with self.settings(WEB_PUBLIC_STREAMS_ENABLED=False):
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", status_code=401
)
# Test error cases # Test error cases
self.login("hamlet")
result = self.client_get("/json/messages/999999") result = self.client_get("/json/messages/999999")
self.assert_json_error(result, "Invalid message(s)") self.assert_json_error(result, "Invalid message(s)")

View File

@ -12,7 +12,7 @@ from zerver.context_processors import get_valid_realm_from_request
from zerver.lib.actions import check_update_message, do_delete_messages from zerver.lib.actions import check_update_message, do_delete_messages
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.html_diff import highlight_html_differences from zerver.lib.html_diff import highlight_html_differences
from zerver.lib.message import access_message, access_web_public_message from zerver.lib.message import access_message, access_web_public_message, messages_for_ids
from zerver.lib.request import REQ, RequestNotes, has_request_variables from zerver.lib.request import REQ, RequestNotes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
@ -190,6 +190,7 @@ def json_fetch_raw_message(
request: HttpRequest, request: HttpRequest,
maybe_user_profile: Union[UserProfile, AnonymousUser], maybe_user_profile: Union[UserProfile, AnonymousUser],
message_id: int = REQ(converter=to_non_negative_int, path_only=True), message_id: int = REQ(converter=to_non_negative_int, path_only=True),
apply_markdown: bool = REQ(json_validator=check_bool, default=True),
) -> HttpResponse: ) -> HttpResponse:
if not maybe_user_profile.is_authenticated: if not maybe_user_profile.is_authenticated:
@ -198,4 +199,30 @@ def json_fetch_raw_message(
else: else:
(message, user_message) = access_message(maybe_user_profile, message_id) (message, user_message) = access_message(maybe_user_profile, message_id)
return json_success(request, data={"raw_content": message.content}) flags = ["read"]
if not maybe_user_profile.is_authenticated:
allow_edit_history = realm.allow_edit_history
else:
if user_message:
flags = user_message.flags_list()
allow_edit_history = maybe_user_profile.realm.allow_edit_history
# Security note: It's important that we call this only with a
# message already fetched via `access_message` type methods,
# as we do above.
message_dict_list = messages_for_ids(
message_ids=[message.id],
user_message_flags={message_id: flags},
search_fields={},
apply_markdown=apply_markdown,
client_gravatar=True,
allow_edit_history=allow_edit_history,
)
response = dict(
message=message_dict_list[0],
# raw_content is deprecated; we will need to wait until
# clients have been fully migrated to using the modern API
# before removing this, probably in 2023.
raw_content=message.content,
)
return json_success(request, response)