Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/messages/scheduled_messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Scheduled messages let a user write a message now and have it sent to a channel later. Server-side, you can schedule, edit, cancel and list scheduled messages on behalf of any user.

Scheduled messages must be enabled for your app. Until the message is sent, only the user who scheduled it can see it; nothing is sent to the channel.

## Scheduling a Message

Send a message with a `scheduled_at` time. It must be at least 1 minute and at most the app's maximum lead time (120 days by default) from now. Slash commands and pending messages cannot be scheduled.

```csharp
var resp = await messageClient.ScheduleMessageAsync("messaging", "general",
new MessageRequest { Text = "Good morning!" }, "user-id", DateTimeOffset.UtcNow.AddHours(1));

var scheduledMessageId = resp.ScheduledMessage.Id;
```

`SendMessageAsync` with `SendMessageOptions.ScheduledAt` does the same. The response carries `ScheduledMessage`. Its `Id` is used to edit, cancel or send the message now. The `Message` in the response holds the content as it will be sent; it is not in the channel yet.

## Updating a Scheduled Message

Change the message, the send time, or both. A new message replaces the scheduled one as a whole; the sender stays the user it was scheduled for.

```csharp
await messageClient.UpdateScheduledMessageAsync(scheduledMessageId, new UpdateScheduledMessageRequest
{
Message = new MessageRequest { Text = "Good afternoon!" },
ScheduledAt = DateTimeOffset.UtcNow.AddHours(2),
});
```

## Sending a Scheduled Message Now

```csharp
await messageClient.SendScheduledMessageNowAsync(scheduledMessageId);
```

## Deleting a Scheduled Message

Deleting cancels a scheduled message so it is never sent, or removes a failed one.

```csharp
await messageClient.DeleteScheduledMessageAsync(scheduledMessageId);
```

Updating, deleting or sending now fails with a 409 error while the message is being sent.

## Querying Scheduled Messages

List the scheduled messages of a user across channels: pending ones first, then failed ones, each by `scheduled_at`. You can filter by channel and status (`scheduled`, `sending`, `failed`).

```csharp
var page = await messageClient.QueryScheduledMessagesAsync(new QueryScheduledMessagesRequest
{
UserId = "user-id",
Filter = new ScheduledMessagesFilter
{
Cid = "messaging:general",
Status = new List<string> { ScheduledMessageStatus.Failed },
},
Limit = 20,
});

// next page
if (!string.IsNullOrEmpty(page.Next))
{
page = await messageClient.QueryScheduledMessagesAsync(new QueryScheduledMessagesRequest
{
UserId = "user-id",
Limit = 20,
Next = page.Next,
});
}
```

## Failed Messages

When a scheduled message cannot be sent, its `Status` becomes `failed` and `FailureReason` says why (see `ScheduledMessageFailureReason`): `banned`, `no_permission`, `channel_frozen`, `channel_deleted`, `user_deleted`, `moderation_blocked`, `hook_rejected`, `expired`, `internal_error`, `invalid_message` or `pending_not_supported`. A failed message is not retried; delete it, and schedule it again if needed.

## Webhooks

The `scheduled_message.sent` and `scheduled_message.failed` events are sent to webhooks. `EventResponse.ScheduledMessage` holds the scheduled message, and on `scheduled_message.sent`, `GetData<string>("message_id")` returns the ID of the sent message.
2 changes: 2 additions & 0 deletions docs/webhooks/webhook_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ When applicable, the following attributes are included to the event user and to
| reminder.updated | when a message reminder is updated. |
| reminder.deleted | when a message reminder is deleted. |
| notification.reminder_due | when a message reminder's due time is reached. |
| scheduled_message.sent | when a scheduled message is sent; message_id is the sent message. |
| scheduled_message.failed | when a scheduled message fails to send (see failure_reason). |

### `message.new`

Expand Down
44 changes: 44 additions & 0 deletions src/Clients/IMessageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,5 +241,49 @@ Task<FileUrlResponse> UploadImageAsync(string channelType,
/// <param name="options">Additional query options like limit, offset</param>
/// <returns>API response with reminders</returns>
Task<QueryRemindersResponse> QueryRemindersAsync(string userId, Dictionary<string, object> filterConditions = null, List<Dictionary<string, object>> sort = null, Dictionary<string, object> options = null);

/// <summary>
/// Schedules a message to be sent to a channel at a later time, on behalf of <paramref name="userId"/>.
/// The message is not sent now; the response carries <see cref="GenericMessageResponse.ScheduledMessage"/>,
/// whose ID is used to update, delete or send the scheduled message now.
/// </summary>
/// <param name="channelType">The type of the channel</param>
/// <param name="channelId">The ID of the channel</param>
/// <param name="msg">The message to send</param>
/// <param name="userId">The ID of the user the message is sent as</param>
/// <param name="scheduledAt">When to send the message. Must be at least 1 minute and at most the app's maximum lead time from now</param>
/// <param name="options">Other send options. Their <see cref="SendMessageOptions.ScheduledAt"/> is ignored in favor of <paramref name="scheduledAt"/>; the object passed in is not changed</param>
Task<GenericMessageResponse> ScheduleMessageAsync(string channelType, string channelId, MessageRequest msg, string userId, DateTimeOffset scheduledAt, SendMessageOptions options = null);

/// <summary>
/// Updates the message and/or the send time of a scheduled message.
/// The message, when set, replaces the scheduled message as a whole.
/// </summary>
/// <param name="scheduledMessageId">The ID of the scheduled message</param>
/// <param name="request">The changes. At least one of message or scheduled_at is required</param>
/// <returns>API response with the updated scheduled message</returns>
Task<ScheduledMessageResponse> UpdateScheduledMessageAsync(string scheduledMessageId, UpdateScheduledMessageRequest request);

/// <summary>
/// Deletes a scheduled message: cancels it so it is never sent, or removes a failed one.
/// </summary>
/// <param name="scheduledMessageId">The ID of the scheduled message</param>
/// <returns>API response with the deleted scheduled message</returns>
Task<ScheduledMessageResponse> DeleteScheduledMessageAsync(string scheduledMessageId);

/// <summary>
/// Sends a scheduled message right away.
/// </summary>
/// <param name="scheduledMessageId">The ID of the scheduled message</param>
/// <returns>API response with the scheduled message</returns>
Task<ScheduledMessageResponse> SendScheduledMessageNowAsync(string scheduledMessageId);

/// <summary>
/// Queries the scheduled messages of one user across channels: pending ones first,
/// then failed ones, each by scheduled_at.
/// </summary>
/// <param name="request">The query. <see cref="QueryScheduledMessagesRequest.UserId"/> is required</param>
/// <returns>API response with a page of scheduled messages</returns>
Task<QueryScheduledMessagesResponse> QueryScheduledMessagesAsync(QueryScheduledMessagesRequest request);
}
}
37 changes: 37 additions & 0 deletions src/Clients/MessageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public async Task<GenericMessageResponse> SendMessageAsync(string channelType, s
SkipPush = options.SkipPush,
IsPendingMessage = options.IsPendingMessage,
ForceModeration = options.ForceModeration,
ScheduledAt = options.ScheduledAt,
};
req.Message.UserId = userId;

