Skip to main content

bpi_rs/dynamic/
action.rs

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/// Parameters for liking or unliking a dynamic item.
14#[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/// Parameters for deleting a scheduled dynamic draft.
46#[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/// Parameters for setting or removing a dynamic top item.
64#[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    /// Likes or unlikes a dynamic item and returns the canonical payload result.
83    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    /// Deletes a scheduled dynamic draft and returns the canonical payload result.
96    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(&params.form_pairs(&csrf))
105            .send_bpi_optional_payload("dynamic.draft.delete")
106            .await
107    }
108
109    /// Sets a dynamic item as top and returns the canonical payload result.
110    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    /// Removes a dynamic item from top and returns the canonical payload result.
123    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}