misskey_api/endpoint/notes/
create.rs

1#[cfg(feature = "12-47-0")]
2use crate::model::channel::Channel;
3use crate::model::{
4    drive::DriveFile,
5    id::Id,
6    note::{Note, Visibility},
7    user::User,
8};
9
10use chrono::{serde::ts_milliseconds_option, DateTime, Duration, Utc};
11use serde::{Deserialize, Serialize, Serializer};
12use typed_builder::TypedBuilder;
13
14fn serialize_duration_milliseconds_option<S>(
15    duration: &Option<Duration>,
16    serializer: S,
17) -> Result<S::Ok, S::Error>
18where
19    S: Serializer,
20{
21    match duration {
22        None => serializer.serialize_none(),
23        Some(x) => serializer.serialize_some(&x.num_milliseconds()),
24    }
25}
26
27#[derive(Serialize, Debug, Clone, TypedBuilder)]
28#[serde(rename_all = "camelCase")]
29#[builder(doc)]
30pub struct PollRequest {
31    pub choices: Vec<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    #[builder(default, setter(strip_option))]
34    pub multiple: Option<bool>,
35    #[serde(
36        skip_serializing_if = "Option::is_none",
37        with = "ts_milliseconds_option"
38    )]
39    #[builder(default, setter(strip_option, into))]
40    pub expires_at: Option<DateTime<Utc>>,
41    #[serde(
42        skip_serializing_if = "Option::is_none",
43        serialize_with = "serialize_duration_milliseconds_option"
44    )]
45    #[builder(default, setter(strip_option))]
46    pub expired_after: Option<Duration>,
47}
48
49#[derive(Serialize, Debug, Clone, TypedBuilder)]
50#[serde(rename_all = "camelCase")]
51#[builder(doc)]
52pub struct Request {
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[builder(default, setter(strip_option))]
55    pub visibility: Option<Visibility>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    #[builder(default, setter(strip_option))]
58    pub visible_user_ids: Option<Vec<Id<User>>>,
59    #[builder(default, setter(strip_option, into))]
60    pub text: Option<String>,
61    #[builder(default, setter(strip_option, into))]
62    pub cw: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[builder(default, setter(strip_option))]
65    pub via_mobile: Option<bool>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    #[builder(default, setter(strip_option))]
68    pub local_only: Option<bool>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    #[builder(default, setter(strip_option))]
71    pub no_extract_mentions: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[builder(default, setter(strip_option))]
74    pub no_extract_hashtags: Option<bool>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[builder(default, setter(strip_option))]
77    pub no_extract_emojis: Option<bool>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    #[builder(default, setter(strip_option))]
80    pub file_ids: Option<Vec<Id<DriveFile>>>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[builder(default, setter(strip_option))]
83    pub reply_id: Option<Id<Note>>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    #[builder(default, setter(strip_option))]
86    pub renote_id: Option<Id<Note>>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    #[builder(default, setter(strip_option))]
89    pub poll: Option<PollRequest>,
90    #[cfg(feature = "12-47-0")]
91    #[cfg_attr(docsrs, doc(cfg(feature = "12-47-0")))]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    #[builder(default, setter(strip_option))]
94    pub channel_id: Option<Id<Channel>>,
95}
96
97#[derive(Deserialize, Debug, Clone)]
98#[serde(rename_all = "camelCase")]
99pub struct Response {
100    pub created_note: Note,
101}
102
103impl misskey_core::Request for Request {
104    type Response = Response;
105    const ENDPOINT: &'static str = "notes/create";
106}
107
108#[cfg(test)]
109mod tests {
110    use super::{PollRequest, Request};
111    use crate::test::{ClientExt, HttpClientExt, TestClient};
112
113    #[tokio::test]
114    async fn request_with_text() {
115        let client = TestClient::new();
116        client
117            .test(Request {
118                visibility: None,
119                visible_user_ids: None,
120                text: Some("some text".to_string()),
121                cw: None,
122                via_mobile: None,
123                local_only: None,
124                no_extract_mentions: None,
125                no_extract_hashtags: None,
126                no_extract_emojis: None,
127                file_ids: None,
128                reply_id: None,
129                renote_id: None,
130                poll: None,
131                #[cfg(feature = "12-47-0")]
132                channel_id: None,
133            })
134            .await;
135    }
136
137    #[tokio::test]
138    async fn request_with_options() {
139        let client = TestClient::new();
140        client
141            .test(Request {
142                visibility: None,
143                visible_user_ids: None,
144                text: Some("aww yeah".to_string()),
145                cw: None,
146                via_mobile: Some(true),
147                local_only: Some(true),
148                no_extract_mentions: Some(true),
149                no_extract_hashtags: Some(true),
150                no_extract_emojis: Some(true),
151                file_ids: None,
152                reply_id: None,
153                renote_id: None,
154                poll: None,
155                #[cfg(feature = "12-47-0")]
156                channel_id: None,
157            })
158            .await;
159    }
160
161    #[tokio::test]
162    async fn request_with_cw() {
163        let client = TestClient::new();
164        client
165            .test(Request {
166                visibility: None,
167                visible_user_ids: None,
168                text: Some("!".to_string()),
169                cw: Some("nsfw".to_string()),
170                via_mobile: None,
171                local_only: None,
172                no_extract_mentions: None,
173                no_extract_hashtags: None,
174                no_extract_emojis: None,
175                file_ids: None,
176                reply_id: None,
177                renote_id: None,
178                poll: None,
179                #[cfg(feature = "12-47-0")]
180                channel_id: None,
181            })
182            .await;
183    }
184
185    #[tokio::test]
186    async fn request_with_visibilty() {
187        use crate::model::note::Visibility;
188
189        let client = TestClient::new();
190        client
191            .test(Request {
192                visibility: Some(Visibility::Home),
193                visible_user_ids: None,
194                text: Some("hello home".to_string()),
195                cw: None,
196                via_mobile: None,
197                local_only: None,
198                no_extract_mentions: None,
199                no_extract_hashtags: None,
200                no_extract_emojis: None,
201                file_ids: None,
202                reply_id: None,
203                renote_id: None,
204                poll: None,
205                #[cfg(feature = "12-47-0")]
206                channel_id: None,
207            })
208            .await;
209        client
210            .test(Request {
211                visibility: Some(Visibility::Public),
212                visible_user_ids: None,
213                text: Some("hello public".to_string()),
214                cw: None,
215                via_mobile: None,
216                local_only: None,
217                no_extract_mentions: None,
218                no_extract_hashtags: None,
219                no_extract_emojis: None,
220                file_ids: None,
221                reply_id: None,
222                renote_id: None,
223                poll: None,
224                #[cfg(feature = "12-47-0")]
225                channel_id: None,
226            })
227            .await;
228        client
229            .test(Request {
230                visibility: Some(Visibility::Followers),
231                visible_user_ids: None,
232                text: Some("hello followers".to_string()),
233                cw: None,
234                via_mobile: None,
235                local_only: None,
236                no_extract_mentions: None,
237                no_extract_hashtags: None,
238                no_extract_emojis: None,
239                file_ids: None,
240                reply_id: None,
241                renote_id: None,
242                poll: None,
243                #[cfg(feature = "12-47-0")]
244                channel_id: None,
245            })
246            .await;
247
248        let admin = client.admin.me().await;
249        client
250            .user
251            .test(Request {
252                visibility: Some(Visibility::Specified),
253                visible_user_ids: Some(vec![admin.id]),
254                text: Some("hello specific person".to_string()),
255                cw: None,
256                via_mobile: None,
257                local_only: None,
258                no_extract_mentions: None,
259                no_extract_hashtags: None,
260                no_extract_emojis: None,
261                file_ids: None,
262                reply_id: None,
263                renote_id: None,
264                poll: None,
265                #[cfg(feature = "12-47-0")]
266                channel_id: None,
267            })
268            .await;
269    }
270
271    #[tokio::test]
272    async fn request_with_renote() {
273        let client = TestClient::new();
274        let note = client
275            .test(Request {
276                visibility: None,
277                visible_user_ids: None,
278                text: Some("renote".to_string()),
279                cw: None,
280                via_mobile: None,
281                local_only: None,
282                no_extract_mentions: None,
283                no_extract_hashtags: None,
284                no_extract_emojis: None,
285                file_ids: None,
286                reply_id: None,
287                renote_id: None,
288                poll: None,
289                #[cfg(feature = "12-47-0")]
290                channel_id: None,
291            })
292            .await
293            .created_note;
294        client
295            .test(Request {
296                visibility: None,
297                visible_user_ids: None,
298                text: None,
299                cw: None,
300                via_mobile: None,
301                local_only: None,
302                no_extract_mentions: None,
303                no_extract_hashtags: None,
304                no_extract_emojis: None,
305                file_ids: None,
306                reply_id: None,
307                renote_id: Some(note.id),
308                poll: None,
309                #[cfg(feature = "12-47-0")]
310                channel_id: None,
311            })
312            .await;
313    }
314
315    #[tokio::test]
316    async fn request_with_reply() {
317        let client = TestClient::new();
318        let note = client
319            .test(Request {
320                visibility: None,
321                visible_user_ids: None,
322                text: Some("reply".to_string()),
323                cw: None,
324                via_mobile: None,
325                local_only: None,
326                no_extract_mentions: None,
327                no_extract_hashtags: None,
328                no_extract_emojis: None,
329                file_ids: None,
330                reply_id: None,
331                renote_id: None,
332                poll: None,
333                #[cfg(feature = "12-47-0")]
334                channel_id: None,
335            })
336            .await
337            .created_note;
338        client
339            .test(Request {
340                visibility: None,
341                visible_user_ids: None,
342                text: Some("hey".to_string()),
343                cw: None,
344                via_mobile: None,
345                local_only: None,
346                no_extract_mentions: None,
347                no_extract_hashtags: None,
348                no_extract_emojis: None,
349                file_ids: None,
350                reply_id: Some(note.id),
351                renote_id: None,
352                poll: None,
353                #[cfg(feature = "12-47-0")]
354                channel_id: None,
355            })
356            .await;
357    }
358
359    #[tokio::test]
360    async fn request_with_poll() {
361        let client = TestClient::new();
362
363        let poll1 = PollRequest {
364            choices: vec!["a".to_string(), "b".to_string()],
365            multiple: Some(true),
366            expires_at: Some(chrono::Utc::now() + chrono::Duration::hours(1)),
367            expired_after: None,
368        };
369        let poll2 = PollRequest {
370            choices: vec!["c".to_string(), "d".to_string()],
371            multiple: Some(false),
372            expires_at: None,
373            expired_after: Some(chrono::Duration::hours(1)),
374        };
375
376        for poll in &[poll1, poll2] {
377            client
378                .test(Request {
379                    visibility: None,
380                    visible_user_ids: None,
381                    text: Some("poll".to_string()),
382                    cw: None,
383                    via_mobile: None,
384                    local_only: None,
385                    no_extract_mentions: None,
386                    no_extract_hashtags: None,
387                    no_extract_emojis: None,
388                    file_ids: None,
389                    reply_id: None,
390                    renote_id: None,
391                    poll: Some(poll.clone()),
392                    #[cfg(feature = "12-47-0")]
393                    channel_id: None,
394                })
395                .await;
396        }
397    }
398
399    #[tokio::test]
400    async fn request_with_files() {
401        let client = TestClient::new();
402        let file1 = client.create_text_file("test1.txt", "hi").await;
403        let file2 = client.create_text_file("test2.txt", "hi").await;
404
405        client
406            .test(Request {
407                visibility: None,
408                visible_user_ids: None,
409                text: Some("some text".to_string()),
410                cw: None,
411                via_mobile: None,
412                local_only: None,
413                no_extract_mentions: None,
414                no_extract_hashtags: None,
415                no_extract_emojis: None,
416                file_ids: Some(vec![file1.id, file2.id]),
417                reply_id: None,
418                renote_id: None,
419                poll: None,
420                #[cfg(feature = "12-47-0")]
421                channel_id: None,
422            })
423            .await;
424    }
425
426    #[cfg(feature = "12-47-0")]
427    #[tokio::test]
428    async fn request_with_channel_id() {
429        let client = TestClient::new();
430        let channel = client
431            .test(crate::endpoint::channels::create::Request {
432                name: "test".to_string(),
433                description: None,
434                banner_id: None,
435            })
436            .await;
437
438        client
439            .test(Request {
440                visibility: None,
441                visible_user_ids: None,
442                text: Some("hi channel".to_string()),
443                cw: None,
444                via_mobile: None,
445                local_only: None,
446                no_extract_mentions: None,
447                no_extract_hashtags: None,
448                no_extract_emojis: None,
449                file_ids: None,
450                reply_id: None,
451                renote_id: None,
452                poll: None,
453                channel_id: Some(channel.id),
454            })
455            .await;
456    }
457}