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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}