Skip to main content

bpi_rs/message/
params.rs

1use crate::{BpiError, BpiResult};
2
3/// Parameters for `/x/im/web/msgfeed/unread`.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct MessageUnreadCountParams {
6    build: String,
7    mobi_app: String,
8}
9
10impl Default for MessageUnreadCountParams {
11    fn default() -> Self {
12        Self {
13            build: "0".to_string(),
14            mobi_app: "web".to_string(),
15        }
16    }
17}
18
19impl MessageUnreadCountParams {
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    pub fn with_build(mut self, build: impl Into<String>) -> BpiResult<Self> {
25        self.build = normalize_non_blank("build", build.into())?;
26        Ok(self)
27    }
28
29    pub fn with_mobi_app(mut self, mobi_app: impl Into<String>) -> BpiResult<Self> {
30        self.mobi_app = normalize_non_blank("mobi_app", mobi_app.into())?;
31        Ok(self)
32    }
33
34    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
35        vec![
36            ("build", self.build.clone()),
37            ("mobi_app", self.mobi_app.clone()),
38        ]
39    }
40}
41
42/// Parameters for `/x/msgfeed/reply`.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct MessageReplyFeedParams {
45    start_id: Option<u64>,
46    start_time: Option<u64>,
47    build: String,
48    mobi_app: String,
49    platform: String,
50    web_location: String,
51}
52
53impl Default for MessageReplyFeedParams {
54    fn default() -> Self {
55        Self {
56            start_id: None,
57            start_time: None,
58            build: "0".to_string(),
59            mobi_app: "web".to_string(),
60            platform: "web".to_string(),
61            web_location: String::new(),
62        }
63    }
64}
65
66impl MessageReplyFeedParams {
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Sets the cursor ID returned by the previous page.
72    pub fn with_start_id(mut self, start_id: u64) -> BpiResult<Self> {
73        self.start_id = Some(validate_positive_u64("id", start_id)?);
74        Ok(self)
75    }
76
77    /// Sets the cursor timestamp returned by the previous page.
78    pub fn with_start_time(mut self, start_time: u64) -> BpiResult<Self> {
79        self.start_time = Some(validate_positive_u64("reply_time", start_time)?);
80        Ok(self)
81    }
82
83    /// Sets Bilibili's raw web-location marker.
84    pub fn with_web_location(mut self, web_location: impl Into<String>) -> Self {
85        self.web_location = web_location.into();
86        self
87    }
88
89    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
90        let mut pairs = vec![
91            ("build", self.build.clone()),
92            ("mobi_app", self.mobi_app.clone()),
93            ("platform", self.platform.clone()),
94            ("web_location", self.web_location.clone()),
95        ];
96
97        if let Some(start_id) = self.start_id {
98            pairs.push(("id", start_id.to_string()));
99        }
100        if let Some(start_time) = self.start_time {
101            pairs.push(("reply_time", start_time.to_string()));
102        }
103
104        pairs
105    }
106}
107
108/// Unread category accepted by `/session_svr/v1/session_svr/single_unread`.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum SingleUnreadType {
111    All,
112    Follow,
113    Unfollow,
114    Blocked,
115    Custom(u32),
116}
117
118impl SingleUnreadType {
119    fn as_query_value(self) -> String {
120        match self {
121            Self::All => "0".to_string(),
122            Self::Follow => "1".to_string(),
123            Self::Unfollow => "2".to_string(),
124            Self::Blocked => "3".to_string(),
125            Self::Custom(value) => value.to_string(),
126        }
127    }
128}
129
130/// Parameters for `/session_svr/v1/session_svr/single_unread`.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct MessageSingleUnreadParams {
133    unread_type: SingleUnreadType,
134    show_unfollow_list: bool,
135    show_dustbin: bool,
136    build: String,
137    mobi_app: String,
138}
139
140impl Default for MessageSingleUnreadParams {
141    fn default() -> Self {
142        Self {
143            unread_type: SingleUnreadType::All,
144            show_unfollow_list: false,
145            show_dustbin: false,
146            build: "0".to_string(),
147            mobi_app: "web".to_string(),
148        }
149    }
150}
151
152impl MessageSingleUnreadParams {
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    pub fn with_unread_type(mut self, unread_type: SingleUnreadType) -> Self {
158        if unread_type == SingleUnreadType::Blocked {
159            self.show_dustbin = true;
160        }
161        self.unread_type = unread_type;
162        self
163    }
164
165    pub fn show_unfollow_list(mut self, show: bool) -> Self {
166        self.show_unfollow_list = show;
167        self
168    }
169
170    pub fn show_dustbin(mut self, show: bool) -> Self {
171        self.show_dustbin = show;
172        self
173    }
174
175    pub fn with_custom_unread_type(mut self, unread_type: u32) -> BpiResult<Self> {
176        self.unread_type =
177            SingleUnreadType::Custom(validate_positive_u32("unread_type", unread_type)?);
178        Ok(self)
179    }
180
181    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
182        vec![
183            ("build", self.build.clone()),
184            ("mobi_app", self.mobi_app.clone()),
185            ("unread_type", self.unread_type.as_query_value()),
186            (
187                "show_unfollow_list",
188                bool_flag(self.show_unfollow_list).to_string(),
189            ),
190            ("show_dustbin", bool_flag(self.show_dustbin).to_string()),
191        ]
192    }
193}
194
195fn bool_flag(value: bool) -> &'static str {
196    if value { "1" } else { "0" }
197}
198
199fn validate_positive_u32(field: &'static str, value: u32) -> BpiResult<u32> {
200    if value == 0 {
201        return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
202    }
203
204    Ok(value)
205}
206
207fn validate_positive_u64(field: &'static str, value: u64) -> BpiResult<u64> {
208    if value == 0 {
209        return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
210    }
211
212    Ok(value)
213}
214
215fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
216    let value = value.trim();
217    if value.is_empty() {
218        return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
219    }
220
221    Ok(value.to_string())
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn unread_count_params_serializes_defaults() {
230        let params = MessageUnreadCountParams::new();
231
232        assert_eq!(
233            params.query_pairs(),
234            vec![("build", "0".to_string()), ("mobi_app", "web".to_string())]
235        );
236    }
237
238    #[test]
239    fn unread_count_params_serializes_custom_client() -> BpiResult<()> {
240        let params = MessageUnreadCountParams::new()
241            .with_build("123")?
242            .with_mobi_app("android")?;
243
244        assert_eq!(
245            params.query_pairs(),
246            vec![
247                ("build", "123".to_string()),
248                ("mobi_app", "android".to_string()),
249            ]
250        );
251        Ok(())
252    }
253
254    #[test]
255    fn unread_count_params_rejects_blank_mobi_app() {
256        let err = MessageUnreadCountParams::new()
257            .with_mobi_app("   ")
258            .unwrap_err();
259
260        assert!(matches!(
261            err,
262            BpiError::InvalidParameter {
263                field: "mobi_app",
264                ..
265            }
266        ));
267    }
268
269    #[test]
270    fn reply_feed_params_serializes_defaults() {
271        let params = MessageReplyFeedParams::new();
272
273        assert_eq!(
274            params.query_pairs(),
275            vec![
276                ("build", "0".to_string()),
277                ("mobi_app", "web".to_string()),
278                ("platform", "web".to_string()),
279                ("web_location", String::new()),
280            ]
281        );
282    }
283
284    #[test]
285    fn reply_feed_params_serializes_cursor() -> BpiResult<()> {
286        let params = MessageReplyFeedParams::new()
287            .with_start_id(1001)?
288            .with_start_time(1_700_000_000)?;
289
290        assert_eq!(
291            params.query_pairs(),
292            vec![
293                ("build", "0".to_string()),
294                ("mobi_app", "web".to_string()),
295                ("platform", "web".to_string()),
296                ("web_location", String::new()),
297                ("id", "1001".to_string()),
298                ("reply_time", "1700000000".to_string()),
299            ]
300        );
301        Ok(())
302    }
303
304    #[test]
305    fn reply_feed_params_rejects_zero_start_id() {
306        let err = MessageReplyFeedParams::new().with_start_id(0).unwrap_err();
307
308        assert!(matches!(
309            err,
310            BpiError::InvalidParameter { field: "id", .. }
311        ));
312    }
313
314    #[test]
315    fn single_unread_params_serializes_defaults() {
316        let params = MessageSingleUnreadParams::new();
317
318        assert_eq!(
319            params.query_pairs(),
320            vec![
321                ("build", "0".to_string()),
322                ("mobi_app", "web".to_string()),
323                ("unread_type", "0".to_string()),
324                ("show_unfollow_list", "0".to_string()),
325                ("show_dustbin", "0".to_string()),
326            ]
327        );
328    }
329
330    #[test]
331    fn single_unread_params_serializes_flags() {
332        let params = MessageSingleUnreadParams::new()
333            .with_unread_type(SingleUnreadType::Follow)
334            .show_unfollow_list(true)
335            .show_dustbin(true);
336
337        assert_eq!(
338            params.query_pairs(),
339            vec![
340                ("build", "0".to_string()),
341                ("mobi_app", "web".to_string()),
342                ("unread_type", "1".to_string()),
343                ("show_unfollow_list", "1".to_string()),
344                ("show_dustbin", "1".to_string()),
345            ]
346        );
347    }
348
349    #[test]
350    fn single_unread_params_enables_dustbin_for_blocked_type() {
351        let params = MessageSingleUnreadParams::new().with_unread_type(SingleUnreadType::Blocked);
352
353        assert_eq!(params.query_pairs()[4], ("show_dustbin", "1".to_string()));
354    }
355
356    #[test]
357    fn single_unread_params_rejects_zero_custom_type() {
358        let err = MessageSingleUnreadParams::new()
359            .with_custom_unread_type(0)
360            .unwrap_err();
361
362        assert!(matches!(
363            err,
364            BpiError::InvalidParameter {
365                field: "unread_type",
366                ..
367            }
368        ));
369    }
370}