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
|
## 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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_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(
|
||||||
|
|
Loading…
Reference in New Issue