1use crate::BilibiliRequest;
8use crate::BpiError;
9use crate::BpiResult;
10use crate::user::UserClient;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct CreateTagResponseData {
17 pub tagid: i64,
19}
20
21#[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 {}