use chrono::{DateTime, Utc};
use openlark_core::api::responses::ApiResponseTrait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserInfoResponse {
pub data: UserInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub open_id: String,
pub union_id: Option<String>,
pub user_id: Option<String>,
pub name: Option<String>,
pub en_name: Option<String>,
pub email: Option<String>,
pub enterprise_email: Option<String>,
pub mobile: Option<String>,
pub avatar_url: Option<String>,
pub avatar: Option<UserAvatar>,
pub status: Option<UserStatus>,
pub department_ids: Option<Vec<String>>,
pub group_ids: Option<Vec<String>>,
pub positions: Option<Vec<String>>,
pub employee_no: Option<String>,
pub dingtalk_user_id: Option<String>,
pub enterprise_extension: Option<UserEnterpriseExtension>,
pub custom_attrs: Option<Vec<UserCustomAttr>>,
pub tenant_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAvatar {
pub avatar_72: Option<String>,
pub avatar_240: Option<String>,
pub avatar_640: Option<String>,
pub avatar_origin: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserStatus {
pub is_activated: Option<bool>,
pub is_joined: Option<bool>,
pub is_reserved: Option<bool>,
pub is_exited: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserEnterpriseExtension {
pub hire_date: Option<DateTime<Utc>>,
pub location: Option<String>,
pub work_station: Option<String>,
pub work_station_id: Option<String>,
pub address: Option<String>,
pub postcode: Option<String>,
pub mobile_phone: Option<String>,
pub extension_number: Option<String>,
pub contact_phone: Option<String>,
pub primary_phone: Option<String>,
pub emergency_contact: Option<String>,
pub emergency_phone: Option<String>,
pub home_phone: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserCustomAttr {
pub key: Option<String>,
pub value: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserAccessTokenResponse {
pub user_access_token: String,
pub refresh_token: Option<String>,
pub expires_in: u64,
pub token_type: Option<String>,
pub scope: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAccessTokenV1Request {
pub grant_code: String,
pub app_id: String,
pub app_secret: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshUserAccessTokenV1Request {
pub refresh_token: String,
pub app_id: String,
pub app_secret: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcUserAccessTokenRequest {
pub code: String,
pub code_verifier: Option<String>,
pub redirect_uri: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub grant_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcRefreshUserAccessTokenRequest {
pub refresh_token: String,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub grant_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfoRequest {
pub user_access_token: String,
pub user_id_type: Option<String>,
}
impl PartialEq for UserInfo {
fn eq(&self, other: &Self) -> bool {
self.open_id == other.open_id
&& self.union_id == other.union_id
&& self.user_id == other.user_id
&& self.name == other.name
&& self.en_name == other.en_name
&& self.email == other.email
&& self.enterprise_email == other.enterprise_email
&& self.mobile == other.mobile
&& self.avatar_url == other.avatar_url
&& self.avatar == other.avatar
&& self.status == other.status
&& self.department_ids == other.department_ids
&& self.group_ids == other.group_ids
&& self.positions == other.positions
&& self.employee_no == other.employee_no
&& self.dingtalk_user_id == other.dingtalk_user_id
&& self.enterprise_extension == other.enterprise_extension
&& self.custom_attrs == other.custom_attrs
&& self.tenant_key == other.tenant_key
}
}
impl PartialEq for UserAvatar {
fn eq(&self, other: &Self) -> bool {
self.avatar_72 == other.avatar_72
&& self.avatar_240 == other.avatar_240
&& self.avatar_640 == other.avatar_640
&& self.avatar_origin == other.avatar_origin
}
}
impl PartialEq for UserStatus {
fn eq(&self, other: &Self) -> bool {
self.is_activated == other.is_activated
&& self.is_joined == other.is_joined
&& self.is_reserved == other.is_reserved
&& self.is_exited == other.is_exited
}
}
impl PartialEq for UserEnterpriseExtension {
fn eq(&self, other: &Self) -> bool {
self.hire_date == other.hire_date
&& self.location == other.location
&& self.work_station == other.work_station
&& self.work_station_id == other.work_station_id
&& self.address == other.address
&& self.postcode == other.postcode
&& self.mobile_phone == other.mobile_phone
&& self.extension_number == other.extension_number
&& self.contact_phone == other.contact_phone
&& self.primary_phone == other.primary_phone
&& self.emergency_contact == other.emergency_contact
&& self.emergency_phone == other.emergency_phone
&& self.home_phone == other.home_phone
}
}
impl PartialEq for UserCustomAttr {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.value == other.value
}
}
impl ApiResponseTrait for UserInfoResponse {}
impl ApiResponseTrait for UserAccessTokenResponse {}
#[derive(Debug, Clone)]
pub struct UserTokenInfo {
pub user_access_token: String,
pub refresh_token: Option<String>,
pub expires_at: DateTime<Utc>,
pub token_type: String,
pub scope: Option<String>,
pub created_at: DateTime<Utc>,
}
impl UserTokenInfo {
pub fn new(
user_access_token: String,
expires_in: u64,
token_type: String,
refresh_token: Option<String>,
scope: Option<String>,
) -> Self {
let now = Utc::now();
let expires_at = now + chrono::Duration::seconds(expires_in as i64);
Self {
user_access_token,
refresh_token,
expires_at,
token_type,
scope,
created_at: now,
}
}
pub fn is_expired(&self) -> bool {
Utc::now() >= self.expires_at
}
pub fn is_expiring_soon(&self) -> bool {
let soon = Utc::now() + chrono::Duration::minutes(5);
soon >= self.expires_at
}
pub fn remaining_seconds(&self) -> i64 {
(self.expires_at - Utc::now()).num_seconds().max(0)
}
pub fn has_refresh_token(&self) -> bool {
self.refresh_token.is_some()
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
#[test]
fn test_user_info_serialization() {
let user_info = UserInfo {
open_id: "test_open_id".to_string(),
union_id: Some("test_union_id".to_string()),
user_id: Some("test_user_id".to_string()),
name: Some("测试用户".to_string()),
en_name: Some("Test User".to_string()),
email: Some("test@example.com".to_string()),
enterprise_email: Some("test@company.com".to_string()),
mobile: Some("13800138000".to_string()),
avatar_url: Some("https://example.com/avatar.jpg".to_string()),
avatar: None,
status: None,
department_ids: None,
group_ids: None,
positions: None,
employee_no: None,
dingtalk_user_id: None,
enterprise_extension: None,
custom_attrs: None,
tenant_key: None,
};
let json = serde_json::to_string(&user_info).unwrap();
assert!(json.contains("test_open_id"));
assert!(json.contains("测试用户"));
}
#[test]
fn test_user_token_info() {
let token = UserTokenInfo::new(
"test_token".to_string(),
3600,
"Bearer".to_string(),
Some("refresh_token".to_string()),
Some("user_info".to_string()),
);
assert_eq!(token.user_access_token, "test_token");
assert_eq!(token.token_type, "Bearer");
assert!(token.has_refresh_token());
assert!(!token.is_expired());
assert!(token.remaining_seconds() > 0);
}
#[test]
fn test_user_avatar() {
let avatar = UserAvatar {
avatar_72: Some("https://example.com/72.jpg".to_string()),
avatar_240: Some("https://example.com/240.jpg".to_string()),
avatar_640: Some("https://example.com/640.jpg".to_string()),
avatar_origin: Some("https://example.com/original.jpg".to_string()),
};
let json = serde_json::to_string(&avatar).unwrap();
assert!(json.contains("72.jpg"));
assert!(json.contains("240.jpg"));
}
#[test]
fn test_user_info_with_enterprise_email() {
let user_info = UserInfo {
open_id: "test_open_id".to_string(),
union_id: Some("test_union_id".to_string()),
user_id: Some("test_user_id".to_string()),
name: Some("测试用户".to_string()),
en_name: Some("Test User".to_string()),
email: Some("test@example.com".to_string()),
enterprise_email: Some("test@company.com".to_string()),
mobile: Some("13800138000".to_string()),
avatar_url: Some("https://example.com/avatar.jpg".to_string()),
avatar: None,
status: None,
department_ids: None,
group_ids: None,
positions: None,
employee_no: None,
dingtalk_user_id: None,
enterprise_extension: None,
custom_attrs: None,
tenant_key: None,
};
let json = serde_json::to_string(&user_info).unwrap();
assert!(json.contains("enterprise_email"));
assert!(json.contains("test@company.com"));
let deserialized: UserInfo = serde_json::from_str(&json).expect("JSON 反序列化失败");
assert_eq!(
deserialized.enterprise_email,
Some("test@company.com".to_string())
);
let user_info_no_enterprise_email = UserInfo {
enterprise_email: None,
..user_info
};
let json_no_enterprise = serde_json::to_string(&user_info_no_enterprise_email).unwrap();
let deserialized_no_enterprise: UserInfo =
serde_json::from_str(&json_no_enterprise).expect("JSON 反序列化失败");
assert!(deserialized_no_enterprise.enterprise_email.is_none());
}
}