mirror of https://github.com/zulip/zulip.git
scheduled_messages: Add endpoints to fetch and delete them.
This commit is contained in:
parent
c0ef1c360a
commit
a06f3d26d0
|
@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
|
|||
|
||||
## 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**
|
||||
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message): Topic editing
|
||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
|
|||
# Changes should be accompanied by documentation explaining what the
|
||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||
# 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
|
||||
# only when going from an old version of the code to a newer version. Bump
|
||||
|
|
|
@ -4971,6 +4971,91 @@ paths:
|
|||
- description: |
|
||||
JSON response for when no draft exists with the provided ID.
|
||||
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:
|
||||
post:
|
||||
operationId: add-default-stream
|
||||
|
@ -17079,6 +17164,64 @@ components:
|
|||
- to
|
||||
- topic
|
||||
- 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:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/UserBase"
|
||||
|
|
|
@ -1520,6 +1520,74 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(new_defer_until))
|
||||
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):
|
||||
def assert_stream_message(
|
||||
self, stream_name: str, topic_name: str = "test topic", content: str = "test content"
|
||||
|
|
|
@ -921,6 +921,7 @@ class OpenAPIAttributesTest(ZulipTestCase):
|
|||
"messages",
|
||||
"drafts",
|
||||
"webhooks",
|
||||
"scheduled_messages",
|
||||
]
|
||||
paths = OpenAPISpec(OPENAPI_SPEC_PATH).openapi()["paths"]
|
||||
for path, path_item in paths.items():
|
||||
|
|
|
@ -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)
|
|
@ -136,6 +136,7 @@ from zerver.views.report import (
|
|||
report_send_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.storage import get_storage, remove_storage, update_storage
|
||||
from zerver.views.streams import (
|
||||
|
@ -321,6 +322,9 @@ v1_api_and_json_patterns = [
|
|||
# Endpoints for syncing drafts.
|
||||
rest_path("drafts", GET=fetch_drafts, POST=create_drafts),
|
||||
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*
|
||||
# GET returns messages, possibly filtered, POST sends a message
|
||||
rest_path(
|
||||
|
|
Loading…
Reference in New Issue