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}