use crate::models::{HasId, HasName, Pager, Snowflake, Timestamp, channel::Channel};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[allow(non_upper_case_globals)]
pub const DefaultColor: u32 = 4_278_245_297;
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());
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Guild {
#[serde(default)]
pub id: Snowflake,
#[serde(default)]
pub name: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub owner_id: Snowflake,
#[serde(rename = "owner")]
#[serde(default)]
pub is_owner: bool,
#[serde(default)]
pub member_count: i32,
#[serde(default)]
pub max_members: i64,
#[serde(default)]
pub description: String,
#[serde(default)]
pub joined_at: Timestamp,
#[serde(default)]
pub channels: Vec<Channel>,
#[serde(default)]
pub union_world_id: String,
#[serde(default)]
pub union_org_id: String,
#[serde(default)]
pub op_user_id: Snowflake,
}
impl Guild {
pub fn new() -> Self {
Self::default()
}
pub fn from_data(_api: crate::api::BotApi, id: String, data: serde_json::Value) -> Self {
let mut guild = serde_json::from_value::<Self>(data).unwrap_or_default();
if guild.id.is_empty() {
guild.id = id;
}
guild
}
pub fn icon_url(&self) -> Option<String> {
(!self.icon.is_empty()).then(|| {
format!(
"https://groupprofile.qq.com/groupicon/{}/{}",
self.id, self.icon
)
})
}
pub fn is_owned_by_current_user(&self) -> bool {
self.is_owner
}
pub fn get_member_count(&self) -> i32 {
self.member_count
}
pub fn get_max_members(&self) -> i64 {
self.max_members
}
pub fn is_at_member_limit(&self) -> bool {
self.max_members > 0 && i64::from(self.member_count) >= self.max_members
}
pub fn display_name(&self) -> Option<&str> {
(!self.name.is_empty()).then_some(self.name.as_str())
}
pub fn has_description(&self) -> bool {
!self.description.is_empty()
}
}
impl HasId for Guild {
fn id(&self) -> Option<&Snowflake> {
(!self.id.is_empty()).then_some(&self.id)
}
}
impl HasName for Guild {
fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct GuildRoles {
#[serde(default)]
pub guild_id: Snowflake,
#[serde(default)]
pub roles: Vec<GuildRole>,
#[serde(default, rename = "role_num_limit")]
pub num_limit: String,
}
impl GuildRoles {
pub fn new(roles: Vec<GuildRole>) -> Self {
Self {
guild_id: String::new(),
roles,
num_limit: String::new(),
}
}
}
pub type RoleId = Snowflake;
pub type RoleID = RoleId;
pub const DEFAULT_ROLE_COLOR: u32 = 4_278_245_297;
fn is_zero_u32(value: &u32) -> bool {
*value == 0
}
fn is_empty_vec<T>(value: &[T]) -> bool {
value.is_empty()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateRoleInfo {
pub name: String,
pub color: u32,
pub hoist: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct GuildRole {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: Snowflake,
#[serde(default)]
pub name: String,
#[serde(default)]
pub color: u32,
#[serde(default)]
pub hoist: u32,
#[serde(default, rename = "number", skip_serializing_if = "is_zero_u32")]
pub member_count: u32,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub member_limit: u32,
}
impl GuildRole {
fn default_color() -> u32 {
DEFAULT_ROLE_COLOR
}
fn hoist_value(&self) -> u32 {
self.hoist
}
}
#[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 == 0 {
role.color = GuildRole::default_color();
}
Self {
guild_id: guild_id.into(),
filter: UpdateRoleFilter::default(),
update: role,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateResult {
#[serde(default)]
pub role_id: Snowflake,
#[serde(default)]
pub guild_id: Snowflake,
#[serde(default)]
pub role: Option<GuildRole>,
}
impl GuildRole {
pub fn new() -> Self {
Self::default()
}
pub fn is_hoisted(&self) -> bool {
self.hoist != 0
}
pub fn hoist_as_u32(&self) -> u32 {
self.hoist_value()
}
pub fn color_hex(&self) -> Option<String> {
(self.color != 0).then(|| format!("#{:06X}", self.color))
}
pub fn member_count(&self) -> u32 {
self.member_count
}
pub fn get_member_limit(&self) -> u32 {
self.member_limit
}
pub fn is_at_member_limit(&self) -> bool {
self.member_limit > 0 && self.member_count >= self.member_limit
}
}
impl HasId for GuildRole {
fn id(&self) -> Option<&Snowflake> {
(!self.id.is_empty()).then_some(&self.id)
}
}
impl HasName for GuildRole {
fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Role {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: Snowflake,
#[serde(default)]
pub name: String,
#[serde(default)]
pub color: u32,
#[serde(default)]
pub hoist: u32,
#[serde(default, rename = "number", skip_serializing_if = "is_zero_u32")]
pub member_count: u32,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub member_limit: u32,
}
impl Role {
pub fn new() -> Self {
Self::default()
}
pub fn is_hoisted(&self) -> bool {
self.hoist != 0
}
pub fn color_hex(&self) -> Option<String> {
(self.color != 0).then(|| format!("#{:06X}", self.color))
}
pub fn member_count(&self) -> u32 {
self.member_count
}
pub fn get_member_limit(&self) -> u32 {
self.member_limit
}
pub fn is_at_member_limit(&self) -> bool {
self.member_limit > 0 && self.member_count >= self.member_limit
}
}
impl HasId for Role {
fn id(&self) -> Option<&Snowflake> {
(!self.id.is_empty()).then_some(&self.id)
}
}
impl HasName for Role {
fn name(&self) -> &str {
&self.name
}
}
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
}
#[allow(non_snake_case)]
pub fn QueryParams(&self) -> HashMap<String, String> {
self.query_params()
}
}
impl Pager for GuildMembersPager {
fn query_params(&self) -> HashMap<String, String> {
GuildMembersPager::query_params(self)
}
}
#[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
}
#[allow(non_snake_case)]
pub fn QueryParams(&self) -> HashMap<String, String> {
self.query_params()
}
}
impl Pager for GuildRoleMembersPager {
fn query_params(&self) -> HashMap<String, String> {
GuildRoleMembersPager::query_params(self)
}
}
#[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
}
#[allow(non_snake_case)]
pub fn QueryParams(&self) -> HashMap<String, String> {
self.query_params()
}
}
impl Pager for GuildPager {
fn query_params(&self) -> HashMap<String, String> {
GuildPager::query_params(self)
}
}
#[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 = 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(default, skip_serializing_if = "String::is_empty")]
pub mute_end_timestamp: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mute_seconds: String,
#[serde(default, skip_serializing_if = "is_empty_vec")]
pub user_ids: Vec<String>,
}
impl UpdateGuildMute {
pub fn new(mute_end_timestamp: Option<&str>, mute_seconds: Option<&str>) -> Self {
Self {
mute_end_timestamp: mute_end_timestamp.unwrap_or_default().to_string(),
mute_seconds: mute_seconds.unwrap_or_default().to_string(),
user_ids: Vec::new(),
}
}
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.unwrap_or_default().to_string(),
mute_seconds: mute_seconds.unwrap_or_default().to_string(),
user_ids,
}
}
pub fn cancel() -> Self {
Self {
mute_end_timestamp: "0".to_string(),
mute_seconds: "0".to_string(),
user_ids: Vec::new(),
}
}
pub fn cancel_multi(user_ids: Vec<String>) -> Self {
Self {
mute_end_timestamp: "0".to_string(),
mute_seconds: "0".to_string(),
user_ids,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct UpdateGuildMuteResponse {
#[serde(default)]
pub user_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Member {
#[serde(default)]
pub guild_id: Snowflake,
#[serde(default)]
pub user: Option<crate::models::User>,
#[serde(default)]
pub nick: String,
#[serde(default)]
pub roles: Vec<Snowflake>,
#[serde(default)]
pub joined_at: Timestamp,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub op_user_id: Snowflake,
}
impl Member {
pub fn new() -> Self {
Self::default()
}
pub fn display_name(&self) -> Option<&str> {
(!self.nick.is_empty())
.then_some(self.nick.as_str())
.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
}
pub fn has_role(&self, role_id: &str) -> bool {
self.role_ids().iter().any(|id| id == role_id)
}
}
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_eq!(guild.id, "");
assert_eq!(guild.name, "");
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 = "123456789".to_string();
guild.name = "Test Guild".to_string();
guild.is_owner = true;
guild.member_count = 100;
guild.max_members = 500;
guild.description = "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 botgo_guild_fields_use_official_json_names() {
let guild = Guild::from_data(
crate::api::BotApi::new(crate::http::HttpClient::new(30, false).unwrap()),
"guild-1".to_string(),
serde_json::json!({
"name": "Guild",
"owner": true,
"channels": [
{
"id": "channel-1",
"guild_id": "guild-1",
"name": "general",
"type": 0
}
],
"union_world_id": "world-1",
"union_org_id": "org-1",
"op_user_id": "operator-1"
}),
);
assert_eq!(guild.id, "guild-1");
assert!(guild.is_owner);
assert_eq!(guild.channels.len(), 1);
assert_eq!(guild.channels[0].id, "channel-1");
assert_eq!(guild.union_world_id, "world-1");
assert_eq!(guild.union_org_id, "org-1");
assert_eq!(guild.op_user_id, "operator-1");
let value = serde_json::to_value(&guild).unwrap();
assert_eq!(value["owner"], serde_json::json!(true));
assert!(value.get("is_owner").is_none());
assert_eq!(value["channels"][0]["id"], serde_json::json!("channel-1"));
assert_eq!(value["union_world_id"], serde_json::json!("world-1"));
}
#[test]
fn botgo_guild_uses_required_zero_value_fields() {
let guild: Guild = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(guild.id, "");
assert_eq!(guild.name, "");
assert_eq!(guild.icon, "");
assert_eq!(guild.owner_id, "");
assert!(!guild.is_owner);
assert_eq!(guild.member_count, 0);
assert_eq!(guild.max_members, 0);
assert_eq!(guild.description, "");
assert_eq!(guild.joined_at, "");
assert!(guild.channels.is_empty());
assert_eq!(guild.union_world_id, "");
assert_eq!(guild.union_org_id, "");
assert_eq!(guild.op_user_id, "");
}
#[test]
fn test_member_limit() {
let mut guild = Guild::new();
guild.member_count = 500;
guild.max_members = 500;
assert!(guild.is_at_member_limit());
guild.member_count = 499;
assert!(!guild.is_at_member_limit());
guild.member_count = 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 = "123456789".to_string();
guild.icon = "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_eq!(role.id, "");
assert_eq!(role.name, "");
assert!(!role.is_hoisted());
assert_eq!(role.member_count(), 0);
}
#[test]
fn test_role_with_data() {
let mut role = Role::new();
role.id = "role123".to_string();
role.name = "Admin".to_string();
role.color = 0xFF0000;
role.hoist = 1;
role.member_count = 5;
role.member_limit = 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 botgo_role_keeps_official_json_shape() {
let role = GuildRole {
id: "role-1".to_string(),
name: "Admin".to_string(),
color: 0xFF0000,
hoist: 1,
member_count: 5,
member_limit: 10,
};
let value = serde_json::to_value(&role).unwrap();
assert_eq!(value["id"], "role-1");
assert_eq!(value["name"], "Admin");
assert_eq!(value["color"], 0xFF0000);
assert_eq!(value["hoist"], 1);
assert_eq!(value["number"], 5);
assert_eq!(value["member_limit"], 10);
let roles = GuildRoles {
guild_id: "guild-1".to_string(),
roles: vec![role],
num_limit: "30".to_string(),
};
let value = serde_json::to_value(&roles).unwrap();
assert_eq!(value["guild_id"], "guild-1");
assert_eq!(value["role_num_limit"], "30");
assert!(value.get("num_limit").is_none());
}
#[test]
fn botgo_update_guild_mute_uses_zero_value_omitempty_shape() {
let empty = serde_json::to_value(UpdateGuildMute::default()).unwrap();
assert_eq!(empty, serde_json::json!({}));
let single = UpdateGuildMute::new(Some("1710000000"), None);
let value = serde_json::to_value(&single).unwrap();
assert_eq!(single.mute_end_timestamp, "1710000000");
assert_eq!(single.mute_seconds, "");
assert!(single.user_ids.is_empty());
assert_eq!(
value,
serde_json::json!({"mute_end_timestamp": "1710000000"})
);
let cancel = UpdateGuildMute::cancel_multi(vec!["user-1".to_string()]);
let value = serde_json::to_value(&cancel).unwrap();
assert_eq!(
value,
serde_json::json!({
"mute_end_timestamp": "0",
"mute_seconds": "0",
"user_ids": ["user-1"]
})
);
}
#[test]
fn botgo_pager_query_params_match_official_priority() {
let members = GuildMembersPager::new("user-1", 100);
assert_eq!(
members.QueryParams().get("after").map(String::as_str),
Some("user-1")
);
let role_members = GuildRoleMembersPager::new("next-1", 50);
assert_eq!(
role_members
.QueryParams()
.get("start_index")
.map(String::as_str),
Some("next-1")
);
let guilds = GuildPager::new()
.with_before("before-1")
.with_after("after-1")
.with_limit(20);
let query = guilds.QueryParams();
assert_eq!(query.get("after").map(String::as_str), Some("after-1"));
assert!(!query.contains_key("before"));
}
#[test]
fn test_member_creation() {
let member = Member::new();
assert!(member.user.is_none());
assert_eq!(member.nick, "");
assert_eq!(member.role_ids().len(), 0);
}
#[test]
fn test_member_with_roles() {
let mut member = Member::new();
member.roles = 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);
}
#[test]
fn botgo_member_uses_required_zero_value_fields() {
let member: Member = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(member.guild_id, "");
assert!(member.user.is_none());
assert_eq!(member.nick, "");
assert!(member.roles.is_empty());
assert_eq!(member.joined_at, "");
assert_eq!(member.op_user_id, "");
let value = serde_json::to_value(&member).unwrap();
assert_eq!(value["guild_id"], "");
assert_eq!(value["nick"], "");
assert_eq!(value["roles"], serde_json::json!([]));
assert_eq!(value["joined_at"], "");
assert!(value.get("op_user_id").is_none());
}
}