use crate::models::{HasId, HasName, Snowflake, Timestamp, channel::Channel};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
fn insert_query_param(query: &mut HashMap<String, String>, key: &str, value: &Option<String>) {
if let Some(value) = value.as_ref().filter(|value| !value.is_empty()) {
query.insert(key.to_string(), value.clone());
}
}
mod role_hoist_serde {
use super::*;
pub fn serialize<S>(value: &Option<bool>, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(value) => serializer.serialize_some(&u32::from(*value)),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Option<bool>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Ok(value.and_then(|value| match value {
serde_json::Value::Bool(value) => Some(value),
serde_json::Value::Number(value) => value.as_u64().map(|value| value != 0),
_ => None,
}))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Guild {
pub id: Option<Snowflake>,
pub name: Option<String>,
pub icon: Option<String>,
pub owner_id: Option<Snowflake>,
pub is_owner: Option<bool>,
pub member_count: Option<u32>,
pub max_members: Option<u32>,
pub description: Option<String>,
pub joined_at: Option<Timestamp>,
}
impl Guild {
pub fn new() -> Self {
Self {
id: None,
name: None,
icon: None,
owner_id: None,
is_owner: None,
member_count: None,
max_members: None,
description: None,
joined_at: None,
}
}
pub fn from_data(_api: crate::api::BotApi, id: String, data: serde_json::Value) -> Self {
Self {
id: Some(id),
name: data.get("name").and_then(|v| v.as_str()).map(String::from),
icon: data.get("icon").and_then(|v| v.as_str()).map(String::from),
owner_id: data
.get("owner_id")
.and_then(|v| v.as_str())
.map(String::from),
is_owner: data.get("is_owner").and_then(|v| v.as_bool()),
member_count: data
.get("member_count")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
max_members: data
.get("max_members")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
description: data
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
joined_at: data
.get("joined_at")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
}
}
pub fn icon_url(&self) -> Option<String> {
self.icon.as_ref().map(|hash| {
format!(
"https://groupprofile.qq.com/groupicon/{}/{}",
self.id.as_ref().unwrap_or(&String::new()),
hash
)
})
}
pub fn is_owned_by_current_user(&self) -> bool {
self.is_owner.unwrap_or(false)
}
pub fn get_member_count(&self) -> u32 {
self.member_count.unwrap_or(0)
}
pub fn get_max_members(&self) -> u32 {
self.max_members.unwrap_or(0)
}
pub fn is_at_member_limit(&self) -> bool {
match (self.member_count, self.max_members) {
(Some(current), Some(max)) => current >= max,
_ => false,
}
}
pub fn display_name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn has_description(&self) -> bool {
self.description
.as_ref()
.is_some_and(|desc| !desc.is_empty())
}
}
impl Default for Guild {
fn default() -> Self {
Self::new()
}
}
impl HasId for Guild {
fn id(&self) -> Option<&Snowflake> {
self.id.as_ref()
}
}
impl HasName for Guild {
fn name(&self) -> &str {
self.name.as_deref().unwrap_or("")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GuildRoles {
pub guild_id: Option<Snowflake>,
pub roles: Vec<GuildRole>,
pub role_num_limit: Option<String>,
}
impl GuildRoles {
pub fn new(roles: Vec<GuildRole>) -> Self {
Self {
guild_id: None,
roles,
role_num_limit: None,
}
}
}
pub type RoleId = Snowflake;
pub type RoleID = RoleId;
pub const DEFAULT_ROLE_COLOR: u32 = 4_278_245_297;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateRoleInfo {
pub name: String,
pub color: u32,
pub hoist: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GuildRole {
pub id: Option<Snowflake>,
pub name: Option<String>,
pub color: Option<u32>,
#[serde(with = "role_hoist_serde", default)]
pub hoist: Option<bool>,
pub number: Option<u32>,
pub member_limit: Option<u32>,
}
impl GuildRole {
fn default_color() -> u32 {
DEFAULT_ROLE_COLOR
}
fn hoist_value(&self) -> u32 {
self.hoist.map(u32::from).unwrap_or(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateRoleFilter {
pub name: u32,
pub color: u32,
pub hoist: u32,
}
impl Default for UpdateRoleFilter {
fn default() -> Self {
Self {
name: 1,
color: 1,
hoist: 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateRole {
pub guild_id: String,
pub filter: UpdateRoleFilter,
#[serde(rename = "info")]
pub update: GuildRole,
}
impl UpdateRole {
pub fn new(guild_id: impl Into<String>, mut role: GuildRole) -> Self {
if role.color.unwrap_or(0) == 0 {
role.color = Some(GuildRole::default_color());
}
role.hoist = Some(role.is_hoisted());
Self {
guild_id: guild_id.into(),
filter: UpdateRoleFilter::default(),
update: role,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateResult {
pub role_id: Option<Snowflake>,
pub guild_id: Option<Snowflake>,
pub role: Option<GuildRole>,
}
impl GuildRole {
pub fn new() -> Self {
Self {
id: None,
name: None,
color: None,
hoist: None,
number: None,
member_limit: None,
}
}
pub fn is_hoisted(&self) -> bool {
self.hoist.unwrap_or(false)
}
pub fn hoist_as_u32(&self) -> u32 {
self.hoist_value()
}
pub fn color_hex(&self) -> Option<String> {
self.color.map(|c| format!("#{c:06X}"))
}
pub fn member_count(&self) -> u32 {
self.number.unwrap_or(0)
}
pub fn get_member_limit(&self) -> u32 {
self.member_limit.unwrap_or(0)
}
pub fn is_at_member_limit(&self) -> bool {
match (self.number, self.member_limit) {
(Some(current), Some(limit)) => current >= limit,
_ => false,
}
}
}
impl Default for GuildRole {
fn default() -> Self {
Self::new()
}
}
impl HasId for GuildRole {
fn id(&self) -> Option<&Snowflake> {
self.id.as_ref()
}
}
impl HasName for GuildRole {
fn name(&self) -> &str {
self.name.as_deref().unwrap_or("")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Role {
pub id: Option<Snowflake>,
pub name: Option<String>,
pub color: Option<u32>,
pub hoist: Option<bool>,
pub number: Option<u32>,
pub member_limit: Option<u32>,
}
impl Role {
pub fn new() -> Self {
Self {
id: None,
name: None,
color: None,
hoist: None,
number: None,
member_limit: None,
}
}
pub fn is_hoisted(&self) -> bool {
self.hoist.unwrap_or(false)
}
pub fn color_hex(&self) -> Option<String> {
self.color.map(|c| format!("#{c:06X}"))
}
pub fn member_count(&self) -> u32 {
self.number.unwrap_or(0)
}
pub fn get_member_limit(&self) -> u32 {
self.member_limit.unwrap_or(0)
}
pub fn is_at_member_limit(&self) -> bool {
match (self.number, self.member_limit) {
(Some(current), Some(limit)) => current >= limit,
_ => false,
}
}
}
impl Default for Role {
fn default() -> Self {
Self::new()
}
}
impl HasId for Role {
fn id(&self) -> Option<&Snowflake> {
self.id.as_ref()
}
}
impl HasName for Role {
fn name(&self) -> &str {
self.name.as_deref().unwrap_or("")
}
}
pub type Roles = Vec<Role>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct GuildMembersPager {
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
}
impl GuildMembersPager {
pub fn new(after: impl Into<String>, limit: impl ToString) -> Self {
Self {
after: Some(after.into()),
limit: Some(limit.to_string()),
}
}
pub fn query_params(&self) -> HashMap<String, String> {
let mut query = HashMap::new();
insert_query_param(&mut query, "limit", &self.limit);
insert_query_param(&mut query, "after", &self.after);
query
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct GuildRoleMembersPager {
#[serde(skip_serializing_if = "Option::is_none")]
pub start_index: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
}
impl GuildRoleMembersPager {
pub fn new(start_index: impl Into<String>, limit: impl ToString) -> Self {
Self {
start_index: Some(start_index.into()),
limit: Some(limit.to_string()),
}
}
pub fn query_params(&self) -> HashMap<String, String> {
let mut query = HashMap::new();
insert_query_param(&mut query, "limit", &self.limit);
insert_query_param(&mut query, "start_index", &self.start_index);
query
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct GuildPager {
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
}
impl GuildPager {
pub fn new() -> Self {
Self::default()
}
pub fn with_after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn with_before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn with_limit(mut self, limit: impl ToString) -> Self {
self.limit = Some(limit.to_string());
self
}
pub fn query_params(&self) -> HashMap<String, String> {
let mut query = HashMap::new();
insert_query_param(&mut query, "limit", &self.limit);
insert_query_param(&mut query, "after", &self.after);
if !query.contains_key("after") {
insert_query_param(&mut query, "before", &self.before);
}
query
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct GuildRoleMembers {
#[serde(default)]
pub data: Vec<Member>,
#[serde(default)]
pub next: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MemberAddRoleBody {
pub channel: Option<Channel>,
}
impl MemberAddRoleBody {
pub fn new() -> Self {
Self { channel: None }
}
pub fn with_channel_id(channel_id: impl Into<String>) -> Self {
let mut channel = Channel::new();
channel.id = Some(channel_id.into());
Self {
channel: Some(channel),
}
}
}
pub type DeleteHistoryMsgDay = i32;
pub const NO_DELETE: DeleteHistoryMsgDay = 0;
pub const DELETE_THREE_DAYS: DeleteHistoryMsgDay = 3;
pub const DELETE_SEVEN_DAYS: DeleteHistoryMsgDay = 7;
pub const DELETE_FIFTEEN_DAYS: DeleteHistoryMsgDay = 15;
pub const DELETE_THIRTY_DAYS: DeleteHistoryMsgDay = 30;
pub const DELETE_ALL: DeleteHistoryMsgDay = -1;
#[allow(non_upper_case_globals)]
pub const NoDelete: DeleteHistoryMsgDay = NO_DELETE;
#[allow(non_upper_case_globals)]
pub const DeleteThreeDays: DeleteHistoryMsgDay = DELETE_THREE_DAYS;
#[allow(non_upper_case_globals)]
pub const DeleteSevenDays: DeleteHistoryMsgDay = DELETE_SEVEN_DAYS;
#[allow(non_upper_case_globals)]
pub const DeleteFifteenDays: DeleteHistoryMsgDay = DELETE_FIFTEEN_DAYS;
#[allow(non_upper_case_globals)]
pub const DeleteThirtyDays: DeleteHistoryMsgDay = DELETE_THIRTY_DAYS;
#[allow(non_upper_case_globals)]
pub const DeleteAll: DeleteHistoryMsgDay = DELETE_ALL;
pub type MemberDeleteOpts = MemberDeleteOptions;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MemberDeleteOptions {
pub add_blacklist: bool,
pub delete_history_msg_days: DeleteHistoryMsgDay,
}
impl MemberDeleteOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_add_blacklist(mut self, add_blacklist: bool) -> Self {
self.add_blacklist = add_blacklist;
self
}
pub fn with_delete_history_msg_days(mut self, days: DeleteHistoryMsgDay) -> Self {
self.delete_history_msg_days = normalize_delete_history_msg_days(days);
self
}
}
impl Default for MemberDeleteOptions {
fn default() -> Self {
Self {
add_blacklist: false,
delete_history_msg_days: NO_DELETE,
}
}
}
pub type MemberDeleteOption = Box<dyn FnOnce(&mut MemberDeleteOptions) + Send>;
#[allow(non_snake_case)]
pub fn WithAddBlackList(add_blacklist: bool) -> MemberDeleteOption {
Box::new(move |options| {
options.add_blacklist = add_blacklist;
})
}
#[allow(non_snake_case)]
pub fn WithDeleteHistoryMsg(days: DeleteHistoryMsgDay) -> MemberDeleteOption {
Box::new(move |options| {
options.delete_history_msg_days = normalize_delete_history_msg_days(days);
})
}
pub fn normalize_delete_history_msg_days(days: DeleteHistoryMsgDay) -> DeleteHistoryMsgDay {
match days {
DELETE_THREE_DAYS | DELETE_SEVEN_DAYS | DELETE_FIFTEEN_DAYS | DELETE_THIRTY_DAYS
| DELETE_ALL => days,
_ => NO_DELETE,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct UpdateGuildMute {
#[serde(skip_serializing_if = "Option::is_none")]
pub mute_end_timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mute_seconds: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_ids: Option<Vec<String>>,
}
impl UpdateGuildMute {
pub fn new(mute_end_timestamp: Option<&str>, mute_seconds: Option<&str>) -> Self {
Self {
mute_end_timestamp: mute_end_timestamp.map(String::from),
mute_seconds: mute_seconds.map(String::from),
user_ids: None,
}
}
pub fn new_multi(
user_ids: Vec<String>,
mute_end_timestamp: Option<&str>,
mute_seconds: Option<&str>,
) -> Self {
Self {
mute_end_timestamp: mute_end_timestamp.map(String::from),
mute_seconds: mute_seconds.map(String::from),
user_ids: Some(user_ids),
}
}
pub fn cancel() -> Self {
Self {
mute_end_timestamp: Some("0".to_string()),
mute_seconds: Some("0".to_string()),
user_ids: None,
}
}
pub fn cancel_multi(user_ids: Vec<String>) -> Self {
Self {
mute_end_timestamp: Some("0".to_string()),
mute_seconds: Some("0".to_string()),
user_ids: Some(user_ids),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct UpdateGuildMuteResponse {
#[serde(default)]
pub user_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Member {
pub guild_id: Option<Snowflake>,
pub user: Option<crate::models::User>,
pub nick: Option<String>,
pub roles: Option<Vec<Snowflake>>,
pub joined_at: Option<Timestamp>,
pub op_user_id: Option<Snowflake>,
}
impl Member {
pub fn new() -> Self {
Self {
guild_id: None,
user: None,
nick: None,
roles: None,
joined_at: None,
op_user_id: None,
}
}
pub fn display_name(&self) -> Option<&str> {
self.nick
.as_deref()
.or_else(|| self.user.as_ref().map(|u| u.username.as_str()))
}
pub fn username(&self) -> Option<&str> {
self.user.as_ref().map(|u| u.username.as_str())
}
pub fn user_id(&self) -> Option<&Snowflake> {
self.user.as_ref().map(|u| &u.id)
}
pub fn is_bot(&self) -> bool {
self.user.as_ref().is_some_and(|u| u.is_bot())
}
pub fn role_ids(&self) -> &[Snowflake] {
self.roles.as_deref().unwrap_or(&[])
}
pub fn has_role(&self, role_id: &str) -> bool {
self.role_ids().iter().any(|id| id == role_id)
}
}
impl Default for Member {
fn default() -> Self {
Self::new()
}
}
impl HasId for Member {
fn id(&self) -> Option<&Snowflake> {
self.user_id()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_guild_creation() {
let guild = Guild::new();
assert!(guild.id.is_none());
assert!(guild.name.is_none());
assert!(!guild.is_owned_by_current_user());
assert_eq!(guild.get_member_count(), 0);
assert_eq!(guild.get_max_members(), 0);
}
#[test]
fn test_guild_with_data() {
let mut guild = Guild::new();
guild.id = Some("123456789".to_string());
guild.name = Some("Test Guild".to_string());
guild.is_owner = Some(true);
guild.member_count = Some(100);
guild.max_members = Some(500);
guild.description = Some("A test guild".to_string());
assert_eq!(guild.id(), Some(&"123456789".to_string()));
assert_eq!(guild.name(), "Test Guild");
assert!(guild.is_owned_by_current_user());
assert_eq!(guild.get_member_count(), 100);
assert_eq!(guild.get_max_members(), 500);
assert!(!guild.is_at_member_limit());
assert!(guild.has_description());
assert_eq!(guild.display_name(), Some("Test Guild"));
}
#[test]
fn test_member_limit() {
let mut guild = Guild::new();
guild.member_count = Some(500);
guild.max_members = Some(500);
assert!(guild.is_at_member_limit());
guild.member_count = Some(499);
assert!(!guild.is_at_member_limit());
guild.member_count = Some(501);
assert!(guild.is_at_member_limit());
}
#[test]
fn test_icon_url() {
let mut guild = Guild::new();
assert!(guild.icon_url().is_none());
guild.id = Some("123456789".to_string());
guild.icon = Some("abc123".to_string());
let url = guild.icon_url().unwrap();
assert!(url.contains("123456789"));
assert!(url.contains("abc123"));
}
#[test]
fn test_role_creation() {
let role = Role::new();
assert!(role.id.is_none());
assert!(role.name.is_none());
assert!(!role.is_hoisted());
assert_eq!(role.member_count(), 0);
}
#[test]
fn test_role_with_data() {
let mut role = Role::new();
role.id = Some("role123".to_string());
role.name = Some("Admin".to_string());
role.color = Some(0xFF0000);
role.hoist = Some(true);
role.number = Some(5);
role.member_limit = Some(10);
assert_eq!(role.id(), Some(&"role123".to_string()));
assert_eq!(role.name(), "Admin");
assert_eq!(role.color_hex(), Some("#FF0000".to_string()));
assert!(role.is_hoisted());
assert_eq!(role.member_count(), 5);
assert_eq!(role.get_member_limit(), 10);
assert!(!role.is_at_member_limit());
}
#[test]
fn test_member_creation() {
let member = Member::new();
assert!(member.user.is_none());
assert!(member.nick.is_none());
assert_eq!(member.role_ids().len(), 0);
}
#[test]
fn test_member_with_roles() {
let mut member = Member::new();
member.roles = Some(vec!["role1".to_string(), "role2".to_string()]);
assert!(member.has_role("role1"));
assert!(member.has_role("role2"));
assert!(!member.has_role("role3"));
assert_eq!(member.role_ids().len(), 2);
}
}