1use crate::BilibiliRequest;
2use crate::BpiError;
3use crate::BpiResult;
4use crate::dynamic::DynamicClient;
5use serde_json::json;
6
7const LIKE_ENDPOINT: &str = "https://api.bilibili.com/x/dynamic/feed/dyn/thumb";
8const REMOVE_DRAFT_ENDPOINT: &str =
9 "https://api.vc.bilibili.com/dynamic_draft/v1/dynamic_draft/rm_draft";
10const SET_TOP_ENDPOINT: &str = "https://api.bilibili.com/x/dynamic/feed/space/set_top";
11const REMOVE_TOP_ENDPOINT: &str = "https://api.bilibili.com/x/dynamic/feed/space/rm_top";
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct DynamicLikeParams {
16 dyn_id_str: String,
17 up: u8,
18}
19
20impl DynamicLikeParams {
21 pub fn new(dyn_id_str: impl Into<String>, up: u8) -> BpiResult<Self> {
22 if !(0..=2).contains(&up) {
23 return Err(BpiError::invalid_parameter(
24 "up",
25 "value must be 0, 1, or 2",
26 ));
27 }
28
29 Ok(Self {
30 dyn_id_str: normalize_non_blank("dyn_id_str", dyn_id_str.into())?,
31 up,
32 })
33 }
34
35 fn json_body(&self) -> serde_json::Value {
36 json!({
37 "dyn_id_str": self.dyn_id_str,
38 "up": self.up,
39 "spmid": "333.1369.0.0",
40 "from_spmid": "333.999.0.0",
41 })
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct DynamicDraftDeleteParams {
48 draft_id: String,
49}
50
51impl DynamicDraftDeleteParams {
52 pub fn new(draft_id: impl Into<String>) -> BpiResult<Self> {
53 Ok(Self {
54 draft_id: normalize_non_blank("draft_id", draft_id.into())?,
55 })
56 }
57
58 fn form_pairs<'a>(&'a self, csrf: &'a str) -> [(&'static str, &'a str); 2] {
59 [("draft_id", &self.draft_id), ("csrf", csrf)]
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct DynamicTopParams {
66 dyn_str: String,
67}
68
69impl DynamicTopParams {
70 pub fn new(dyn_str: impl Into<String>) -> BpiResult<Self> {
71 Ok(Self {
72 dyn_str: normalize_non_blank("dyn_str", dyn_str.into())?,
73 })
74 }
75
76 fn json_body(&self) -> serde_json::Value {
77 json!({ "dyn_str": self.dyn_str })
78 }
79}
80
81impl<'a> DynamicClient<'a> {
82 pub async fn like(&self, params: DynamicLikeParams) -> BpiResult<Option<serde_json::Value>> {
84 let csrf = self.client.csrf()?;
85 let json_body = params.json_body();
86
87 self.client
88 .post(LIKE_ENDPOINT)
89 .query(&[("csrf", csrf)])
90 .json(&json_body)
91 .send_bpi_optional_payload("dynamic.like")
92 .await
93 }
94
95 pub async fn delete_draft(
97 &self,
98 params: DynamicDraftDeleteParams,
99 ) -> BpiResult<Option<serde_json::Value>> {
100 let csrf = self.client.csrf()?;
101
102 self.client
103 .post(REMOVE_DRAFT_ENDPOINT)
104 .form(¶ms.form_pairs(&csrf))
105 .send_bpi_optional_payload("dynamic.draft.delete")
106 .await
107 }
108
109 pub async fn set_top(&self, params: DynamicTopParams) -> BpiResult<Option<serde_json::Value>> {
111 let csrf = self.client.csrf()?;
112 let json_body = params.json_body();
113
114 self.client
115 .post(SET_TOP_ENDPOINT)
116 .query(&[("csrf", csrf)])
117 .json(&json_body)
118 .send_bpi_optional_payload("dynamic.top.set")
119 .await
120 }
121
122 pub async fn remove_top(
124 &self,
125 params: DynamicTopParams,
126 ) -> BpiResult<Option<serde_json::Value>> {
127 let csrf = self.client.csrf()?;
128 let json_body = params.json_body();
129
130 self.client
131 .post(REMOVE_TOP_ENDPOINT)
132 .query(&[("csrf", csrf)])
133 .json(&json_body)
134 .send_bpi_optional_payload("dynamic.top.remove")
135 .await
136 }
137}
138
139fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
140 let value = value.trim().to_string();
141 if value.is_empty() {
142 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
143 }
144
145 Ok(value)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn dynamic_like_params_rejects_invalid_action() {
154 let err = DynamicLikeParams::new("123", 3).unwrap_err();
155
156 assert!(matches!(
157 err,
158 BpiError::InvalidParameter { field: "up", .. }
159 ));
160 }
161}