zulip_api/message/send_message/request.rs
1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4
5use crate::message::{MessageTo, MessageType};
6use crate::uuid::Uuid;
7use crate::{field_access, impl_default, impl_display};
8
9/// Represents a message to send to a Zulip server.
10#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
11pub struct Request {
12 #[serde(rename = "type")]
13 msg_type: MessageType,
14 to: MessageTo,
15 content: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 topic: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 queue_id: Option<Uuid>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 local_id: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 read_by_sender: Option<bool>,
24}
25
26impl Request {
27 /// Creates a new [Request].
28 pub const fn new() -> Self {
29 Self {
30 msg_type: MessageType::new(),
31 to: MessageTo::new(),
32 content: String::new(),
33 topic: None,
34 queue_id: None,
35 local_id: None,
36 read_by_sender: None,
37 }
38 }
39
40 /// Gets the message `queue_id` field.
41 ///
42 /// For clients supporting
43 /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
44 /// the [event queue](https://zulip.com/api/register-queue)
45 /// ID for the client.
46 ///
47 /// If passed, `local_id` is required. If the message is
48 /// successfully sent, the server will include `local_id` in the `message` event
49 /// that the client with this `queue_id` will receive notifying it of the new message
50 /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
51 /// that it should replace the locally echoed message, rather than adding this new
52 /// message (which would be correct if the user had sent the new message from another
53 /// device).
54 pub const fn queue_id(&self) -> Option<Uuid> {
55 self.queue_id
56 }
57
58 /// Sets the message `queue_id` and `local_id` fields.
59 ///
60 /// If set, the `queue_id` field requires setting the `local_id` field.
61 ///
62 /// For clients supporting
63 /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
64 /// the [event queue](https://zulip.com/api/register-queue)
65 /// ID for the client.
66 ///
67 /// If passed, `local_id` is required. If the message is
68 /// successfully sent, the server will include `local_id` in the `message` event
69 /// that the client with this `queue_id` will receive notifying it of the new message
70 /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
71 /// that it should replace the locally echoed message, rather than adding this new
72 /// message (which would be correct if the user had sent the new message from another
73 /// device).
74 pub fn set_queue_id<'a, U: Into<Uuid>, S: Into<Cow<'a, str>>>(
75 &mut self,
76 queue_id: U,
77 local_id: S,
78 ) {
79 self.queue_id = Some(queue_id.into());
80 self.local_id = Some(S::into(local_id).into());
81 }
82
83 /// Unsets the message `queue_id` and `local_id` fields.
84 ///
85 /// If set, the `queue_id` field requires setting the `local_id` field.
86 ///
87 /// For clients supporting
88 /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
89 /// the [event queue](https://zulip.com/api/register-queue)
90 /// ID for the client.
91 ///
92 /// If passed, `local_id` is required. If the message is
93 /// successfully sent, the server will include `local_id` in the `message` event
94 /// that the client with this `queue_id` will receive notifying it of the new message
95 /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
96 /// that it should replace the locally echoed message, rather than adding this new
97 /// message (which would be correct if the user had sent the new message from another
98 /// device).
99 pub fn unset_queue_id(&mut self) -> (Option<Uuid>, Option<String>) {
100 (self.queue_id.take(), self.local_id.take())
101 }
102
103 /// Builder function that sets the message `queue_id` and `local_id` fields.
104 ///
105 /// If set, the `queue_id` field requires setting the `local_id` field.
106 ///
107 /// For clients supporting
108 /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
109 /// the [event queue](https://zulip.com/api/register-queue)
110 /// ID for the client.
111 ///
112 /// If passed, `local_id` is required. If the message is
113 /// successfully sent, the server will include `local_id` in the `message` event
114 /// that the client with this `queue_id` will receive notifying it of the new message
115 /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
116 /// that it should replace the locally echoed message, rather than adding this new
117 /// message (which would be correct if the user had sent the new message from another
118 /// device).
119 pub fn with_queue_id<'a, U: Into<Uuid>, S: Into<Cow<'a, str>>>(
120 self,
121 queue_id: U,
122 local_id: S,
123 ) -> Self {
124 Self {
125 queue_id: Some(queue_id.into()),
126 local_id: Some(S::into(local_id).into()),
127 ..self
128 }
129 }
130
131 /// Gets the message `local_id` field.
132 ///
133 /// For clients supporting local echo, a unique string-format identifier
134 /// chosen freely by the client; the server will pass it back to the client without
135 /// inspecting it, as described in the `queue_id` description.
136 pub fn local_id(&self) -> Option<&str> {
137 self.local_id.as_deref()
138 }
139}
140
141field_access! {
142 Request {
143 /// The type of message to be sent.
144 ///
145 /// `"direct"` for a direct message and `"stream"` or `"channel"` for a
146 /// channel message.
147 ///
148 /// **Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
149 /// an additional value for this parameter to request a channel message.
150 ///
151 /// In Zulip 7.0 (feature level 174), `"direct"` was added as
152 /// the preferred way to request a direct message, deprecating the original
153 /// `"private"`. While `"private"` is still supported for requesting direct
154 /// messages, clients are encouraged to use to the modern convention with
155 /// servers that support it, because support for `"private"` will eventually
156 /// be removed.
157 msg_type: MessageType,
158 }
159}
160
161field_access! {
162 Request {
163 /// For channel messages, either the name or integer ID of the channel. For
164 /// direct messages, either a list containing integer user IDs or a list
165 /// containing string Zulip API email addresses.
166 ///
167 /// **Changes**: Support for using user/channel IDs was added in Zulip 2.0.0.
168 to: as_ref { &MessageTo, MessageTo },
169 }
170}
171
172field_access! {
173 Request {
174 /// The content of the message.
175 ///
176 /// Clients should use the `max_message_length` returned by the
177 /// [`POST /register`](https://zulip.com/api/register-queue) endpoint to determine
178 /// the maximum message size.
179 content: cow { str },
180 }
181}
182
183field_access! {
184 Request {
185 /// The topic of the message.
186 ///
187 /// Only required for channel messages
188 /// (`"type": "stream"` or `"type": "channel"`), ignored otherwise.
189 ///
190 /// Clients should use the `max_topic_length` returned by the
191 /// [`POST /register`](https://zulip.com/api/register-queue) endpoint to determine
192 /// the maximum topic length.
193 ///
194 /// Note: When `"(no topic)"` or the value of `realm_empty_topic_display_name`
195 /// found in the [POST /register](https://zulip.com/api/register-queue) response is used for this
196 /// parameter, it is interpreted as an empty string.
197 ///
198 /// When [topics are required](https://zulip.com/help/require-topics), this parameter can't
199 /// be `"(no topic)"`, an empty string, or the value of `realm_empty_topic_display_name`.
200 ///
201 /// **Changes**: Before Zulip 10.0 (feature level 370), `"(no topic)"`
202 /// was not interpreted as an empty string.
203 ///
204 /// Before Zulip 10.0 (feature level 334), empty string
205 /// was not a valid topic name for channel messages.
206 ///
207 /// New in Zulip 2.0.0. Previous Zulip releases encoded
208 /// this as `subject`, which is currently a deprecated alias.
209 topic: option_cow_deref { str, String },
210 }
211}
212
213field_access! {
214 Request {
215 /// Whether the message should be initially marked read by its
216 /// sender. If unspecified, the server uses a heuristic based
217 /// on the client name.
218 ///
219 /// **Changes**: New in Zulip 8.0 (feature level 236).
220 read_by_sender: option { bool },
221 }
222}
223
224impl_default!(Request);
225impl_display!(Request);
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_message_minimal() {
233 let message_ty = MessageType::Channel;
234 let ty_json = serde_json::to_string(&message_ty).unwrap();
235
236 let message_to = MessageTo::ChannelId(42);
237 let to_json = serde_json::to_string(&message_to).unwrap();
238
239 let content = "test message";
240 let content_json = serde_json::to_string(&content).unwrap();
241
242 let message = Request::new()
243 .with_msg_type(message_ty)
244 .with_to(message_to.clone())
245 .with_content(content);
246
247 let message_json =
248 format!(r#"{{"type":{ty_json},"to":{to_json},"content":{content_json}}}"#);
249
250 assert_eq!(serde_json::to_string(&message).unwrap(), message_json);
251 assert_eq!(
252 serde_json::from_str::<Request>(&message_json).unwrap(),
253 message
254 );
255 assert_eq!(message.msg_type(), message_ty);
256 assert_eq!(message.to(), &message_to);
257 assert_eq!(message.content(), content);
258 }
259
260 #[test]
261 fn test_message_full() {
262 let message_ty = MessageType::Channel;
263 let ty_json = serde_json::to_string(&message_ty).unwrap();
264
265 let message_to = MessageTo::ChannelId(42);
266 let to_json = serde_json::to_string(&message_to).unwrap();
267
268 let content = "test message";
269 let content_json = serde_json::to_string(&content).unwrap();
270
271 let topic = "test topic";
272 let topic_json = serde_json::to_string(&topic).unwrap();
273
274 let queue_id = Uuid::try_from("936da01f-9abd-4d9d-80c7-02af85c822a8").unwrap();
275 let queue_id_json = serde_json::to_string(&queue_id).unwrap();
276
277 let local_id = "test.local_id";
278 let local_id_json = serde_json::to_string(&local_id).unwrap();
279
280 let read_by_sender = false;
281 let read_by_sender_json = serde_json::to_string(&read_by_sender).unwrap();
282
283 let message = Request::new()
284 .with_msg_type(message_ty)
285 .with_to(message_to.clone())
286 .with_content(content)
287 .with_topic(topic)
288 .with_queue_id(queue_id, local_id)
289 .with_read_by_sender(read_by_sender);
290
291 let message_json = format!(
292 r#"{{"type":{ty_json},"to":{to_json},"content":{content_json},"topic":{topic_json},"queue_id":{queue_id_json},"local_id":{local_id_json},"read_by_sender":{read_by_sender_json}}}"#
293 );
294
295 assert_eq!(serde_json::to_string(&message).unwrap(), message_json);
296 assert_eq!(
297 serde_json::from_str::<Request>(&message_json).unwrap(),
298 message
299 );
300 assert_eq!(message.msg_type(), message_ty);
301 assert_eq!(message.to(), &message_to);
302 assert_eq!(message.content(), content);
303 assert_eq!(message.topic(), Some(topic));
304 assert_eq!(message.queue_id(), Some(queue_id));
305 assert_eq!(message.local_id(), Some(local_id));
306 assert_eq!(message.read_by_sender(), Some(read_by_sender));
307 }
308}