1use crate::BilibiliRequest;
8use crate::BpiError;
9use crate::BpiResult;
10use crate::user::UserClient;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct SpaceNoticeResponseData(pub String);
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct SetSpaceNoticeResponseData;
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct BangumiFollowItem {
25 pub season_id: i64,
26 pub media_id: i64,
27 pub season_type: i64,
28 pub season_type_name: String,
29 pub title: String,
30 pub cover: String,
31 pub total_count: i64,
32 pub is_finish: i64,
33 pub is_started: i64,
34 pub is_play: i64,
35 pub badge: String,
36 pub badge_type: i64,
37 pub rights: Rights,
38 pub stat: Stat,
39 pub new_ep: NewEp,
40 pub rating: Option<Rating>,
41 pub square_cover: String,
42 pub season_status: i64,
43 pub season_title: String,
44 pub badge_ep: String,
45 pub media_attr: i64,
46 pub season_attr: i64,
47 pub evaluate: String,
48 pub areas: Vec<Area>,
49 pub subtitle: String,
50 pub first_ep: i64,
51 pub can_watch: i64,
52 pub series: Series,
53 pub publish: Publish,
54 pub mode: i64,
55 pub section: Vec<Section>,
56 pub url: String,
57 pub badge_info: BadgeInfo,
58 pub renewal_time: Option<String>,
59 pub first_ep_info: FirstEpInfo,
60 pub formal_ep_count: i64,
61 pub short_url: String,
62 pub badge_infos: BadgeInfos,
63 pub season_version: String,
64 pub subtitle_14: String,
65 pub viewable_crowd_type: i64,
66 pub summary: String,
67 pub styles: Vec<String>,
68 pub follow_status: i64,
69 pub is_new: i64,
70 pub progress: String,
71 pub both_follow: bool,
72}
73
74#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Rights {
76 pub allow_review: i64,
77 pub is_selection: i64,
78 pub selection_style: i64,
79 pub is_rcmd: i64,
80
81 pub demand_end_time: serde_json::Value,
82 pub allow_preview: Option<i64>,
83 pub allow_bp_rank: Option<i64>,
84}
85
86#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct Stat {
88 pub follow: i64,
89 pub view: i64,
90 pub danmaku: i64,
91 pub reply: i64,
92 pub coin: i64,
93 pub series_follow: i64,
94 pub series_view: i64,
95 pub likes: i64,
96 pub favorite: i64,
97}
98
99#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct NewEp {
101 pub id: i64,
102 pub index_show: String,
103 pub cover: String,
104 pub title: String,
105 pub long_title: Option<String>,
106 pub pub_time: String,
107 pub duration: i64,
108}
109
110#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct Rating {
112 pub score: f64,
113 pub count: i64,
114}
115
116#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct Area {
118 pub id: i64,
119 pub name: String,
120}
121
122#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct Series {
124 pub series_id: i64,
125 pub title: String,
126 pub season_count: i64,
127 pub new_season_id: i64,
128 pub series_ord: i64,
129}
130
131#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct Publish {
133 pub pub_time: String,
134 pub pub_time_show: String,
135 pub release_date: String,
136 pub release_date_show: String,
137}
138
139#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Section {
141 pub section_id: i64,
142 pub season_id: i64,
143 pub limit_group: i64,
144 pub watch_platform: i64,
145 pub copyright: String,
146 pub ban_area_show: i64,
147 pub episode_ids: Vec<i64>,
148 #[serde(rename = "type")]
149 pub type_field: Option<i64>,
150 pub title: Option<String>,
151 pub attr: Option<i64>,
152}
153
154#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct BadgeInfo {
156 pub text: String,
157 pub bg_color: String,
158 pub bg_color_night: String,
159 pub img: String,
160 pub multi_img: MultiImg,
161}
162
163#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct MultiImg {
165 pub color: String,
166 pub medium_remind: String,
167}
168
169#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct FirstEpInfo {
171 pub id: i64,
172 pub cover: String,
173 pub title: String,
174 pub long_title: Option<String>,
175 pub pub_time: String,
176 pub duration: i64,
177}
178
179#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct BadgeInfos {
181 pub vip_or_pay: VipOrPay,
182}
183
184#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct VipOrPay {
186 pub text: String,
187 pub bg_color: String,
188 pub bg_color_night: String,
189 pub img: String,
190 pub multi_img: MultiImg2,
191}
192
193#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct MultiImg2 {
195 pub color: String,
196 pub medium_remind: String,
197}
198
199#[derive(Debug, Clone, Deserialize, Serialize)]
201pub struct BangumiFollowListResponseData {
202 pub list: Vec<BangumiFollowItem>,
204 pub pn: u32,
206 pub ps: u32,
208 pub total: u64,
210}
211
212#[derive(Debug, Clone, Default, PartialEq, Eq)]
214pub struct UserSpaceNoticeSetParams {
215 notice: Option<String>,
216}
217
218impl UserSpaceNoticeSetParams {
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 pub fn notice(mut self, notice: impl Into<String>) -> BpiResult<Self> {
224 let notice = notice.into();
225 if notice.len() > 150 {
226 return Err(BpiError::invalid_parameter(
227 "notice",
228 "length cannot exceed 150 bytes",
229 ));
230 }
231 self.notice = Some(notice);
232 Ok(self)
233 }
234
235 fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
236 let mut form = reqwest::multipart::Form::new().text("csrf", csrf.to_string());
237
238 if let Some(notice) = self.notice {
239 form = form.text("notice", notice);
240 }
241
242 form
243 }
244}
245
246impl<'a> UserClient<'a> {
251 pub async fn set_space_notice(
253 &self,
254 params: UserSpaceNoticeSetParams,
255 ) -> BpiResult<Option<()>> {
256 let csrf = self.client.csrf()?;
257 let form = params.into_multipart(&csrf);
258
259 self.client
260 .post("https://api.bilibili.com/x/space/notice/set")
261 .multipart(form)
262 .send_bpi_optional_payload("user.space_notice.set")
263 .await
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 use crate::probe::contract::HttpMethod;
272 use crate::probe::endpoint_contract::EndpointContract;
273
274 use crate::{ApiEnvelope, BpiError, BpiResult};
275
276 fn public_read_contract(endpoint: &str) -> BpiResult<EndpointContract> {
280 let bytes: &[u8] = match endpoint {
281 "bangumi-follow-list" => include_bytes!(
282 "../../tests/contracts/user/public-read/bangumi-follow-list/contract.json"
283 ),
284 "space-notice" => {
285 include_bytes!("../../tests/contracts/user/public-read/space-notice/contract.json")
286 }
287 _ => {
288 return Err(BpiError::invalid_parameter(
289 "endpoint",
290 "unknown user space contract",
291 ));
292 }
293 };
294
295 EndpointContract::from_slice(bytes)
296 }
297
298 #[test]
299 fn legacy_user_space_contracts_match_endpoint_requests() -> BpiResult<()> {
300 let notice = public_read_contract("space-notice")?;
301 assert_eq!(notice.name, "user.space_notice");
302 assert_eq!(notice.request.method, HttpMethod::Get);
303 assert_eq!(
304 notice.request.url.as_str(),
305 "https://api.bilibili.com/x/space/notice"
306 );
307 assert_eq!(
308 notice.request.query.get("mid").map(String::as_str),
309 Some("2")
310 );
311
312 let bangumi = public_read_contract("bangumi-follow-list")?;
313 assert_eq!(bangumi.name, "user.bangumi_follow_list");
314 assert_eq!(
315 bangumi.request.url.as_str(),
316 "https://api.bilibili.com/x/space/bangumi/follow/list"
317 );
318 assert_eq!(
319 bangumi.request.query.get("vmid").map(String::as_str),
320 Some("4279370")
321 );
322 assert_eq!(
323 bangumi.request.query.get("type").map(String::as_str),
324 Some("1")
325 );
326 Ok(())
327 }
328
329 #[test]
330 fn legacy_user_space_fixtures_parse_promoted_contract_models() -> BpiResult<()> {
331 let notice = ApiEnvelope::<SpaceNoticeResponseData>::from_slice(include_bytes!(
332 "../../tests/contracts/user/public-read/space-notice/responses/success.json"
333 ))?
334 .into_payload()?;
335 let _notice_text = notice.0;
336
337 let follow_list =
338 ApiEnvelope::<BangumiFollowListResponseData>::from_slice(include_bytes!(
339 "../../tests/contracts/user/public-read/bangumi-follow-list/responses/success.json"
340 ))?
341 .into_payload()?;
342 assert_eq!(follow_list.pn, 1);
343 Ok(())
344 }
345}