1use crate::BilibiliRequest;
4use crate::BpiError;
5use crate::BpiResult;
6use crate::ids::Aid;
7use crate::note::NoteClient;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
14pub struct NoteAddResponseData {
15 pub note_id: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct NoteAddParams {
22 oid: Aid,
23 title: String,
24 summary: String,
25 content: String,
26 note_id: Option<String>,
27 tags: Option<String>,
28 publish: Option<bool>,
29 auto_comment: Option<bool>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct NoteDeleteParams {
35 oid: Aid,
36 note_id: Option<String>,
37}
38
39impl NoteDeleteParams {
40 pub fn new(oid: Aid) -> Self {
41 Self { oid, note_id: None }
42 }
43
44 pub fn note_id(mut self, note_id: impl Into<String>) -> BpiResult<Self> {
45 self.note_id = Some(normalize_non_blank("note_id", note_id.into())?);
46 Ok(self)
47 }
48
49 fn form_pairs(&self, csrf: impl Into<String>) -> Vec<(&'static str, String)> {
50 let mut form = vec![("oid", self.oid.to_string()), ("csrf", csrf.into())];
51
52 if let Some(note_id) = &self.note_id {
53 form.push(("note_id", note_id.clone()));
54 }
55
56 form
57 }
58}
59
60impl NoteAddParams {
61 pub fn new(
63 oid: Aid,
64 title: impl Into<String>,
65 summary: impl Into<String>,
66 content: impl Into<String>,
67 ) -> BpiResult<Self> {
68 let params = Self {
69 oid,
70 title: title.into(),
71 summary: summary.into(),
72 content: content.into(),
73 note_id: None,
74 tags: None,
75 publish: None,
76 auto_comment: None,
77 };
78 params.validate()?;
79 Ok(params)
80 }
81
82 pub fn note_id(mut self, note_id: impl Into<String>) -> BpiResult<Self> {
84 self.note_id = Some(note_id.into());
85 self.validate()?;
86 Ok(self)
87 }
88
89 pub fn tags(mut self, tags: impl Into<String>) -> BpiResult<Self> {
91 self.tags = Some(tags.into());
92 self.validate()?;
93 Ok(self)
94 }
95
96 pub fn publish(mut self, publish: bool) -> Self {
98 self.publish = Some(publish);
99 self
100 }
101
102 pub fn auto_comment(mut self, auto_comment: bool) -> Self {
104 self.auto_comment = Some(auto_comment);
105 self
106 }
107
108 fn validate(&self) -> BpiResult<()> {
109 normalize_non_blank("title", self.title.clone())?;
110 normalize_non_blank("summary", self.summary.clone())?;
111 normalize_non_blank("content", self.content.clone())?;
112 if let Some(note_id) = self.note_id.as_deref() {
113 normalize_non_blank("note_id", note_id.to_string())?;
114 }
115 if let Some(tags) = self.tags.as_deref() {
116 normalize_non_blank("tags", tags.to_string())?;
117 }
118 Ok(())
119 }
120
121 fn form_pairs(&self, csrf: impl Into<String>) -> Vec<(&'static str, String)> {
122 let content = json!([{"insert": self.content}]);
123 let mut form = vec![
124 ("oid", self.oid.to_string()),
125 ("oid_type", "0".to_string()),
126 ("title", self.title.clone()),
127 ("summary", self.summary.clone()),
128 ("content", content.to_string()),
129 ("cls", "1".to_string()),
130 ("from", "save".to_string()),
131 ("platform", "web".to_string()),
132 ("csrf", csrf.into()),
133 ];
134
135 if let Some(tags) = self.tags.as_ref() {
136 form.push(("tags", tags.clone()));
137 }
138 if let Some(note_id) = self.note_id.as_ref() {
139 form.push(("note_id", note_id.clone()));
140 }
141 if let Some(publish) = self.publish {
142 form.push(("publish", bool_flag(publish)));
143 }
144 if let Some(auto_comment) = self.auto_comment {
145 form.push(("auto_comment", bool_flag(auto_comment)));
146 }
147
148 form
149 }
150}
151
152fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
153 let value = value.trim().to_string();
154 if value.is_empty() {
155 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
156 }
157
158 Ok(value)
159}
160
161fn bool_flag(value: bool) -> String {
162 if value {
163 "1".to_string()
164 } else {
165 "0".to_string()
166 }
167}
168
169impl<'a> NoteClient<'a> {
172 pub async fn add(&self, params: NoteAddParams) -> BpiResult<NoteAddResponseData> {
174 let csrf = self.client.csrf()?;
175 let form = params.form_pairs(csrf);
176
177 self.client
178 .post("https://api.bilibili.com/x/note/add")
179 .form(&form)
180 .send_bpi_payload("note.add")
181 .await
182 }
183
184 pub async fn delete(&self, params: NoteDeleteParams) -> BpiResult<Option<serde_json::Value>> {
186 let csrf = self.client.csrf()?;
187 let form = params.form_pairs(csrf);
188
189 self.client
190 .post("https://api.bilibili.com/x/note/del")
191 .form(&form)
192 .send_bpi_optional_payload("note.delete")
193 .await
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn note_add_params_rejects_blank_content() {
203 let err = NoteAddParams::new(
204 Aid::new(170001).expect("test aid should be valid"),
205 "title",
206 "summary",
207 " ",
208 )
209 .unwrap_err();
210
211 assert!(matches!(
212 err,
213 BpiError::InvalidParameter {
214 field: "content",
215 ..
216 }
217 ));
218 }
219}