mirror of https://github.com/zulip/zulip.git
urls: Add new endpoint to create scheduled messages.
This will help us remove scheduled message and reminder logic from `/messages` code path. Removes `deliver_at`/`defer_until` and `tz_guess` parameters. And adds the `scheduled_delivery_timestamp` instead. Also updates the scheduled message dicts to return `scheduled_delivery_timestamp`. Also, revises some text in `/delete-scheduled-message` endpoint and in the `ScheduledMessage` schema in the API documentation.
This commit is contained in:
parent
7739703111
commit
d60d6e9115
|
@ -4288,7 +4288,7 @@ class StreamScheduledMessageAPI(TypedDict):
|
|||
content: str
|
||||
rendered_content: str
|
||||
topic: str
|
||||
deliver_at: int
|
||||
scheduled_delivery_timestamp: int
|
||||
|
||||
|
||||
class DirectScheduledMessageAPI(TypedDict):
|
||||
|
@ -4297,7 +4297,7 @@ class DirectScheduledMessageAPI(TypedDict):
|
|||
type: str
|
||||
content: str
|
||||
rendered_content: str
|
||||
deliver_at: int
|
||||
scheduled_delivery_timestamp: int
|
||||
|
||||
|
||||
class ScheduledMessage(models.Model):
|
||||
|
@ -4348,7 +4348,7 @@ class ScheduledMessage(models.Model):
|
|||
type=recipient_type_str,
|
||||
content=self.content,
|
||||
rendered_content=self.rendered_content,
|
||||
deliver_at=int(self.scheduled_timestamp.timestamp() * 1000),
|
||||
scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp),
|
||||
)
|
||||
|
||||
# The recipient for stream messages should always just be the unique stream ID.
|
||||
|
@ -4361,7 +4361,7 @@ class ScheduledMessage(models.Model):
|
|||
content=self.content,
|
||||
rendered_content=self.rendered_content,
|
||||
topic=self.topic_name(),
|
||||
deliver_at=int(self.scheduled_timestamp.timestamp() * 1000),
|
||||
scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -5021,8 +5021,9 @@ paths:
|
|||
scheduled_messages:
|
||||
type: array
|
||||
description: |
|
||||
Returns all of the current user's scheduled
|
||||
messages, ordered by scheduled timestamp (ascending).
|
||||
Returns all of the current user's undelivered scheduled
|
||||
messages, ordered by `scheduled_delivery_timestamp`
|
||||
(ascending).
|
||||
items:
|
||||
$ref: "#/components/schemas/ScheduledMessage"
|
||||
example:
|
||||
|
@ -5033,7 +5034,7 @@ paths:
|
|||
[
|
||||
{
|
||||
"scheduled_message_id": 27,
|
||||
"to": [14],
|
||||
"to": 14,
|
||||
"type": "stream",
|
||||
"content": "Hi",
|
||||
"rendered_content": "<p>Hi</p>",
|
||||
|
@ -5042,13 +5043,160 @@ paths:
|
|||
},
|
||||
],
|
||||
}
|
||||
post:
|
||||
operationId: create-or-update-scheduled-messages
|
||||
tags: ["scheduled_messages"]
|
||||
summary: Create or edit scheduled messages
|
||||
description: |
|
||||
Create a new scheduled message or edit an existing scheduled message.
|
||||
|
||||
The `scheduled_message_id` parameter determines whether a scheduled
|
||||
message is created or updated. When it is omitted, a new scheduled
|
||||
message is created. When it is specified, the existing scheduled
|
||||
message with that unique ID is updated for the values passed to the
|
||||
other endpoint parameters.
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
description: |
|
||||
The type of scheduled message to be sent. `"direct"` for a direct
|
||||
message and `"stream"` for a stream message.
|
||||
|
||||
In Zulip 7.0 (feature level 174), `"direct"` was added as the
|
||||
preferred way to indicate the type of a direct message, deprecating
|
||||
the original `"private"`. While `"private"` is supported for
|
||||
scheduling direct messages, clients are encouraged to use to the
|
||||
modern convention because support for `"private"` may eventually
|
||||
be removed.
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- direct
|
||||
- stream
|
||||
- private
|
||||
example: direct
|
||||
required: true
|
||||
- name: to
|
||||
in: query
|
||||
description: |
|
||||
The scheduled message's tentative target audience.
|
||||
|
||||
For stream messages, integer ID of the stream. For direct messages,
|
||||
either a list containing integer user IDs or a list containing string
|
||||
email addresses.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
- type: array
|
||||
items:
|
||||
type: integer
|
||||
minLength: 1
|
||||
example: [9, 10]
|
||||
required: true
|
||||
- $ref: "#/components/parameters/RequiredContent"
|
||||
- name: topic
|
||||
in: query
|
||||
description: |
|
||||
The topic of the message. Only required for stream messages
|
||||
(`type="stream"`).
|
||||
|
||||
Clients should use the `max_topic_length` returned by the
|
||||
[`POST /register`](/api/register-queue) endpoint to determine
|
||||
the maximum topic length.
|
||||
schema:
|
||||
type: string
|
||||
example: Castle
|
||||
allowEmptyValue: true
|
||||
- name: scheduled_message_id
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: |
|
||||
The unique ID of the scheduled message to be updated.
|
||||
|
||||
If omitted, a new scheduled message will be created. Otherwise,
|
||||
the existing scheduled message with this unique ID will be updated.
|
||||
example: 1
|
||||
- name: scheduled_delivery_timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: |
|
||||
The UNIX timestamp for when the message will be sent,
|
||||
in UTC seconds.
|
||||
required: true
|
||||
example: 3165826990
|
||||
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_message_id:
|
||||
type: integer
|
||||
description: |
|
||||
The unique ID of the scheduled message.
|
||||
|
||||
This is different from the unique ID that the message will have
|
||||
after it is sent.
|
||||
example:
|
||||
{
|
||||
"msg": "",
|
||||
"scheduled_message_id": 42,
|
||||
"result": "success",
|
||||
}
|
||||
"400":
|
||||
description: Bad request.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/NonExistingStreamError"
|
||||
- description: |
|
||||
A typical failed JSON response for when a stream message is sent to a stream
|
||||
that does not exist:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"msg": "Invalid email 'eeshan@zulip.com'",
|
||||
"result": "error",
|
||||
}
|
||||
description: |
|
||||
A typical failed JSON response for when a private message is sent to a user
|
||||
that does not exist:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/JsonError"
|
||||
- description: |
|
||||
Example response for when no scheduled message exists with the provided ID.
|
||||
example:
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"result": "error",
|
||||
"msg": "Scheduled message does not exist",
|
||||
}
|
||||
/scheduled_messages/{scheduled_message_id}:
|
||||
delete:
|
||||
operationId: delete-scheduled-message
|
||||
tags: ["scheduled_messages"]
|
||||
summary: Delete a scheduled message.
|
||||
summary: Delete a scheduled message
|
||||
description: |
|
||||
Delete/cancel a previously scheduled message.
|
||||
Delete, and therefore cancel sending, a previously scheduled message.
|
||||
|
||||
**Changes**: New in Zulip 7.0 (feature level 173).
|
||||
parameters:
|
||||
|
@ -5057,7 +5205,10 @@ paths:
|
|||
schema:
|
||||
type: integer
|
||||
description: |
|
||||
The ID of the scheduled message you want to delete.
|
||||
The ID of the scheduled message to delete.
|
||||
|
||||
This is different from the unique ID that a message would have
|
||||
after it was sent.
|
||||
required: True
|
||||
example: 1
|
||||
responses:
|
||||
|
@ -5074,6 +5225,7 @@ paths:
|
|||
Example response for when no scheduled message exists with the provided ID.
|
||||
example:
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"result": "error",
|
||||
"msg": "Scheduled message does not exist",
|
||||
}
|
||||
|
@ -17340,21 +17492,21 @@ components:
|
|||
ScheduledMessage:
|
||||
type: object
|
||||
description: |
|
||||
A dictionary for representing a scheduled message.
|
||||
Object containing details of the scheduled message.
|
||||
properties:
|
||||
scheduled_message_id:
|
||||
type: integer
|
||||
description: |
|
||||
The unique ID of the scheduled message. It can be used to modify and
|
||||
delete the scheduled message. This ID is different than the ID of the
|
||||
message that will actually be sent.
|
||||
The unique ID of the scheduled message, which can be used to
|
||||
modify or delete the scheduled message.
|
||||
|
||||
This is different from the unique ID that the message will have
|
||||
after it is sent.
|
||||
type:
|
||||
type: string
|
||||
description: |
|
||||
The type of the scheduled message. Either unaddressed (empty string), "stream",
|
||||
or "private" (for PMs and private group messages).
|
||||
The type of the scheduled message. Either `"stream"` or `"private"`.
|
||||
enum:
|
||||
- ""
|
||||
- stream
|
||||
- private
|
||||
to:
|
||||
|
@ -17378,16 +17530,16 @@ components:
|
|||
content:
|
||||
type: string
|
||||
description: |
|
||||
The body of the scheduled message. Should not contain null bytes.
|
||||
The content/body of the scheduled message, in text/markdown format.
|
||||
rendered_content:
|
||||
type: string
|
||||
description: |
|
||||
The body of the scheduled message rendered in HTML.
|
||||
deliver_at:
|
||||
type: number
|
||||
The content/body of the scheduled message rendered in HTML.
|
||||
scheduled_delivery_timestamp:
|
||||
type: integer
|
||||
description: |
|
||||
A Unix timestamp (seconds only) representing when the scheduled
|
||||
message will be sent by the server.
|
||||
The UNIX timestamp for when the message will be sent
|
||||
by the server, in UTC seconds.
|
||||
example: 1595479019
|
||||
additionalProperties: false
|
||||
required:
|
||||
|
@ -17396,7 +17548,7 @@ components:
|
|||
- to
|
||||
- content
|
||||
- rendered_content
|
||||
- deliver_at
|
||||
- scheduled_delivery_timestamp
|
||||
User:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/UserBase"
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import datetime
|
||||
import sys
|
||||
import time
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
import orjson
|
||||
from django.utils.timezone import now as timezone_now
|
||||
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.timestamp import convert_to_UTC
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import ScheduledMessage
|
||||
|
||||
if sys.version_info < (3, 9): # nocoverage
|
||||
from backports import zoneinfo
|
||||
else: # nocoverage
|
||||
import zoneinfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
||||
|
||||
|
@ -28,11 +21,9 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
def do_schedule_message(
|
||||
self,
|
||||
msg_type: str,
|
||||
to: Union[str, int, List[str], List[int]],
|
||||
to: Union[int, List[str], List[int]],
|
||||
msg: str,
|
||||
defer_until: str = "",
|
||||
tz_guess: str = "",
|
||||
delivery_type: str = "send_later",
|
||||
scheduled_delivery_timestamp: int,
|
||||
scheduled_message_id: str = "",
|
||||
) -> "TestHttpResponse":
|
||||
self.login("hamlet")
|
||||
|
@ -46,158 +37,98 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
"to": orjson.dumps(to).decode(),
|
||||
"content": msg,
|
||||
"topic": topic_name,
|
||||
"delivery_type": delivery_type,
|
||||
"tz_guess": tz_guess,
|
||||
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
|
||||
}
|
||||
if defer_until:
|
||||
payload["deliver_at"] = defer_until
|
||||
|
||||
if scheduled_message_id:
|
||||
payload["scheduled_message_id"] = scheduled_message_id
|
||||
|
||||
# `Topic` cannot be empty according to OpenAPI specification.
|
||||
intentionally_undocumented: bool = topic_name == ""
|
||||
result = self.client_post(
|
||||
"/json/messages", payload, intentionally_undocumented=intentionally_undocumented
|
||||
)
|
||||
result = self.client_post("/json/scheduled_messages", payload)
|
||||
return result
|
||||
|
||||
def test_schedule_message(self) -> None:
|
||||
content = "Test message"
|
||||
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
|
||||
defer_until_str = str(defer_until)
|
||||
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||
verona_stream_id = self.get_stream_id("Verona")
|
||||
|
||||
# Scheduling a message to a stream you are subscribed is successful.
|
||||
result = self.do_schedule_message("stream", "Verona", content + " 1", defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Test message 1")
|
||||
self.assertEqual(message.rendered_content, "<p>Test message 1</p>")
|
||||
self.assertEqual(message.topic_name(), "Test topic")
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
# Scheduling a message for reminders.
|
||||
result = self.do_schedule_message(
|
||||
"stream", "Verona", content + " 2", defer_until_str, delivery_type="remind"
|
||||
"stream", verona_stream_id, content + " 1", scheduled_delivery_timestamp
|
||||
)
|
||||
message = self.last_scheduled_message()
|
||||
scheduled_message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.REMIND)
|
||||
self.assertEqual(scheduled_message.content, "Test message 1")
|
||||
self.assertEqual(scheduled_message.rendered_content, "<p>Test message 1</p>")
|
||||
self.assertEqual(scheduled_message.topic_name(), "Test topic")
|
||||
self.assertEqual(
|
||||
scheduled_message.scheduled_timestamp,
|
||||
timestamp_to_datetime(scheduled_delivery_timestamp),
|
||||
)
|
||||
|
||||
# Scheduling a private message is successful.
|
||||
othello = self.example_user("othello")
|
||||
hamlet = self.example_user("hamlet")
|
||||
result = self.do_schedule_message(
|
||||
"private", [othello.email], content + " 3", defer_until_str
|
||||
"direct", [othello.email], content + " 3", scheduled_delivery_timestamp
|
||||
)
|
||||
message = self.last_scheduled_message()
|
||||
scheduled_message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Test message 3")
|
||||
self.assertEqual(message.rendered_content, "<p>Test message 3</p>")
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
|
||||
# Setting a reminder in PM's to other users causes a error.
|
||||
result = self.do_schedule_message(
|
||||
"private", [othello.email], content + " 4", defer_until_str, delivery_type="remind"
|
||||
self.assertEqual(scheduled_message.content, "Test message 3")
|
||||
self.assertEqual(scheduled_message.rendered_content, "<p>Test message 3</p>")
|
||||
self.assertEqual(
|
||||
scheduled_message.scheduled_timestamp,
|
||||
timestamp_to_datetime(scheduled_delivery_timestamp),
|
||||
)
|
||||
self.assert_json_error(result, "Reminders can only be set for streams.")
|
||||
|
||||
# Setting a reminder in PM's to ourself is successful.
|
||||
# Required by reminders from message actions popover caret feature.
|
||||
result = self.do_schedule_message(
|
||||
"private", [hamlet.email], content + " 5", defer_until_str, delivery_type="remind"
|
||||
)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Test message 5")
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.REMIND)
|
||||
|
||||
# Scheduling a message while guessing time zone.
|
||||
tz_guess = "Asia/Kolkata"
|
||||
result = self.do_schedule_message(
|
||||
"stream", "Verona", content + " 6", defer_until_str, tz_guess=tz_guess
|
||||
)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Test message 6")
|
||||
local_tz = zoneinfo.ZoneInfo(tz_guess)
|
||||
utz_defer_until = defer_until.replace(tzinfo=local_tz)
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(utz_defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
|
||||
# Test with users time zone setting as set to some time zone rather than
|
||||
# empty. This will help interpret timestamp in users local time zone.
|
||||
user = self.example_user("hamlet")
|
||||
user.timezone = "US/Pacific"
|
||||
user.save(update_fields=["timezone"])
|
||||
result = self.do_schedule_message("stream", "Verona", content + " 7", defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Test message 7")
|
||||
local_tz = zoneinfo.ZoneInfo(user.timezone)
|
||||
utz_defer_until = defer_until.replace(tzinfo=local_tz)
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(utz_defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
|
||||
def test_scheduling_in_past(self) -> None:
|
||||
# Scheduling a message in past should fail.
|
||||
content = "Test message"
|
||||
defer_until = timezone_now()
|
||||
defer_until_str = str(defer_until)
|
||||
verona_stream_id = self.get_stream_id("Verona")
|
||||
scheduled_delivery_timestamp = int(time.time() - 86400)
|
||||
|
||||
result = self.do_schedule_message("stream", "Verona", content + " 1", defer_until_str)
|
||||
self.assert_json_error(result, "Time must be in the future.")
|
||||
|
||||
def test_invalid_timestamp(self) -> None:
|
||||
# Scheduling a message from which timestamp couldn't be parsed
|
||||
# successfully should fail.
|
||||
content = "Test message"
|
||||
defer_until = "Missed the timestamp"
|
||||
|
||||
result = self.do_schedule_message("stream", "Verona", content + " 1", defer_until)
|
||||
self.assert_json_error(result, "Invalid time format")
|
||||
|
||||
def test_missing_deliver_at(self) -> None:
|
||||
content = "Test message"
|
||||
|
||||
result = self.do_schedule_message("stream", "Verona", content + " 1")
|
||||
self.assert_json_error(
|
||||
result, "Missing deliver_at in a request for delayed message delivery"
|
||||
result = self.do_schedule_message(
|
||||
"stream", verona_stream_id, content + " 1", scheduled_delivery_timestamp
|
||||
)
|
||||
self.assert_json_error(result, "Scheduled delivery time must be in the future.")
|
||||
|
||||
def test_edit_schedule_message(self) -> None:
|
||||
content = "Original test message"
|
||||
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
|
||||
defer_until_str = str(defer_until)
|
||||
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||
verona_stream_id = self.get_stream_id("Verona")
|
||||
|
||||
# Scheduling a message to a stream you are subscribed is successful.
|
||||
result = self.do_schedule_message("stream", "Verona", content, defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
result = self.do_schedule_message(
|
||||
"stream", verona_stream_id, content, scheduled_delivery_timestamp
|
||||
)
|
||||
scheduled_message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, "Original test message")
|
||||
self.assertEqual(message.topic_name(), "Test topic")
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
self.assertEqual(scheduled_message.content, "Original test message")
|
||||
self.assertEqual(scheduled_message.topic_name(), "Test topic")
|
||||
self.assertEqual(
|
||||
scheduled_message.scheduled_timestamp,
|
||||
timestamp_to_datetime(scheduled_delivery_timestamp),
|
||||
)
|
||||
|
||||
# Edit content and time of scheduled message.
|
||||
edited_content = "Edited test message"
|
||||
new_defer_until = defer_until + datetime.timedelta(days=3)
|
||||
new_defer_until_str = str(new_defer_until)
|
||||
new_scheduled_delivery_timestamp = scheduled_delivery_timestamp + int(
|
||||
time.time() + (3 * 86400)
|
||||
)
|
||||
|
||||
result = self.do_schedule_message(
|
||||
"stream",
|
||||
"Verona",
|
||||
verona_stream_id,
|
||||
edited_content,
|
||||
new_defer_until_str,
|
||||
scheduled_message_id=str(message.id),
|
||||
new_scheduled_delivery_timestamp,
|
||||
scheduled_message_id=str(scheduled_message.id),
|
||||
)
|
||||
message = self.get_scheduled_message(str(message.id))
|
||||
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, edited_content)
|
||||
self.assertEqual(message.topic_name(), "Test topic")
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(new_defer_until))
|
||||
self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER)
|
||||
self.assertEqual(scheduled_message.content, edited_content)
|
||||
self.assertEqual(scheduled_message.topic_name(), "Test topic")
|
||||
self.assertEqual(
|
||||
scheduled_message.scheduled_timestamp,
|
||||
timestamp_to_datetime(new_scheduled_delivery_timestamp),
|
||||
)
|
||||
|
||||
def test_fetch_scheduled_messages(self) -> None:
|
||||
self.login("hamlet")
|
||||
|
@ -206,10 +137,10 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
self.assert_json_success(result)
|
||||
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)
|
||||
|
||||
verona_stream_id = self.get_stream_id("Verona")
|
||||
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)
|
||||
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||
self.do_schedule_message("stream", verona_stream_id, content, scheduled_delivery_timestamp)
|
||||
|
||||
# Single scheduled message
|
||||
result = self.client_get("/json/scheduled_messages")
|
||||
|
@ -221,16 +152,16 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
scheduled_messages[0]["scheduled_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]["to"], verona_stream_id)
|
||||
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)
|
||||
scheduled_messages[0]["scheduled_delivery_timestamp"], scheduled_delivery_timestamp
|
||||
)
|
||||
|
||||
othello = self.example_user("othello")
|
||||
result = self.do_schedule_message(
|
||||
"private", [othello.email], content + " 3", defer_until_str
|
||||
"direct", [othello.email], content + " 3", scheduled_delivery_timestamp
|
||||
)
|
||||
|
||||
# Multiple scheduled messages
|
||||
|
@ -249,21 +180,22 @@ class ScheduledMessageTest(ZulipTestCase):
|
|||
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()
|
||||
verona_stream_id = self.get_stream_id("Verona")
|
||||
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||
|
||||
self.do_schedule_message("stream", verona_stream_id, content, scheduled_delivery_timestamp)
|
||||
scheduled_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}")
|
||||
result = self.api_delete(othello, f"/api/v1/scheduled_messages/{scheduled_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}")
|
||||
result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
|
||||
self.assert_json_success(result)
|
||||
|
||||
# Already deleted.
|
||||
result = self.client_delete(f"/json/scheduled_messages/{message.id}")
|
||||
result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
|
||||
self.assert_json_error(result, "Scheduled message does not exist", 404)
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import sys
|
||||
from email.headerregistry import Address
|
||||
from typing import Iterable, Optional, Sequence, Union, cast
|
||||
|
||||
from dateutil.parser import parse as dateparser
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.actions.message_send import (
|
||||
|
@ -17,30 +14,22 @@ from zerver.actions.message_send import (
|
|||
extract_private_recipients,
|
||||
extract_stream_indicator,
|
||||
)
|
||||
from zerver.actions.scheduled_messages import check_schedule_message
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.message import render_markdown
|
||||
from zerver.lib.request import REQ, RequestNotes, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.timestamp import convert_to_UTC
|
||||
from zerver.lib.topic import REQ_topic
|
||||
from zerver.lib.validator import check_int, check_string_in, to_float
|
||||
from zerver.lib.validator import check_string_in, to_float
|
||||
from zerver.lib.zcommand import process_zcommands
|
||||
from zerver.lib.zephyr import compute_mit_user_fullname
|
||||
from zerver.models import (
|
||||
Client,
|
||||
Message,
|
||||
Realm,
|
||||
RealmDomain,
|
||||
UserProfile,
|
||||
get_user_including_cross_realm,
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 9): # nocoverage
|
||||
from backports import zoneinfo
|
||||
else: # nocoverage
|
||||
import zoneinfo
|
||||
|
||||
|
||||
class InvalidMirrorInputError(Exception):
|
||||
pass
|
||||
|
@ -139,56 +128,6 @@ def same_realm_jabber_user(user_profile: UserProfile, email: str) -> bool:
|
|||
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
||||
|
||||
|
||||
def handle_deferred_message(
|
||||
sender: UserProfile,
|
||||
client: Client,
|
||||
recipient_type_name: str,
|
||||
message_to: Union[Sequence[str], Sequence[int]],
|
||||
topic_name: Optional[str],
|
||||
message_content: str,
|
||||
scheduled_message_id: Optional[int],
|
||||
delivery_type: str,
|
||||
defer_until: str,
|
||||
tz_guess: Optional[str],
|
||||
forwarder_user_profile: UserProfile,
|
||||
realm: Optional[Realm],
|
||||
) -> str:
|
||||
deliver_at = None
|
||||
local_tz = "UTC"
|
||||
if tz_guess:
|
||||
local_tz = tz_guess
|
||||
elif sender.timezone:
|
||||
local_tz = sender.timezone
|
||||
try:
|
||||
deliver_at = dateparser(defer_until)
|
||||
except ValueError:
|
||||
raise JsonableError(_("Invalid time format"))
|
||||
|
||||
deliver_at_usertz = deliver_at
|
||||
if deliver_at_usertz.tzinfo is None:
|
||||
user_tz = zoneinfo.ZoneInfo(local_tz)
|
||||
deliver_at_usertz = deliver_at.replace(tzinfo=user_tz)
|
||||
deliver_at = convert_to_UTC(deliver_at_usertz)
|
||||
|
||||
if deliver_at <= timezone_now():
|
||||
raise JsonableError(_("Time must be in the future."))
|
||||
|
||||
check_schedule_message(
|
||||
sender,
|
||||
client,
|
||||
recipient_type_name,
|
||||
message_to,
|
||||
topic_name,
|
||||
message_content,
|
||||
scheduled_message_id,
|
||||
delivery_type,
|
||||
deliver_at,
|
||||
realm=realm,
|
||||
forwarder_user_profile=forwarder_user_profile,
|
||||
)
|
||||
return str(deliver_at_usertz)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
def send_message_backend(
|
||||
request: HttpRequest,
|
||||
|
@ -202,12 +141,6 @@ def send_message_backend(
|
|||
widget_content: Optional[str] = REQ(default=None, documentation_pending=True),
|
||||
local_id: Optional[str] = REQ(default=None),
|
||||
queue_id: Optional[str] = REQ(default=None),
|
||||
scheduled_message_id: Optional[int] = REQ(
|
||||
default=None, json_validator=check_int, documentation_pending=True
|
||||
),
|
||||
delivery_type: str = REQ("delivery_type", default="send_now", documentation_pending=True),
|
||||
defer_until: Optional[str] = REQ("deliver_at", default=None, documentation_pending=True),
|
||||
tz_guess: Optional[str] = REQ("tz_guess", default=None, documentation_pending=True),
|
||||
time: Optional[float] = REQ(default=None, converter=to_float, documentation_pending=True),
|
||||
) -> HttpResponse:
|
||||
recipient_type_name = req_type
|
||||
|
@ -294,26 +227,6 @@ def send_message_backend(
|
|||
raise JsonableError(_("Invalid mirrored message"))
|
||||
sender = user_profile
|
||||
|
||||
if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is None:
|
||||
raise JsonableError(_("Missing deliver_at in a request for delayed message delivery"))
|
||||
|
||||
if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is not None:
|
||||
deliver_at = handle_deferred_message(
|
||||
sender,
|
||||
client,
|
||||
recipient_type_name,
|
||||
message_to,
|
||||
topic_name,
|
||||
message_content,
|
||||
scheduled_message_id,
|
||||
delivery_type,
|
||||
defer_until,
|
||||
tz_guess,
|
||||
forwarder_user_profile=user_profile,
|
||||
realm=realm,
|
||||
)
|
||||
return json_success(request, data={"deliver_at": deliver_at})
|
||||
|
||||
ret = check_send_message(
|
||||
sender,
|
||||
client,
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
from django.http import HttpRequest, HttpResponse
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
from zerver.actions.scheduled_messages import delete_scheduled_message
|
||||
from zerver.lib.request import has_request_variables
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.actions.message_send import extract_private_recipients
|
||||
from zerver.actions.scheduled_messages import (
|
||||
check_schedule_message,
|
||||
delete_scheduled_message,
|
||||
)
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.request import REQ, RequestNotes, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.scheduled_messages import get_undelivered_scheduled_messages
|
||||
from zerver.models import UserProfile
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.lib.topic import REQ_topic
|
||||
from zerver.lib.validator import check_int, check_string_in
|
||||
from zerver.models import Message, UserProfile
|
||||
|
||||
|
||||
@has_request_variables
|
||||
|
@ -20,3 +32,51 @@ def delete_scheduled_messages(
|
|||
) -> HttpResponse:
|
||||
delete_scheduled_message(user_profile, scheduled_message_id)
|
||||
return json_success(request)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
def scheduled_messages_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
req_type: str = REQ("type", str_validator=check_string_in(Message.API_RECIPIENT_TYPES)),
|
||||
req_to: str = REQ("to"),
|
||||
topic_name: Optional[str] = REQ_topic(),
|
||||
message_content: str = REQ("content"),
|
||||
scheduled_message_id: Optional[int] = REQ(default=None, json_validator=check_int),
|
||||
scheduled_delivery_timestamp: int = REQ(json_validator=check_int),
|
||||
) -> HttpResponse:
|
||||
recipient_type_name = req_type
|
||||
if recipient_type_name == "direct":
|
||||
# For now, use "private" from Message.API_RECIPIENT_TYPES.
|
||||
# TODO: Use "direct" here, as well as in events and
|
||||
# scheduled message objects/dicts.
|
||||
recipient_type_name = "private"
|
||||
|
||||
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
|
||||
if deliver_at <= timezone_now():
|
||||
raise JsonableError(_("Scheduled delivery time must be in the future."))
|
||||
|
||||
sender = user_profile
|
||||
client = RequestNotes.get_notes(request).client
|
||||
assert client is not None
|
||||
|
||||
if recipient_type_name == "stream":
|
||||
# req_to is ID of the recipient stream.
|
||||
message_to: Union[Sequence[str], Sequence[int]] = [int(req_to)]
|
||||
else:
|
||||
message_to = extract_private_recipients(req_to)
|
||||
|
||||
scheduled_message_id = check_schedule_message(
|
||||
sender,
|
||||
client,
|
||||
recipient_type_name,
|
||||
message_to,
|
||||
topic_name,
|
||||
message_content,
|
||||
scheduled_message_id,
|
||||
"send_later",
|
||||
deliver_at,
|
||||
realm=user_profile.realm,
|
||||
forwarder_user_profile=user_profile,
|
||||
)
|
||||
return json_success(request, data={"scheduled_message_id": scheduled_message_id})
|
||||
|
|
|
@ -136,7 +136,11 @@ 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.scheduled_messages import (
|
||||
delete_scheduled_messages,
|
||||
fetch_scheduled_messages,
|
||||
scheduled_messages_backend,
|
||||
)
|
||||
from zerver.views.sentry import sentry_tunnel
|
||||
from zerver.views.storage import get_storage, remove_storage, update_storage
|
||||
from zerver.views.streams import (
|
||||
|
@ -323,7 +327,7 @@ v1_api_and_json_patterns = [
|
|||
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", GET=fetch_scheduled_messages, POST=scheduled_messages_backend),
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue