Skip to main content

fishpi_sdk/model/
user.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::utils::error::Error;
5
6#[derive(Clone, Serialize, Deserialize, Default)]
7#[allow(non_snake_case)]
8pub struct UserInfo {
9    /// 用户id
10    #[serde(rename = "oId")]
11    oId: String,
12    /// 用户编号
13    userNo: String,
14    /// 用户名
15    userName: String,
16    /// 用户昵称
17    userNickname: String,
18    /// 首页地址
19    #[serde(rename = "userURL")]
20    URL: String,
21    /// 所在城市
22    #[serde(rename = "userCity")]
23    city: String,
24    /// 签名
25    #[serde(rename = "userIntro")]
26    intro: String,
27    /// 是否在线
28    #[serde(rename = "userOnlineFlag")]
29    online: bool,
30    /// 用户积分
31    #[serde(rename = "userPoint")]
32    points: i32,
33    /// 用户组
34    #[serde(rename = "userRole")]
35    role: String,
36    /// 角色
37    #[serde(rename = "userAppRole")]
38    appRole: UserAppRole,
39    /// 头像地址
40    #[serde(rename = "userAvatarURL")]
41    avatar: String,
42    /// 用户卡片背景
43    cardBg: String,
44    /// 用户关注数
45    #[serde(rename = "followingUserCount")]
46    following: i32,
47    /// 用户粉丝数
48    #[serde(rename = "followerCount")]
49    follower: i32,
50    /// 在线时长(分钟)
51    #[serde(rename = "onlineMinute")]
52    onlineMinutes: i32,
53    // / 是否已经关注,未登录则为 `hide`
54    // canFollow: String,
55    // / 用户所有勋章列表,包含未佩戴
56    // ownedMetal: Vec<Metal>,
57    /// 用户勋章列表
58    sysMetal: Vec<Metal>,
59    // / MBTI 性格类型
60    // mbti: String,
61}
62
63impl UserInfo {
64    pub fn from_value(data: &Value) -> Result<Self, Error> {
65        let mut data = data.clone();
66
67        if let Some(sys_metal_str) = data["sysMetal"].as_str() {
68            let metals = to_metal(sys_metal_str).map_err(|e| Error::Parse(e.to_string()))?;
69            data["sysMetal"] =
70                serde_json::to_value(metals).map_err(|e| Error::Parse(e.to_string()))?;
71        }
72
73        if let Some(owned_metal_str) = data["ownedMetal"].as_str() {
74            let metals = to_metal(owned_metal_str).map_err(|e| Error::Parse(e.to_string()))?;
75            data["ownedMetal"] =
76                serde_json::to_value(metals).map_err(|e| Error::Parse(e.to_string()))?;
77        }
78
79        serde_json::from_value(data)
80            .map_err(|e| Error::Parse(format!("Failed to parse UserInfo: {}", e)))
81    }
82}
83
84/// 更新用户信息参数
85#[derive(Clone, Serialize, Deserialize)]
86#[allow(non_snake_case)]
87pub struct UpdateUserInfoParams {
88    /// 用户昵称
89    pub nickName: Option<String>,
90    ///  用户标签,多个标签用逗号分隔
91    pub userTag: Option<String>,
92    /// 个人主页 URL
93    pub userUrl: Option<String>,
94    /// 个人简介
95    pub userIntro: Option<String>,
96    /// MBTI 性格类型(例如:ENFP)
97    pub mbti: Option<String>,
98}
99
100#[derive(Clone, Serialize, Deserialize, Default)]
101#[repr(u8)]
102#[serde(try_from = "String")]
103enum UserAppRole {
104    /// 黑客
105    #[default]
106    Hack = 0,
107    /// 画家
108    Artist = 1,
109}
110
111#[derive(Clone, Serialize, Deserialize, Default, Debug)]
112pub struct MetalBase {
113    pub attr: MetalAttrOrString,
114    pub name: String,
115    pub description: String,
116    pub data: String,
117}
118
119#[derive(Clone, Serialize, Deserialize, Default, Debug)]
120pub struct MetalAttr {
121    /// 徽标图地址
122    url: String,
123    /// 背景色
124    backcolor: String,
125    /// 文字颜色
126    fontcolor: String,
127    /// 版本号
128    ver: f32,
129    /// 缩放比例
130    scale: f32,
131}
132
133#[derive(Clone, Serialize, Deserialize, Debug)]
134pub enum MetalAttrOrString {
135    Attr(MetalAttr),
136    Str(String),
137}
138
139impl std::fmt::Display for MetalAttrOrString {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            MetalAttrOrString::Attr(attr) => {
143                write!(
144                    f,
145                    "ver={}&scale={}&backcolor={}&fontcolor={}&url={}",
146                    attr.ver, attr.scale, attr.backcolor, attr.fontcolor, attr.url
147                )
148            }
149            MetalAttrOrString::Str(s) => write!(f, "{}", s),
150        }
151    }
152}
153
154#[derive(Clone, Serialize, Deserialize, Debug)]
155pub struct Metal {
156    /// 徽章基本信息
157    base: MetalBase,
158    /// 完整徽章地址(含文字)
159    url: String,
160    /// 徽章地址(不含文字)
161    icon: String,
162    /// 是否佩戴
163    enable: bool,
164}
165
166#[derive(Clone, Deserialize)]
167#[allow(non_snake_case)]
168pub struct AtUser {
169    /// 用户名
170    pub userName: String,
171    /// 用户头像
172    pub userAvatarURL: String,
173    /// 全小写用户名
174    pub userNameLowerCase: String,
175}
176
177impl AtUser {
178    pub fn from_value(value: &Value) -> Result<Self, Error> {
179        serde_json::from_value(value.clone())
180            .map_err(|e| Error::Parse(format!("Failed to parse AtUser: {}", e)))
181    }
182}
183
184impl From<u8> for UserAppRole {
185    fn from(value: u8) -> Self {
186        match value {
187            0 => UserAppRole::Hack,
188            1 => UserAppRole::Artist,
189            _ => UserAppRole::Hack,
190        }
191    }
192}
193
194impl From<UserAppRole> for u8 {
195    fn from(value: UserAppRole) -> Self {
196        match value {
197            UserAppRole::Hack => 0,
198            UserAppRole::Artist => 1,
199        }
200    }
201}
202
203impl TryFrom<String> for UserAppRole {
204    type Error = String;
205
206    fn try_from(value: String) -> Result<Self, Self::Error> {
207        let num: u8 = value.parse().map_err(|_| "Invalid number".to_string())?;
208        Ok(UserAppRole::from(num))
209    }
210}
211
212impl UserInfo {
213    pub fn name(&self) -> &str {
214        if self.userNickname.is_empty() {
215            &self.userName
216        } else {
217            &self.userNickname
218        }
219    }
220
221    pub fn username(&self) -> &str {
222        &self.userName
223    }
224
225    pub fn nickname(&self) -> &str {
226        &self.userNickname
227    }
228
229    pub fn intro(&self) -> &str {
230        &self.intro
231    }
232
233    pub fn city(&self) -> &str {
234        &self.city
235    }
236
237    pub fn url(&self) -> &str {
238        &self.URL
239    }
240
241    pub fn avatar(&self) -> &str {
242        &self.avatar
243    }
244
245    pub fn points(&self) -> i32 {
246        self.points
247    }
248
249    pub fn role(&self) -> &str {
250        &self.role
251    }
252
253    pub fn following(&self) -> i32 {
254        self.following
255    }
256
257    pub fn follower(&self) -> i32 {
258        self.follower
259    }
260
261    pub fn online_minutes(&self) -> i32 {
262        self.onlineMinutes
263    }
264}
265
266impl Default for MetalAttrOrString {
267    fn default() -> Self {
268        MetalAttrOrString::Attr(MetalAttr::default())
269    }
270}
271
272/// 共同trait
273#[allow(dead_code)]
274trait MetalCommon {
275    fn attr(&self) -> &MetalAttrOrString;
276    fn name(&self) -> &str;
277    fn description(&self) -> &str;
278    fn data(&self) -> &str;
279    fn to_url(&self, include_text: bool) -> String {
280        let domain = "fishpi.cn";
281        let attr_str = match self.attr() {
282            MetalAttrOrString::Attr(attr) => {
283                format!(
284                    "ver={}&scale={}&backcolor={}&fontcolor={}",
285                    attr.ver, attr.scale, attr.backcolor, attr.fontcolor
286                )
287            }
288            MetalAttrOrString::Str(s) => s.clone(),
289        };
290        let text_str = if include_text {
291            self.name().to_string()
292        } else {
293            "".to_string()
294        };
295        format!("https://{}/gen?txt={}&{}", domain, text_str, attr_str)
296    }
297}
298
299impl MetalCommon for MetalBase {
300    fn attr(&self) -> &MetalAttrOrString {
301        &self.attr
302    }
303    fn name(&self) -> &str {
304        &self.name
305    }
306    fn description(&self) -> &str {
307        &self.description
308    }
309    fn data(&self) -> &str {
310        &self.data
311    }
312}
313
314#[allow(dead_code)]
315impl MetalBase {
316    fn new(metal: Option<&Self>) -> Self {
317        metal.cloned().unwrap_or_default()
318    }
319}
320
321impl Default for Metal {
322    fn default() -> Self {
323        Self {
324            base: MetalBase::default(),
325            url: String::default(),
326            icon: String::default(),
327            enable: true,
328        }
329    }
330}
331
332impl MetalCommon for Metal {
333    fn attr(&self) -> &MetalAttrOrString {
334        self.base.attr()
335    }
336    fn name(&self) -> &str {
337        self.base.name()
338    }
339    fn description(&self) -> &str {
340        self.base.description()
341    }
342    fn data(&self) -> &str {
343        self.base.data()
344    }
345}
346
347#[allow(dead_code)]
348impl Metal {
349    fn new(metal: Option<&Self>) -> Self {
350        let base = MetalBase::new(metal.map(|m| &m.base));
351        Self {
352            base,
353            ..Default::default()
354        }
355    }
356}
357
358pub fn to_metal(sys_metal: &str) -> Result<Vec<Metal>, Box<dyn std::error::Error>> {
359    let parsed: Value = serde_json::from_str(sys_metal)?;
360    let list = parsed["list"].as_array().ok_or("no list in sysMetal")?;
361    let mut metals = Vec::new();
362    for item in list {
363        let attr_str = item["attr"].as_str().unwrap_or("");
364        let base = MetalBase {
365            attr: analyze_metal_attr(attr_str),
366            name: item["name"].as_str().unwrap_or("").to_string(),
367            description: item["description"].as_str().unwrap_or("").to_string(),
368            data: item["data"].as_str().unwrap_or("").to_string(),
369        };
370        let url = base.to_url(true);
371        let icon = base.to_url(false);
372        let enable = item["enabled"].as_bool().unwrap_or(true);
373        metals.push(Metal {
374            base,
375            url,
376            icon,
377            enable,
378        });
379    }
380    Ok(metals)
381}
382
383pub fn analyze_metal_attr(attr_str: &str) -> MetalAttrOrString {
384    if attr_str.is_empty() {
385        return MetalAttrOrString::Str("".to_string());
386    }
387    let mut url = String::new();
388    let mut backcolor = String::new();
389    let mut fontcolor = String::new();
390    let mut ver = 1.0;
391    let mut scale = 0.79;
392    for pair in attr_str.split('&') {
393        let mut parts = pair.split('=');
394        if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
395            match key {
396                "url" => url = value.to_string(),
397                "backcolor" => backcolor = value.to_string(),
398                "fontcolor" => fontcolor = value.to_string(),
399                "ver" => ver = value.parse().unwrap_or(1.0),
400                "scale" => scale = value.parse().unwrap_or(0.79),
401                _ => {}
402            }
403        }
404    }
405    if url.is_empty() && backcolor.is_empty() && fontcolor.is_empty() {
406        MetalAttrOrString::Str(attr_str.to_string())
407    } else {
408        MetalAttrOrString::Attr(MetalAttr {
409            url,
410            backcolor,
411            fontcolor,
412            ver,
413            scale,
414        })
415    }
416}
417
418#[derive(Clone, Serialize, Deserialize, Debug)]
419pub struct UserPoint {
420    #[serde(rename = "userPoint")]
421    pub point: u32,
422    #[serde(rename = "userName")]
423    pub name: String,
424}
425
426impl UserPoint {
427    pub fn from_value(data: &Value) -> Result<Self, Error> {
428        serde_json::from_value(data["data"].clone())
429            .map_err(|e| Error::Parse(format!("Failed to parse UserPoint: {}", e)))
430    }
431}