Skip to content

Commit b3dad6d

Browse files
[CHA-5933] feat: support scheduled messages (#278)
* feat: support scheduled messages Message.send gains scheduledAt, and its response carries the stored scheduled message. A new ScheduledMessage model lists (query), edits (update), cancels (delete) and sends early (sendNow) the scheduled messages of any user. Event reads scheduled_message and message_id for the scheduled_message.* events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: note pending(false) is accepted with scheduledAt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop the editable claim from STATUS_SCHEDULED Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: read scheduled message custom fields from the top level The v1 API returns a scheduled message's custom fields as top-level keys of its message object, as for regular messages, not under "custom". Drop the custom field so they land in getAdditionalFields(), as on Message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8e7b119 commit b3dad6d

7 files changed

Lines changed: 939 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
Scheduled messages let a user write a message now and have it sent later. A server-side client can schedule, list, edit, cancel and send early the scheduled messages of any user.
2+
3+
Scheduled messages must be enabled for your app. Until the scheduled message is sent it is visible only to the user who scheduled it (and to your backend); the channel's other members see nothing until the message is sent.
4+
5+
## Scheduling a Message
6+
7+
Pass `scheduledAt` to a regular send. The message is not sent: it is stored and sent at that time. The send time must be at least 1 minute and at most the app's maximum lead time (120 days by default) away.
8+
9+
```java
10+
// Backend SDK
11+
Date in1Hour = new Date(System.currentTimeMillis() + 60 * 60 * 1000);
12+
13+
MessageSendResponse response =
14+
Message.send("messaging", "general")
15+
.message(MessageRequestObject.builder().text("Good morning!").userId("john").build())
16+
.scheduledAt(in1Hour)
17+
.request();
18+
19+
ScheduledMessage scheduled = response.getScheduledMessage();
20+
scheduled.getId(); // ID of the scheduled message
21+
scheduled.getStatus(); // "scheduled"
22+
scheduled.getScheduledAt(); // when it will be sent
23+
```
24+
25+
`response.getMessage()` holds the message as it will be sent; it is not in the channel yet. Slash commands cannot be scheduled, and `scheduledAt` cannot be combined with `pending(true)` (`pending(false)` is accepted).
26+
27+
## Listing Scheduled Messages
28+
29+
Lists one user's scheduled messages across channels: pending ones first, then failed ones, each by `scheduled_at`. `userId` is required. Each item carries a summary of its channel.
30+
31+
```java
32+
ScheduledMessageQueryResponse page =
33+
ScheduledMessage.query()
34+
.userId("john")
35+
.filter(
36+
ScheduledMessagesFilter.builder()
37+
.cid("messaging:general") // optional: one channel only
38+
.status("scheduled") // optional: scheduled, sending, failed
39+
.build())
40+
.limit(100) // 1 to 100, defaults to 20
41+
.request();
42+
43+
List<ScheduledMessage> items = page.getScheduledMessages();
44+
45+
// Next page
46+
if (page.getNext() != null && !page.getNext().isEmpty()) {
47+
ScheduledMessage.query().userId("john").next(page.getNext()).request();
48+
}
49+
```
50+
51+
Only sorting by `scheduled_at` ascending is supported.
52+
53+
## Editing a Scheduled Message
54+
55+
Replace the message, move the send time, or both. A new `message` replaces the stored one as a whole; the sender stays the user who scheduled it.
56+
57+
```java
58+
ScheduledMessage.update(scheduled.getId())
59+
.message(MessageRequestObject.builder().text("Good morning, everyone!").build())
60+
.scheduledAt(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000))
61+
.request();
62+
```
63+
64+
## Canceling a Scheduled Message
65+
66+
Cancels a scheduled message so it is never sent, or removes one that failed.
67+
68+
```java
69+
ScheduledMessage canceled = ScheduledMessage.delete(scheduled.getId()).request().getScheduledMessage();
70+
canceled.getStatus(); // "canceled"
71+
```
72+
73+
## Sending a Scheduled Message Now
74+
75+
Moves the send time to now and queues the send.
76+
77+
```java
78+
ScheduledMessage.sendNow(scheduled.getId()).request();
79+
```
80+
81+
Editing, canceling and sending now return a 409 error while the message is being sent.
82+
83+
## Statuses and Failures
84+
85+
| Status | Meaning |
86+
| ----------- | ------------------------------------------------------------------------------- |
87+
| `scheduled` | Waiting for its send time |
88+
| `sending` | Being sent |
89+
| `failed` | The send failed; `getFailureReason()` says why. It can only be deleted |
90+
| `canceled` | Canceled; returned by `delete` |
91+
| `sent` | Sent; reported only by the `scheduled_message.sent` event |
92+
93+
Failure reasons: `banned`, `no_permission`, `channel_frozen`, `channel_deleted`, `user_deleted`, `moderation_blocked`, `hook_rejected`, `expired`, `internal_error`, `invalid_message`, `pending_not_supported`.
94+
95+
## Events and Webhooks
96+
97+
| Event | Webhook | When |
98+
| --------------------------- | ------- | -------------------------------------------------------------------- |
99+
| `scheduled_message.created` | No | A message was scheduled |
100+
| `scheduled_message.updated` | No | A scheduled message was edited or sent now |
101+
| `scheduled_message.deleted` | No | A scheduled message was canceled |
102+
| `scheduled_message.sent` | Yes | A scheduled message was sent; `getMessageId()` is the sent message ID |
103+
| `scheduled_message.failed` | Yes | A scheduled message failed to send |
104+
105+
`Event.getScheduledMessage()` holds the scheduled message on all of them. The sent message itself also triggers the usual `message.new`.
106+
107+
## Permissions
108+
109+
Scheduling needs the `ScheduleMessage` permission (channel capability `schedule-message`) in addition to the permission to send the message.

‎src/main/java/io/getstream/chat/java/models/Event.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public class Event {
112112
@JsonProperty("created_at")
113113
private Date createdAt;
114114

115+
/** Set on scheduled_message.* events. */
116+
@Nullable
117+
@JsonProperty("scheduled_message")
118+
private ScheduledMessage scheduledMessage;
119+
120+
/** On scheduled_message.sent, the ID of the message that was sent. */
121+
@Nullable
122+
@JsonProperty("message_id")
123+
private String messageId;
124+
115125
@NotNull @JsonIgnore private Map<String, Object> additionalFields = new HashMap<>();
116126

117127
@JsonAnyGetter

‎src/main/java/io/getstream/chat/java/models/Message.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,17 @@ public static class MessageSendRequestData {
806806
@JsonProperty("pending_message_metadata")
807807
private Map<String, Object> pendingMessageMetadata;
808808

809+
/**
810+
* When set, the message is not sent now: it is stored and sent at this time, and the response
811+
* carries {@link MessageSendResponse#getScheduledMessage()}. Must be at least 1 minute and at
812+
* most the app's maximum lead time (120 days by default) away. Requires the scheduled messages
813+
* feature on the app. Cannot be combined with {@code pending(true)}; {@code pending(false)} is
814+
* accepted. Slash commands cannot be scheduled.
815+
*/
816+
@Nullable
817+
@JsonProperty("scheduled_at")
818+
private Date scheduledAt;
819+
809820
public static class MessageSendRequest extends StreamRequest<MessageSendResponse> {
810821
@NotNull private String channelId;
811822

@@ -1405,9 +1416,18 @@ protected Call<StreamResponseObject> generateCall(Client client) {
14051416
@NoArgsConstructor
14061417
@EqualsAndHashCode(callSuper = true)
14071418
public static class MessageSendResponse extends StreamResponseObject {
1419+
/**
1420+
* The sent message. When the request had {@code scheduledAt}, the message as it will be sent;
1421+
* it is not in the channel yet.
1422+
*/
14081423
@NotNull
14091424
@JsonProperty("message")
14101425
private Message message;
1426+
1427+
/** The scheduled message. Set only when the request had {@code scheduledAt}. */
1428+
@Nullable
1429+
@JsonProperty("scheduled_message")
1430+
private ScheduledMessage scheduledMessage;
14111431
}
14121432

14131433
@Data

0 commit comments

Comments
 (0)