scheduled_messages: Add endpoints to fetch and delete them.

This commit is contained in:
Aman Agrawal 2023-04-14 19:19:46 +00:00 committed by Tim Abbott
parent c0ef1c360a
commit a06f3d26d0
7 changed files with 285 additions and 1 deletions

View File

@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 7.0 ## Changes in Zulip 7.0
**Feature level 173**:
* [`GET /scheduled_messages`](/api/get-scheduled-messages), [`DELETE
/scheduled_messages/<int:scheduled_message_id>`](/api/delete-scheduled-message):
Added new endpoints to fetch and delete scheduled messages.
**Feature level 172** **Feature level 172**
* [`PATCH /messages/{message_id}`](/api/update-message): Topic editing * [`PATCH /messages/{message_id}`](/api/update-message): Topic editing

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 api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 172 API_FEATURE_LEVEL = 173
# 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

@ -4971,6 +4971,91 @@ paths:
- description: | - description: |
JSON response for when no draft exists with the provided ID. JSON response for when no draft exists with the provided ID.
example: {"result": "error", "msg": "Draft does not exist"} example: {"result": "error", "msg": "Draft does not exist"}
/scheduled_messages:
get:
operationId: get-scheduled-messages
tags: ["scheduled_messages"]
summary: Get scheduled messages
description: |
Fetch all scheduled messages for the current user.
Scheduled messages are messages the user has scheduled to be
sent in the future via the send later feature.
**Changes**: New in Zulip 7.0 (feature level 173).
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- $ref: "#/components/schemas/SuccessDescription"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
scheduled_messages:
type: array
description: |
Returns all of the current user's scheduled
messages, ordered by scheduled timestamp (ascending).
items:
$ref: "#/components/schemas/ScheduledMessage"
example:
{
"result": "success",
"msg": "",
"scheduled_messages":
[
{
"message_id": 27,
"to": [14],
"type": "stream",
"content": "Hi",
"rendered_content": "<p>Hi</p>",
"topic": "Introduction",
"deliver_at": 1681662420000,
},
],
}
/scheduled_messages/{scheduled_message_id}:
delete:
operationId: delete-scheduled-message
tags: ["scheduled_messages"]
summary: Delete a scheduled message.
description: |
Delete/cancel a previously scheduled message.
**Changes**: New in Zulip 7.0 (feature level 173).
parameters:
- name: scheduled_message_id
in: path
schema:
type: integer
description: |
The ID of the scheduled message you want to delete.
required: True
example: 1
responses:
"200":
$ref: "#/components/responses/SimpleSuccess"
"404":
description: Not Found.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonError"
- description: |
Example response for when no scheduled message exists with the provided ID.
example:
{
"result": "error",
"msg": "Scheduled message does not exist",
}
/default_streams: /default_streams:
post: post:
operationId: add-default-stream operationId: add-default-stream
@ -17079,6 +17164,64 @@ components:
- to - to
- topic - topic
- content - content
ScheduledMessage:
type: object
description: |
A dictionary for representing a scheduled message.
properties:
message_id:
type: integer
description: |
The unique ID of the scheduled message. It can be used to modify and
delete the scheduled message.
type:
type: string
description: |
The type of the scheduled message. Either unaddressed (empty string), "stream",
or "private" (for PMs and private group messages).
enum:
- ""
- stream
- private
to:
type: array
description: |
An array of the tentative target audience IDs. For "stream"
messages, this should contain exactly 1 ID, the ID of the
target stream. For private messages, this should be an array
of target user IDs. This cannot be an empty array since
scheduled messages are always addressed.
items:
type: integer
topic:
type: string
description: |
For stream scheduled message, the tentative topic name. For private messages,
this will be ignored and should ideally
be the empty string. Should not contain null bytes.
content:
type: string
description: |
The body of the scheduled message. Should not contain null bytes.
rendered_content:
type: string
description: |
The body of the scheduled message rendered in HTML.
deliver_at:
type: number
description: |
A Unix timestamp (seconds only) representing when the scheduled
message will be sent by the server.
example: 1595479019
additionalProperties: false
required:
- message_id
- type
- to
- topic
- content
- rendered_content
- deliver_at
User: User:
allOf: allOf:
- $ref: "#/components/schemas/UserBase" - $ref: "#/components/schemas/UserBase"

View File

@ -1520,6 +1520,74 @@ class ScheduledMessageTest(ZulipTestCase):
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(new_defer_until)) self.assertEqual(message.scheduled_timestamp, convert_to_UTC(new_defer_until))
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER) self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
def test_fetch_scheduled_messages(self) -> None:
self.login("hamlet")
# No scheduled message
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)
content = "Test message"
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
defer_until_str = str(defer_until)
self.do_schedule_message("stream", "Verona", content, defer_until_str)
# Single scheduled message
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
scheduled_messages = orjson.loads(result.content)["scheduled_messages"]
self.assert_length(scheduled_messages, 1)
self.assertEqual(scheduled_messages[0]["message_id"], self.last_scheduled_message().id)
self.assertEqual(scheduled_messages[0]["content"], content)
self.assertEqual(scheduled_messages[0]["to"], [self.get_stream_id("Verona")])
self.assertEqual(scheduled_messages[0]["type"], "stream")
self.assertEqual(scheduled_messages[0]["topic"], "Test topic")
self.assertEqual(
scheduled_messages[0]["deliver_at"], int(convert_to_UTC(defer_until).timestamp() * 1000)
)
othello = self.example_user("othello")
result = self.do_schedule_message(
"private", [othello.email], content + " 3", defer_until_str
)
# Multiple scheduled messages
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 2)
# Check if another user can access these scheduled messages.
self.logout()
self.login("othello")
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)
def test_delete_scheduled_messages(self) -> None:
self.login("hamlet")
content = "Test message"
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
defer_until_str = str(defer_until)
self.do_schedule_message("stream", "Verona", content, defer_until_str)
message = self.last_scheduled_message()
self.logout()
# Other user cannot delete it.
othello = self.example_user("othello")
result = self.api_delete(othello, f"/api/v1/scheduled_messages/{message.id}")
self.assert_json_error(result, "Scheduled message does not exist", 404)
self.login("hamlet")
result = self.client_delete(f"/json/scheduled_messages/{message.id}")
self.assert_json_success(result)
# Already deleted.
result = self.client_delete(f"/json/scheduled_messages/{message.id}")
self.assert_json_error(result, "Scheduled message does not exist", 404)
class StreamMessagesTest(ZulipTestCase): class StreamMessagesTest(ZulipTestCase):
def assert_stream_message( def assert_stream_message(
self, stream_name: str, topic_name: str = "test topic", content: str = "test content" self, stream_name: str, topic_name: str = "test topic", content: str = "test content"

View File

@ -921,6 +921,7 @@ class OpenAPIAttributesTest(ZulipTestCase):
"messages", "messages",
"drafts", "drafts",
"webhooks", "webhooks",
"scheduled_messages",
] ]
paths = OpenAPISpec(OPENAPI_SPEC_PATH).openapi()["paths"] paths = OpenAPISpec(OPENAPI_SPEC_PATH).openapi()["paths"]
for path, path_item in paths.items(): for path, path_item in paths.items():

View File

@ -0,0 +1,62 @@
from typing import List, TypedDict
from django.http import HttpRequest, HttpResponse
from zerver.lib.request import has_request_variables
from zerver.lib.response import json_success
from zerver.lib.scheduled_messages import access_scheduled_message
from zerver.models import ScheduledMessage, UserProfile, get_recipient_ids
from zerver.tornado.django_api import send_event
class ScheduledMessageDict(TypedDict):
message_id: int
to: List[int]
type: str
content: str
rendered_content: str
topic: str
deliver_at: int
@has_request_variables
def fetch_scheduled_messages(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
scheduled_messages = ScheduledMessage.objects.filter(
sender=user_profile, delivered=False, delivery_type=ScheduledMessage.SEND_LATER
).order_by("scheduled_timestamp")
scheduled_message_dicts: List[ScheduledMessageDict] = []
for scheduled_message in scheduled_messages:
recipient, recipient_type_str = get_recipient_ids(
scheduled_message.recipient, user_profile.id
)
msg_to_dict: ScheduledMessageDict = {
"message_id": scheduled_message.id,
"to": recipient,
"type": recipient_type_str,
"content": scheduled_message.content,
"rendered_content": scheduled_message.rendered_content,
"topic": scheduled_message.topic_name(),
"deliver_at": int(scheduled_message.scheduled_timestamp.timestamp() * 1000),
}
scheduled_message_dicts.append(msg_to_dict)
return json_success(request, data={"scheduled_messages": scheduled_message_dicts})
@has_request_variables
def delete_scheduled_messages(
request: HttpRequest, user_profile: UserProfile, scheduled_message_id: int
) -> HttpResponse:
scheduled_message_object = access_scheduled_message(user_profile, scheduled_message_id)
scheduled_message_id = scheduled_message_object.id
scheduled_message_object.delete()
event = {
"type": "scheduled_message",
"op": "remove",
"scheduled_message_id": scheduled_message_id,
}
send_event(user_profile.realm, event, [user_profile.id])
return json_success(request)

View File

@ -136,6 +136,7 @@ from zerver.views.report import (
report_send_times, report_send_times,
report_unnarrow_times, report_unnarrow_times,
) )
from zerver.views.scheduled_messages import delete_scheduled_messages, fetch_scheduled_messages
from zerver.views.sentry import sentry_tunnel from zerver.views.sentry import sentry_tunnel
from zerver.views.storage import get_storage, remove_storage, update_storage from zerver.views.storage import get_storage, remove_storage, update_storage
from zerver.views.streams import ( from zerver.views.streams import (
@ -321,6 +322,9 @@ v1_api_and_json_patterns = [
# Endpoints for syncing drafts. # Endpoints for syncing drafts.
rest_path("drafts", GET=fetch_drafts, POST=create_drafts), rest_path("drafts", GET=fetch_drafts, POST=create_drafts),
rest_path("drafts/<int:draft_id>", PATCH=edit_draft, DELETE=delete_draft), rest_path("drafts/<int:draft_id>", PATCH=edit_draft, DELETE=delete_draft),
# New scheduled messages are created via send_message_backend.
rest_path("scheduled_messages", GET=fetch_scheduled_messages),
rest_path("scheduled_messages/<int:scheduled_message_id>", DELETE=delete_scheduled_messages),
# messages -> zerver.views.message* # messages -> zerver.views.message*
# GET returns messages, possibly filtered, POST sends a message # GET returns messages, possibly filtered, POST sends a message
rest_path( rest_path(