Skip to main content

bpi_rs/dynamic/
params.rs

1use crate::ids::{DynamicId, Mid};
2use crate::{BpiError, BpiResult};
3
4const DEFAULT_ALL_FEATURES: &str = "itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard,onlyfansAssetsV2,forwardListHidden,ugcDelete";
5const DEFAULT_ALL_WEB_LOCATION: &str = "333.1365";
6const DEFAULT_DETAIL_FEATURES: &str = "htmlNewStyle,itemOpusStyle,decorationCard";
7
8/// Parameters for `/x/polymer/web-dynamic/v1/feed/all`.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DynamicAllParams {
11    features: String,
12    web_location: String,
13    host_mid: Option<Mid>,
14    offset: Option<String>,
15    update_baseline: Option<String>,
16}
17
18impl Default for DynamicAllParams {
19    fn default() -> Self {
20        Self {
21            features: DEFAULT_ALL_FEATURES.to_string(),
22            web_location: DEFAULT_ALL_WEB_LOCATION.to_string(),
23            host_mid: None,
24            offset: None,
25            update_baseline: None,
26        }
27    }
28}
29
30impl DynamicAllParams {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    pub fn with_features(mut self, features: impl Into<String>) -> BpiResult<Self> {
36        self.features = normalize_non_blank("features", features.into())?;
37        Ok(self)
38    }
39
40    pub fn with_web_location(mut self, web_location: impl Into<String>) -> BpiResult<Self> {
41        self.web_location = normalize_non_blank("web_location", web_location.into())?;
42        Ok(self)
43    }
44
45    pub fn with_host_mid(mut self, host_mid: Mid) -> Self {
46        self.host_mid = Some(host_mid);
47        self
48    }
49
50    pub fn with_offset(mut self, offset: impl Into<String>) -> BpiResult<Self> {
51        self.offset = Some(normalize_non_blank("offset", offset.into())?);
52        Ok(self)
53    }
54
55    pub fn with_update_baseline(mut self, update_baseline: impl Into<String>) -> BpiResult<Self> {
56        self.update_baseline = Some(normalize_non_blank(
57            "update_baseline",
58            update_baseline.into(),
59        )?);
60        Ok(self)
61    }
62
63    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
64        let mut query = vec![
65            ("features", self.features.clone()),
66            ("web_location", self.web_location.clone()),
67        ];
68
69        if let Some(host_mid) = self.host_mid {
70            query.push(("host_mid", host_mid.to_string()));
71        }
72        if let Some(offset) = &self.offset {
73            query.push(("offset", offset.clone()));
74        }
75        if let Some(update_baseline) = &self.update_baseline {
76            query.push(("update_baseline", update_baseline.clone()));
77        }
78
79        query
80    }
81}
82
83/// Parameters for `/x/polymer/web-dynamic/v1/feed/all/update`.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct DynamicCheckNewParams {
86    update_baseline: String,
87    typ: Option<String>,
88}
89
90impl DynamicCheckNewParams {
91    pub fn new(update_baseline: impl Into<String>) -> BpiResult<Self> {
92        Ok(Self {
93            update_baseline: normalize_non_blank("update_baseline", update_baseline.into())?,
94            typ: None,
95        })
96    }
97
98    pub fn with_type(mut self, typ: impl Into<String>) -> BpiResult<Self> {
99        self.typ = Some(normalize_non_blank("type", typ.into())?);
100        Ok(self)
101    }
102
103    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
104        let mut query = vec![("update_baseline", self.update_baseline.clone())];
105        if let Some(typ) = &self.typ {
106            query.push(("type", typ.clone()));
107        }
108
109        query
110    }
111}
112
113/// Parameters for `/x/polymer/web-dynamic/v1/feed/nav`.
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct DynamicNavFeedParams {
116    update_baseline: Option<String>,
117    offset: Option<String>,
118}
119
120impl DynamicNavFeedParams {
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    pub fn with_update_baseline(mut self, update_baseline: impl Into<String>) -> BpiResult<Self> {
126        self.update_baseline = Some(normalize_non_blank(
127            "update_baseline",
128            update_baseline.into(),
129        )?);
130        Ok(self)
131    }
132
133    pub fn with_offset(mut self, offset: impl Into<String>) -> BpiResult<Self> {
134        self.offset = Some(normalize_non_blank("offset", offset.into())?);
135        Ok(self)
136    }
137
138    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
139        optional_cursor_query(self.update_baseline.as_deref(), self.offset.as_deref())
140    }
141}
142
143/// Parameters for `/dynamic_svr/v1/dynamic_svr/w_live_users`.
144#[derive(Debug, Clone, Default, PartialEq, Eq)]
145pub struct DynamicLiveUsersParams {
146    size: Option<u32>,
147}
148
149impl DynamicLiveUsersParams {
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    pub fn with_size(mut self, size: u32) -> BpiResult<Self> {
155        if size == 0 {
156            return Err(BpiError::invalid_parameter(
157                "size",
158                "value must be non-zero",
159            ));
160        }
161
162        self.size = Some(size);
163        Ok(self)
164    }
165
166    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
167        self.size
168            .map(|size| vec![("size", size.to_string())])
169            .unwrap_or_default()
170    }
171}
172
173/// Parameters for `/dynamic_svr/v1/dynamic_svr/w_dyn_uplist`.
174#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct DynamicUpUsersParams {
176    teenagers_mode: bool,
177}
178
179impl DynamicUpUsersParams {
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    pub fn with_teenagers_mode(mut self, teenagers_mode: bool) -> Self {
185        self.teenagers_mode = teenagers_mode;
186        self
187    }
188
189    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
190        vec![("teenagers_mode", u8::from(self.teenagers_mode).to_string())]
191    }
192}
193
194/// Parameters for `/x/polymer/web-dynamic/v1/detail`.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct DynamicDetailParams {
197    id: DynamicId,
198    features: String,
199}
200
201impl DynamicDetailParams {
202    pub fn new(id: DynamicId) -> Self {
203        Self {
204            id,
205            features: DEFAULT_DETAIL_FEATURES.to_string(),
206        }
207    }
208
209    pub fn with_features(mut self, features: impl Into<String>) -> BpiResult<Self> {
210        self.features = normalize_non_blank("features", features.into())?;
211        Ok(self)
212    }
213
214    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
215        vec![
216            ("id", self.id.to_string()),
217            ("features", self.features.clone()),
218        ]
219    }
220}
221
222/// Parameters for `/x/polymer/web-dynamic/v1/detail/reaction`.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct DynamicReactionsParams {
225    id: DynamicId,
226    offset: Option<String>,
227}
228
229impl DynamicReactionsParams {
230    pub fn new(id: DynamicId) -> Self {
231        Self { id, offset: None }
232    }
233
234    pub fn with_offset(mut self, offset: impl Into<String>) -> BpiResult<Self> {
235        self.offset = Some(normalize_non_blank("offset", offset.into())?);
236        Ok(self)
237    }
238
239    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
240        dynamic_offset_query(&self.id, self.offset.as_deref())
241    }
242}
243
244/// Parameters for `/lottery_svr/v1/lottery_svr/lottery_notice`.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct DynamicLotteryNoticeParams {
247    business_id: DynamicId,
248}
249
250impl DynamicLotteryNoticeParams {
251    pub fn new(business_id: DynamicId) -> Self {
252        Self { business_id }
253    }
254
255    pub fn query_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
256        vec![
257            ("business_id", self.business_id.to_string()),
258            ("business_type", "1".to_string()),
259            ("csrf", csrf.to_string()),
260        ]
261    }
262}
263
264/// Parameters for `/x/polymer/web-dynamic/v1/detail/forward`.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct DynamicForwardsParams {
267    id: DynamicId,
268    offset: Option<String>,
269}
270
271impl DynamicForwardsParams {
272    pub fn new(id: DynamicId) -> Self {
273        Self { id, offset: None }
274    }
275
276    pub fn with_offset(mut self, offset: impl Into<String>) -> BpiResult<Self> {
277        self.offset = Some(normalize_non_blank("offset", offset.into())?);
278        Ok(self)
279    }
280
281    pub fn query_pairs(&self) -> Vec<(&'static str, String)> {
282        dynamic_offset_query(&self.id, self.offset.as_deref())
283    }
284}
285
286/// Parameters for `/x/polymer/web-dynamic/v1/detail/pic`.
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct DynamicPicsParams {
289    id: DynamicId,
290}
291
292impl DynamicPicsParams {
293    pub fn new(id: DynamicId) -> Self {
294        Self { id }
295    }
296
297    pub fn query_pairs(&self) -> [(&'static str, String); 1] {
298        [("id", self.id.to_string())]
299    }
300}
301
302/// Parameters for `/x/polymer/web-dynamic/v1/detail/forward/item`.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct DynamicForwardItemParams {
305    id: DynamicId,
306}
307
308impl DynamicForwardItemParams {
309    pub fn new(id: DynamicId) -> Self {
310        Self { id }
311    }
312
313    pub fn query_pairs(&self) -> [(&'static str, String); 1] {
314        [("id", self.id.to_string())]
315    }
316}
317
318fn dynamic_offset_query(id: &DynamicId, offset: Option<&str>) -> Vec<(&'static str, String)> {
319    let mut query = vec![("id", id.to_string())];
320    if let Some(offset) = offset {
321        query.push(("offset", offset.to_string()));
322    }
323    query
324}
325
326fn optional_cursor_query(
327    update_baseline: Option<&str>,
328    offset: Option<&str>,
329) -> Vec<(&'static str, String)> {
330    let mut query = Vec::new();
331    if let Some(update_baseline) = update_baseline {
332        query.push(("update_baseline", update_baseline.to_string()));
333    }
334    if let Some(offset) = offset {
335        query.push(("offset", offset.to_string()));
336    }
337
338    query
339}
340
341fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
342    let value = value.trim();
343    if value.is_empty() {
344        return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
345    }
346
347    Ok(value.to_string())
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn all_params_serializes_defaults() {
356        let params = DynamicAllParams::new();
357
358        assert_eq!(
359            params.query_pairs(),
360            vec![
361                (
362                    "features",
363                    "itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard,onlyfansAssetsV2,forwardListHidden,ugcDelete".to_string(),
364                ),
365                ("web_location", "333.1365".to_string()),
366            ]
367        );
368    }
369
370    #[test]
371    fn all_params_serializes_optional_filters() -> BpiResult<()> {
372        let params = DynamicAllParams::new()
373            .with_host_mid(Mid::new(12345)?)
374            .with_offset("offset-token")?
375            .with_update_baseline("baseline-token")?;
376
377        assert_eq!(
378            params.query_pairs(),
379            vec![
380                (
381                    "features",
382                    "itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard,onlyfansAssetsV2,forwardListHidden,ugcDelete".to_string(),
383                ),
384                ("web_location", "333.1365".to_string()),
385                ("host_mid", "12345".to_string()),
386                ("offset", "offset-token".to_string()),
387                ("update_baseline", "baseline-token".to_string()),
388            ]
389        );
390        Ok(())
391    }
392
393    #[test]
394    fn all_params_rejects_blank_offset() {
395        let err = DynamicAllParams::new().with_offset("   ").unwrap_err();
396
397        assert!(matches!(
398            err,
399            BpiError::InvalidParameter {
400                field: "offset",
401                ..
402            }
403        ));
404    }
405
406    #[test]
407    fn check_new_params_serializes_required_baseline() -> BpiResult<()> {
408        let params = DynamicCheckNewParams::new("baseline-token")?;
409
410        assert_eq!(
411            params.query_pairs(),
412            vec![("update_baseline", "baseline-token".to_string())]
413        );
414        Ok(())
415    }
416
417    #[test]
418    fn check_new_params_serializes_type_filter() -> BpiResult<()> {
419        let params = DynamicCheckNewParams::new("baseline-token")?.with_type("video")?;
420
421        assert_eq!(
422            params.query_pairs(),
423            vec![
424                ("update_baseline", "baseline-token".to_string()),
425                ("type", "video".to_string()),
426            ]
427        );
428        Ok(())
429    }
430
431    #[test]
432    fn nav_feed_params_serializes_optional_cursor() -> BpiResult<()> {
433        let params = DynamicNavFeedParams::new()
434            .with_update_baseline("baseline-token")?
435            .with_offset("offset-token")?;
436
437        assert_eq!(
438            params.query_pairs(),
439            vec![
440                ("update_baseline", "baseline-token".to_string()),
441                ("offset", "offset-token".to_string()),
442            ]
443        );
444        Ok(())
445    }
446
447    #[test]
448    fn live_users_params_serializes_size() -> BpiResult<()> {
449        let params = DynamicLiveUsersParams::new().with_size(20)?;
450
451        assert_eq!(params.query_pairs(), vec![("size", "20".to_string())]);
452        Ok(())
453    }
454
455    #[test]
456    fn live_users_params_rejects_zero_size() {
457        let err = DynamicLiveUsersParams::new().with_size(0).unwrap_err();
458
459        assert!(matches!(
460            err,
461            BpiError::InvalidParameter { field: "size", .. }
462        ));
463    }
464
465    #[test]
466    fn up_users_params_serializes_default_teenagers_mode() {
467        let params = DynamicUpUsersParams::new();
468
469        assert_eq!(
470            params.query_pairs(),
471            vec![("teenagers_mode", "0".to_string())]
472        );
473    }
474
475    #[test]
476    fn up_users_params_serializes_enabled_teenagers_mode() {
477        let params = DynamicUpUsersParams::new().with_teenagers_mode(true);
478
479        assert_eq!(
480            params.query_pairs(),
481            vec![("teenagers_mode", "1".to_string())]
482        );
483    }
484}