Repository navigation
[CHA-5933] feat: support scheduled messages #278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ed1c26e
feat: support scheduled messages
nijeesh-stream 6093ccb
docs: note pending(false) is accepted with scheduledAt
nijeesh-stream 7063902
docs: drop the editable claim from STATUS_SCHEDULED
nijeesh-stream 7dd8e6e
fix: read scheduled message custom fields from the top level
nijeesh-stream File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Scheduling a Message | ||
|
|
||
| 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. | ||
|
|
||
| ```java | ||
| // Backend SDK | ||
| Date in1Hour = new Date(System.currentTimeMillis() + 60 * 60 * 1000); | ||
|
|
||
| MessageSendResponse response = | ||
| Message.send("messaging", "general") | ||
| .message(MessageRequestObject.builder().text("Good morning!").userId("john").build()) | ||
| .scheduledAt(in1Hour) | ||
| .request(); | ||
|
|
||
| ScheduledMessage scheduled = response.getScheduledMessage(); | ||
| scheduled.getId(); // ID of the scheduled message | ||
| scheduled.getStatus(); // "scheduled" | ||
| scheduled.getScheduledAt(); // when it will be sent | ||
| ``` | ||
|
|
||
| `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). | ||
|
|
||
| ## Listing Scheduled Messages | ||
|
|
||
| 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. | ||
|
|
||
| ```java | ||
| ScheduledMessageQueryResponse page = | ||
| ScheduledMessage.query() | ||
| .userId("john") | ||
| .filter( | ||
| ScheduledMessagesFilter.builder() | ||
| .cid("messaging:general") // optional: one channel only | ||
| .status("scheduled") // optional: scheduled, sending, failed | ||
| .build()) | ||
| .limit(100) // 1 to 100, defaults to 20 | ||
| .request(); | ||
|
|
||
| List<ScheduledMessage> items = page.getScheduledMessages(); | ||
|
|
||
| // Next page | ||
| if (page.getNext() != null && !page.getNext().isEmpty()) { | ||
| ScheduledMessage.query().userId("john").next(page.getNext()).request(); | ||
| } | ||
| ``` | ||
|
|
||
| Only sorting by `scheduled_at` ascending is supported. | ||
|
|
||
| ## Editing a Scheduled Message | ||
|
|
||
| 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. | ||
|
|
||
| ```java | ||
| ScheduledMessage.update(scheduled.getId()) | ||
| .message(MessageRequestObject.builder().text("Good morning, everyone!").build()) | ||
| .scheduledAt(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) | ||
| .request(); | ||
| ``` | ||
|
|
||
| ## Canceling a Scheduled Message | ||
|
|
||
| Cancels a scheduled message so it is never sent, or removes one that failed. | ||
|
|
||
| ```java | ||
| ScheduledMessage canceled = ScheduledMessage.delete(scheduled.getId()).request().getScheduledMessage(); | ||
| canceled.getStatus(); // "canceled" | ||
| ``` | ||
|
|
||
| ## Sending a Scheduled Message Now | ||
|
|
||
| Moves the send time to now and queues the send. | ||
|
|
||
| ```java | ||
| ScheduledMessage.sendNow(scheduled.getId()).request(); | ||
| ``` | ||
|
|
||
| Editing, canceling and sending now return a 409 error while the message is being sent. | ||
|
|
||
| ## Statuses and Failures | ||
|
|
||
| | Status | Meaning | | ||
| | ----------- | ------------------------------------------------------------------------------- | | ||
| | `scheduled` | Waiting for its send time | | ||
| | `sending` | Being sent | | ||
| | `failed` | The send failed; `getFailureReason()` says why. It can only be deleted | | ||
| | `canceled` | Canceled; returned by `delete` | | ||
| | `sent` | Sent; reported only by the `scheduled_message.sent` event | | ||
|
|
||
| Failure reasons: `banned`, `no_permission`, `channel_frozen`, `channel_deleted`, `user_deleted`, `moderation_blocked`, `hook_rejected`, `expired`, `internal_error`, `invalid_message`, `pending_not_supported`. | ||
|
|
||
| ## Events and Webhooks | ||
|
|
||
| | Event | Webhook | When | | ||
| | --------------------------- | ------- | -------------------------------------------------------------------- | | ||
| | `scheduled_message.created` | No | A message was scheduled | | ||
| | `scheduled_message.updated` | No | A scheduled message was edited or sent now | | ||
| | `scheduled_message.deleted` | No | A scheduled message was canceled | | ||
| | `scheduled_message.sent` | Yes | A scheduled message was sent; `getMessageId()` is the sent message ID | | ||
| | `scheduled_message.failed` | Yes | A scheduled message failed to send | | ||
|
|
||
| `Event.getScheduledMessage()` holds the scheduled message on all of them. The sent message itself also triggers the usual `message.new`. | ||
|
|
||
| ## Permissions | ||
|
|
||
| Scheduling needs the `ScheduleMessage` permission (channel capability `schedule-message`) in addition to the permission to send the message. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Should Fix] This typed field also takes
message_idfrom events other thanscheduled_message.sent. The backend puts a top-levelmessage_idon every message webhook (message.new,message.updated, ...) and onreminder.*andpoll.*events. Today Jackson puts that key ingetAdditionalFields(); after this change it goes togetMessageId()and is no longer in the map.Why it matters: a backend that parses webhooks into
Eventand readsgetAdditionalFields().get("message_id")getsnullafter upgrading, with no compile error.Fix: keep the typed field, but make the Javadoc say it is the ID of the message the event is about (on
scheduled_message.sent, the sent message), and mention the move fromgetAdditionalFields()togetMessageId()in the commit message so the changelog carries it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks — #278 merged before this was addressed, so the fix is in a follow-up: #279 (commit 8b59fc5).
Rather than only documenting the move, it keeps full backward compatibility:
messageIdis no longer a Jackson-mapped field;getMessageId()/setMessageId()read and writeadditionalFields["message_id"]. SogetAdditionalFields().get("message_id")keeps working onmessage.*/reminder.*/poll.*events, JSON round-trips exactly as in 1.50.0, andgetMessageId()works on every event includingscheduled_message.sent. Tests cover amessage.newevent withmessage_idand ascheduled_message.sentevent.