Skip to main content

bpi_rs/user/relation/
group.rs

1// B站用户分组相关接口
2//
3// [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
4
5// --- 响应数据结构体 ---
6
7use crate::BilibiliRequest;
8use crate::BpiError;
9use crate::BpiResult;
10use crate::user::UserClient;
11use serde::{Deserialize, Serialize};
12
13/// 创建分组响应数据
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct CreateTagResponseData {
17    /// 创建的分组的 ID
18    pub tagid: i64,
19}
20
21// --- API 实现 ---
22
23// --- 测试模块 ---
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct UserGroupCreateParams {
27    group_name: String,
28}
29
30impl UserGroupCreateParams {
31    pub fn new(group_name: impl Into<String>) -> BpiResult<Self> {
32        Ok(Self {
33            group_name: normalize_name("group_name", group_name.into())?,
34        })
35    }
36
37    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
38        reqwest::multipart::Form::new()
39            .text("tag", self.group_name)
40            .text("csrf", csrf.to_string())
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct UserGroupUpdateParams {
46    tag_id: i64,
47    new_name: String,
48}
49
50impl UserGroupUpdateParams {
51    pub fn new(tag_id: i64, new_name: impl Into<String>) -> BpiResult<Self> {
52        Ok(Self {
53            tag_id: validate_positive_i64("tag_id", tag_id)?,
54            new_name: normalize_name("new_name", new_name.into())?,
55        })
56    }
57
58    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
59        reqwest::multipart::Form::new()
60            .text("tagid", self.tag_id.to_string())
61            .text("name", self.new_name)
62            .text("csrf", csrf.to_string())
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct UserGroupDeleteParams {
68    tag_id: i64,
69}
70
71impl UserGroupDeleteParams {
72    pub fn new(tag_id: i64) -> BpiResult<Self> {
73        Ok(Self {
74            tag_id: validate_positive_i64("tag_id", tag_id)?,
75        })
76    }
77
78    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
79        reqwest::multipart::Form::new()
80            .text("tagid", self.tag_id.to_string())
81            .text("csrf", csrf.to_string())
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct UserGroupUsersParams {
87    fids: String,
88    tagids: String,
89}
90
91impl UserGroupUsersParams {
92    pub fn new(fids: &[u64], tagids: &[i64]) -> BpiResult<Self> {
93        Ok(Self {
94            fids: join_u64_ids("fids", fids)?,
95            tagids: join_i64_ids("tagids", tagids)?,
96        })
97    }
98
99    pub fn remove_from_all(fids: &[u64]) -> BpiResult<Self> {
100        Ok(Self {
101            fids: join_u64_ids("fids", fids)?,
102            tagids: "0".to_string(),
103        })
104    }
105
106    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
107        reqwest::multipart::Form::new()
108            .text("fids", self.fids)
109            .text("tagids", self.tagids)
110            .text("csrf", csrf.to_string())
111    }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct UserGroupMoveUsersParams {
116    fids: String,
117    before_tag_ids: String,
118    after_tag_ids: String,
119}
120
121impl UserGroupMoveUsersParams {
122    pub fn new(fids: &[u64], before_tag_ids: &[i64], after_tag_ids: &[i64]) -> BpiResult<Self> {
123        Ok(Self {
124            fids: join_u64_ids("fids", fids)?,
125            before_tag_ids: join_i64_ids("before_tag_ids", before_tag_ids)?,
126            after_tag_ids: join_i64_ids("after_tag_ids", after_tag_ids)?,
127        })
128    }
129
130    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
131        reqwest::multipart::Form::new()
132            .text("fids", self.fids)
133            .text("beforeTagids", self.before_tag_ids)
134            .text("afterTagids", self.after_tag_ids)
135            .text("csrf", csrf.to_string())
136    }
137}
138
139impl<'a> UserClient<'a> {
140    pub async fn create_group_tag(
141        &self,
142        params: UserGroupCreateParams,
143    ) -> BpiResult<CreateTagResponseData> {
144        let csrf = self.client.csrf()?;
145        let form = params.into_multipart(&csrf);
146
147        self.client
148            .post("https://api.bilibili.com/x/relation/tag/create")
149            .multipart(form)
150            .send_bpi_payload("user.group.create")
151            .await
152    }
153
154    pub async fn update_group_tag(
155        &self,
156        params: UserGroupUpdateParams,
157    ) -> BpiResult<Option<serde_json::Value>> {
158        let csrf = self.client.csrf()?;
159        let form = params.into_multipart(&csrf);
160
161        self.client
162            .post("https://api.bilibili.com/x/relation/tag/update")
163            .multipart(form)
164            .send_bpi_optional_payload("user.group.update")
165            .await
166    }
167
168    pub async fn delete_group_tag(
169        &self,
170        params: UserGroupDeleteParams,
171    ) -> BpiResult<Option<serde_json::Value>> {
172        let csrf = self.client.csrf()?;
173        let form = params.into_multipart(&csrf);
174
175        self.client
176            .post("https://api.bilibili.com/x/relation/tag/del")
177            .multipart(form)
178            .send_bpi_optional_payload("user.group.delete")
179            .await
180    }
181
182    pub async fn add_group_users_to_tags(
183        &self,
184        params: UserGroupUsersParams,
185    ) -> BpiResult<Option<serde_json::Value>> {
186        let csrf = self.client.csrf()?;
187        let form = params.into_multipart(&csrf);
188
189        self.client
190            .post("https://api.bilibili.com/x/relation/tags/addUsers")
191            .multipart(form)
192            .send_bpi_optional_payload("user.group.users.add")
193            .await
194    }
195
196    pub async fn remove_group_users(
197        &self,
198        params: UserGroupUsersParams,
199    ) -> BpiResult<Option<serde_json::Value>> {
200        let csrf = self.client.csrf()?;
201        let form = params.into_multipart(&csrf);
202
203        self.client
204            .post("https://api.bilibili.com/x/relation/tags/addUsers")
205            .multipart(form)
206            .send_bpi_optional_payload("user.group.users.remove")
207            .await
208    }
209
210    pub async fn copy_group_users_to_tags(
211        &self,
212        params: UserGroupUsersParams,
213    ) -> BpiResult<Option<serde_json::Value>> {
214        let csrf = self.client.csrf()?;
215        let form = params.into_multipart(&csrf);
216
217        self.client
218            .post("https://api.bilibili.com/x/relation/tags/copyUsers")
219            .multipart(form)
220            .send_bpi_optional_payload("user.group.users.copy")
221            .await
222    }
223
224    pub async fn move_group_users_to_tags(
225        &self,
226        params: UserGroupMoveUsersParams,
227    ) -> BpiResult<Option<serde_json::Value>> {
228        let csrf = self.client.csrf()?;
229        let form = params.into_multipart(&csrf);
230
231        self.client
232            .post("https://api.bilibili.com/x/relation/tags/moveUsers")
233            .multipart(form)
234            .send_bpi_optional_payload("user.group.users.move")
235            .await
236    }
237}
238
239fn normalize_name(field: &'static str, value: String) -> BpiResult<String> {
240    let value = value.trim().to_string();
241    if value.is_empty() {
242        return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
243    }
244    if value.len() > 16 {
245        return Err(BpiError::invalid_parameter(
246            field,
247            "length cannot exceed 16 bytes",
248        ));
249    }
250
251    Ok(value)
252}
253
254fn validate_positive_i64(field: &'static str, value: i64) -> BpiResult<i64> {
255    if value <= 0 {
256        return Err(BpiError::invalid_parameter(field, "id must be positive"));
257    }
258
259    Ok(value)
260}
261
262fn join_u64_ids(field: &'static str, values: &[u64]) -> BpiResult<String> {
263    if values.is_empty() || values.contains(&0) {
264        return Err(BpiError::invalid_parameter(
265            field,
266            "ids must be non-empty and non-zero",
267        ));
268    }
269
270    Ok(values
271        .iter()
272        .map(u64::to_string)
273        .collect::<Vec<_>>()
274        .join(","))
275}
276
277fn join_i64_ids(field: &'static str, values: &[i64]) -> BpiResult<String> {
278    if values.is_empty() || values.iter().any(|value| *value <= 0) {
279        return Err(BpiError::invalid_parameter(
280            field,
281            "ids must be non-empty and positive",
282        ));
283    }
284
285    Ok(values
286        .iter()
287        .map(i64::to_string)
288        .collect::<Vec<_>>()
289        .join(","))
290}
291
292#[cfg(test)]
293mod tests {}