use crate::models::api::BotInfo;
use crate::models::{HasId, Snowflake, Timestamp};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct User {
#[serde(default)]
pub id: Snowflake,
#[serde(default)]
pub username: String,
#[serde(default)]
pub avatar: String,
#[serde(default)]
pub bot: bool,
#[serde(default)]
pub union_openid: String,
#[serde(default)]
pub union_user_account: String,
}
impl User {
pub fn new(id: impl Into<Snowflake>, username: impl Into<String>) -> Self {
Self {
id: id.into(),
username: username.into(),
avatar: String::new(),
bot: false,
union_openid: String::new(),
union_user_account: String::new(),
}
}
pub fn from_data(data: serde_json::Value) -> Self {
serde_json::from_value(data).unwrap_or_default()
}
pub fn avatar_url(&self) -> Option<String> {
(!self.avatar.is_empty()).then(|| {
format!(
"https://thirdqq.qlogo.cn/headimg_dl?dst_uin={}&spec=640",
self.id
)
})
}
pub fn display_name(&self) -> &str {
&self.username
}
pub fn is_bot(&self) -> bool {
self.bot
}
pub fn is_human(&self) -> bool {
!self.bot
}
pub fn mention(&self) -> String {
format!("<@!{}>", self.id)
}
}
impl HasId for User {
fn id(&self) -> Option<&Snowflake> {
Some(&self.id)
}
}
impl From<BotInfo> for User {
fn from(bot: BotInfo) -> Self {
Self {
id: bot.id,
username: bot.username,
avatar: bot.avatar.unwrap_or_default(),
bot: bot.bot,
union_openid: String::new(),
union_user_account: String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Member {
#[serde(flatten)]
pub user: User,
pub nick: Option<String>,
pub roles: Vec<Snowflake>,
pub joined_at: Timestamp,
#[serde(default)]
pub deaf: bool,
#[serde(default)]
pub mute: bool,
}
impl Member {
pub fn new(user: User, joined_at: Timestamp) -> Self {
Self {
user,
nick: None,
roles: Vec::new(),
joined_at,
deaf: false,
mute: false,
}
}
pub fn from_data(data: serde_json::Value) -> Self {
let wire: MemberWire = serde_json::from_value(data).unwrap_or_default();
Self {
user: wire.user,
nick: wire.nick,
roles: wire.roles,
joined_at: wire.joined_at,
deaf: wire.deaf,
mute: wire.mute,
}
}
pub fn display_name(&self) -> &str {
self.nick.as_deref().unwrap_or(&self.user.username)
}
pub fn mention(&self) -> String {
self.user.mention()
}
pub fn has_role(&self, role_id: &Snowflake) -> bool {
self.roles.contains(role_id)
}
pub fn has_any_role(&self, role_ids: &[Snowflake]) -> bool {
role_ids.iter().any(|role_id| self.has_role(role_id))
}
pub fn has_all_roles(&self, role_ids: &[Snowflake]) -> bool {
role_ids.iter().all(|role_id| self.has_role(role_id))
}
pub fn avatar_url(&self) -> Option<String> {
self.user.avatar_url()
}
pub fn is_bot(&self) -> bool {
self.user.is_bot()
}
}
impl HasId for Member {
fn id(&self) -> Option<&Snowflake> {
Some(&self.user.id)
}
}
#[derive(Debug, Default, Deserialize)]
struct MemberWire {
#[serde(default)]
user: User,
#[serde(default)]
nick: Option<String>,
#[serde(default)]
roles: Vec<Snowflake>,
#[serde(default)]
joined_at: Timestamp,
#[serde(default)]
deaf: bool,
#[serde(default)]
mute: bool,
}
impl std::ops::Deref for Member {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.user
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Role {
pub id: Snowflake,
pub name: String,
pub color: u32,
#[serde(default)]
pub hoist: bool,
pub position: i32,
pub permissions: String,
#[serde(default)]
pub managed: bool,
#[serde(default)]
pub mentionable: bool,
}
impl Role {
pub fn new(id: impl Into<Snowflake>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
color: 0,
hoist: false,
position: 0,
permissions: "0".to_string(),
managed: false,
mentionable: false,
}
}
pub fn mention(&self) -> String {
format!("<@&{}>", self.id)
}
pub fn rgb(&self) -> (u8, u8, u8) {
let r = ((self.color >> 16) & 0xFF) as u8;
let g = ((self.color >> 8) & 0xFF) as u8;
let b = (self.color & 0xFF) as u8;
(r, g, b)
}
pub fn hex_color(&self) -> String {
format!("#{:06X}", self.color)
}
}
impl HasId for Role {
fn id(&self) -> Option<&Snowflake> {
Some(&self.id)
}
}
impl crate::models::HasName for Role {
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation() {
let user = User::new("123456789", "TestUser");
assert_eq!(user.id, "123456789");
assert_eq!(user.username, "TestUser");
assert_eq!(user.avatar, "");
assert_eq!(user.union_openid, "");
assert_eq!(user.union_user_account, "");
assert!(!user.is_bot());
assert!(user.is_human());
}
#[test]
fn user_uses_required_zero_value_fields() {
let user: User = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(user.id, "");
assert_eq!(user.username, "");
assert_eq!(user.avatar, "");
assert!(!user.bot);
assert_eq!(user.union_openid, "");
assert_eq!(user.union_user_account, "");
assert!(user.avatar_url().is_none());
}
#[test]
fn user_keeps_official_json_shape() {
let user = User {
id: "user-1".to_string(),
username: "alice".to_string(),
avatar: "avatar-key".to_string(),
bot: true,
union_openid: "union-openid".to_string(),
union_user_account: "union-account".to_string(),
};
let value = serde_json::to_value(&user).unwrap();
assert_eq!(value["id"], "user-1");
assert_eq!(value["username"], "alice");
assert_eq!(value["avatar"], "avatar-key");
assert_eq!(value["bot"], true);
assert_eq!(value["union_openid"], "union-openid");
assert_eq!(value["union_user_account"], "union-account");
assert!(user.avatar_url().is_some());
}
#[test]
fn test_user_mention() {
let user = User::new("123456789", "TestUser");
assert_eq!(user.mention(), "<@!123456789>");
}
#[test]
fn test_member_display_name() {
let user = User::new("123456789", "TestUser");
let mut member = Member::new(user, "2024-01-01T00:00:00Z".to_string());
assert_eq!(member.display_name(), "TestUser");
member.nick = Some("Nickname".to_string());
assert_eq!(member.display_name(), "Nickname");
}
#[test]
fn test_member_roles() {
let user = User::new("123456789", "TestUser");
let mut member = Member::new(user, "2024-01-01T00:00:00Z".to_string());
member.roles = vec!["role1".to_string(), "role2".to_string()];
assert!(member.has_role(&"role1".to_string()));
assert!(!member.has_role(&"role3".to_string()));
assert!(member.has_any_role(&["role1".to_string(), "role3".to_string()]));
assert!(member.has_all_roles(&["role1".to_string(), "role2".to_string()]));
assert!(!member.has_all_roles(&["role1".to_string(), "role3".to_string()]));
}
#[test]
fn test_role_creation() {
let role = Role::new("123456789", "TestRole");
assert_eq!(role.id, "123456789");
assert_eq!(role.name, "TestRole");
assert_eq!(role.mention(), "<@&123456789>");
}
#[test]
fn test_role_color() {
let mut role = Role::new("123456789", "TestRole");
role.color = 0xFF5733;
let (r, g, b) = role.rgb();
assert_eq!(r, 255);
assert_eq!(g, 87);
assert_eq!(b, 51);
assert_eq!(role.hex_color(), "#FF5733");
}
}