Expand Down Expand Up @@ -296,5 +297,41 @@ public async Task<QueryRemindersResponse> QueryRemindersAsync(string userId, Dic
HttpStatusCode.Created,
data);
}

public async Task<GenericMessageResponse> ScheduleMessageAsync(string channelType, string channelId, MessageRequest msg, string userId, DateTimeOffset scheduledAt, SendMessageOptions options = null)
{
// Copy instead of setting ScheduledAt on the caller's options: a reused options object
// would otherwise schedule every later send made with it.
var opts = new SendMessageOptions
{
SkipPush = options?.SkipPush ?? false,
IsPendingMessage = options?.IsPendingMessage ?? false,
ForceModeration = options?.ForceModeration,
ScheduledAt = scheduledAt,
};
return await SendMessageAsync(channelType, channelId, msg, userId, opts);
}

public async Task<ScheduledMessageResponse> UpdateScheduledMessageAsync(string scheduledMessageId, UpdateScheduledMessageRequest request)
=> await ExecuteRequestAsync<ScheduledMessageResponse>($"scheduled_messages/{scheduledMessageId}",
HttpMethod.PATCH,
HttpStatusCode.OK,
request);

public async Task<ScheduledMessageResponse> DeleteScheduledMessageAsync(string scheduledMessageId)
=> await ExecuteRequestAsync<ScheduledMessageResponse>($"scheduled_messages/{scheduledMessageId}",
HttpMethod.DELETE,
HttpStatusCode.OK);

public async Task<ScheduledMessageResponse> SendScheduledMessageNowAsync(string scheduledMessageId)
=> await ExecuteRequestAsync<ScheduledMessageResponse>($"scheduled_messages/{scheduledMessageId}/send_now",
HttpMethod.POST,
HttpStatusCode.Created);

public async Task<QueryScheduledMessagesResponse> QueryScheduledMessagesAsync(QueryScheduledMessagesRequest request)
=> await ExecuteRequestAsync<QueryScheduledMessagesResponse>("scheduled_messages/query",
HttpMethod.POST,
HttpStatusCode.Created,
request);
}
}
7 changes: 7 additions & 0 deletions src/Models/Event.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Newtonsoft.Json;

namespace StreamChat.Models
{
Expand Down Expand Up @@ -54,6 +55,12 @@ public class EventResponse : CustomDataBase
public string ParentId { get; set; }
public string Team { get; set; }
public DateTimeOffset? CreatedAt { get; set; }

/// <summary>
/// The scheduled message of a scheduled_message.* event.
/// </summary>
[JsonProperty("scheduled_message")]
public ScheduledMessage ScheduledMessage { get; set; }
}

public class UserCustomEvent : CustomDataBase
Expand Down
7 changes: 7 additions & 0 deletions src/Models/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class MessageSendRequest
public bool? SkipPush { get; set; }
public bool? IsPendingMessage { get; set; }
public bool? ForceModeration { get; set; }
public DateTimeOffset? ScheduledAt { get; set; }
}

public class MessageSearchResponse : ApiResponse
Expand Down Expand Up @@ -91,6 +92,12 @@ public class MessagePartialUpdateRequest
public class GenericMessageResponse : ApiResponse
{
public Message Message { get; set; }

/// <summary>
/// The scheduled message. Only set when the message was sent with a scheduled_at;
/// <see cref="Message"/> then holds the content as it will be sent and is not in the channel.
/// </summary>
public ScheduledMessage ScheduledMessage { get; set; }
}

public class MessagePartialUpdateResponse : GenericMessageResponse
Expand Down
8 changes: 8 additions & 0 deletions src/Models/MessageRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ public class SendMessageOptions
public bool SkipPush { get; set; }
public bool IsPendingMessage { get; set; }
public bool? ForceModeration { get; set; }

/// <summary>
/// When set, the message is not sent now: it is stored and sent at this time.
/// The response then carries the scheduled message in GenericMessageResponse.ScheduledMessage.
/// Must be at least 1 minute and at most the app's maximum lead time from now.
/// <see cref="IsPendingMessage"/> must be false: pending messages cannot be scheduled.
/// </summary>
public DateTimeOffset? ScheduledAt { get; set; }
}
Loading
Loading