use chrono::{DateTime, Utc};
use serde_json::Value;
use crate::discord::{
GuildMemberListUpdateInfo, GuildMembersChunkInfo, MemberInfo,
avatar::{member_avatar_url, user_avatar_url},
events::{AppEvent, PresenceEventFields},
ids::{
Id,
marker::{GuildMarker, RoleMarker, UserMarker},
},
};
use super::{
presence::{parse_activities, parse_presence_entry},
shared::{display_name_from_parts_or_unknown, extra_fields, parse_id, parse_status},
};
pub(super) fn parse_member_upsert(data: &Value) -> Option<AppEvent> {
let guild_id = parse_id::<GuildMarker>(data.get("guild_id")?)?;
let member = parse_member_info(data, Some(guild_id))?;
Some(AppEvent::GuildMemberUpsert { guild_id, member })
}
pub(super) fn parse_member_add(data: &Value) -> Option<AppEvent> {
let guild_id = parse_id::<GuildMarker>(data.get("guild_id")?)?;
let member = parse_member_info(data, Some(guild_id))?;
Some(AppEvent::GuildMemberAdd { guild_id, member })
}
pub(super) fn parse_user_update(data: &Value) -> Option<AppEvent> {
let user_id = parse_id::<UserMarker>(data.get("id")?)?;
let username = data.get("username").and_then(Value::as_str)?.to_owned();
let global_name = data
.get("global_name")
.and_then(Value::as_str)
.map(str::to_owned);
Some(AppEvent::UserIdentityUpdate {
user_id,
username,
global_name,
avatar_url: user_avatar_url(user_id, data),
is_bot: data.get("bot").and_then(Value::as_bool).unwrap_or(false),
})
}
pub(super) fn parse_current_user_verification(data: &Value) -> Option<AppEvent> {
let email_verified = data.get("verified").and_then(Value::as_bool);
let phone_verified = data
.get("phone")
.map(|phone| phone.as_str().is_some_and(|phone| !phone.is_empty()));
let mfa_enabled = data.get("mfa_enabled").and_then(Value::as_bool);
(email_verified.is_some() || phone_verified.is_some() || mfa_enabled.is_some()).then_some(
AppEvent::CurrentUserVerification {
email_verified,
phone_verified,
mfa_enabled,
},
)
}
pub(super) fn parse_member_chunk(data: &Value) -> Vec<AppEvent> {
let Some(guild_id) = data.get("guild_id").and_then(parse_id::<GuildMarker>) else {
return Vec::new();
};
let members = data
.get("members")
.and_then(Value::as_array)
.map(|members| {
members
.iter()
.filter_map(|member| parse_member_info(member, Some(guild_id)))
.collect()
})
.unwrap_or_default();
let presences = data
.get("presences")
.and_then(Value::as_array)
.map(|presences| presences.iter().filter_map(parse_presence_entry).collect())
.unwrap_or_default();
vec![AppEvent::GuildMembersChunk {
chunk: GuildMembersChunkInfo {
guild_id,
members,
presences,
chunk_index: data.get("chunk_index").and_then(Value::as_u64),
chunk_count: data.get("chunk_count").and_then(Value::as_u64),
nonce: data.get("nonce").and_then(Value::as_str).map(str::to_owned),
not_found: parse_id_array(data.get("not_found")),
extra_fields: extra_fields(
data,
&[
"guild_id",
"members",
"presences",
"chunk_index",
"chunk_count",
"nonce",
"not_found",
],
),
},
}]
}
pub(super) fn parse_member_list_update(data: &Value) -> Vec<AppEvent> {
let Some(guild_id) = data.get("guild_id").and_then(parse_id::<GuildMarker>) else {
return Vec::new();
};
let Some(ops) = data.get("ops").and_then(Value::as_array) else {
return Vec::new();
};
let mut online_count = None;
let mut members = Vec::new();
let mut presences = Vec::new();
if let Some(groups) = data.get("groups").and_then(Value::as_array) {
online_count = Some(
groups
.iter()
.filter(|g| g.get("id").and_then(Value::as_str) != Some("offline"))
.filter_map(|g| g.get("count").and_then(Value::as_u64))
.map(|c| c as u32)
.sum(),
);
}
for op in ops {
match op.get("op").and_then(Value::as_str) {
Some("SYNC") => {
if let Some(items) = op.get("items").and_then(Value::as_array) {
for item in items {
if let Some(item) = parse_member_list_item(guild_id, item) {
members.push(item.member);
if let Some(presence) = item.presence {
presences.push(presence);
}
}
}
}
}
Some("INSERT" | "UPDATE") => {
if let Some(item) = op.get("item")
&& let Some(item) = parse_member_list_item(guild_id, item)
{
members.push(item.member);
if let Some(presence) = item.presence {
presences.push(presence);
}
}
}
_ => {}
}
}
vec![AppEvent::GuildMemberListUpdate {
update: GuildMemberListUpdateInfo {
guild_id,
list_id: data.get("id").and_then(Value::as_str).map(str::to_owned),
member_count: data.get("member_count").and_then(Value::as_u64),
online_count,
members,
presences,
groups: clone_array(data.get("groups")),
ops: ops.to_vec(),
extra_fields: extra_fields(
data,
&[
"guild_id",
"id",
"member_count",
"online_count",
"groups",
"ops",
],
),
},
}]
}
fn clone_array(value: Option<&Value>) -> Vec<Value> {
value
.and_then(Value::as_array)
.map(|values| values.to_vec())
.unwrap_or_default()
}
fn parse_id_array<T>(value: Option<&Value>) -> Vec<Id<T>> {
value
.and_then(Value::as_array)
.map(|values| values.iter().filter_map(parse_id::<T>).collect())
.unwrap_or_default()
}
struct MemberListItemInfo {
member: MemberInfo,
presence: Option<PresenceEventFields>,
}
fn parse_member_list_item(guild_id: Id<GuildMarker>, item: &Value) -> Option<MemberListItemInfo> {
let member = item
.get("member")
.or_else(|| item.get("user").map(|_| item))?;
let member_info = parse_member_info(member, Some(guild_id))?;
let user_id = member_info.user_id;
let presence = member.get("presence");
let status = presence
.and_then(|presence| presence.get("status"))
.and_then(Value::as_str)
.map(parse_status);
let activities = presence.map(parse_activities).unwrap_or_default();
let presence = status.map(|status| PresenceEventFields {
user_id,
status,
activities,
});
Some(MemberListItemInfo {
member: member_info,
presence,
})
}
pub(super) fn parse_member_remove(data: &Value) -> Option<AppEvent> {
let guild_id = parse_id::<GuildMarker>(data.get("guild_id")?)?;
let user = data.get("user")?;
let user_id = parse_id::<UserMarker>(user.get("id")?)?;
Some(AppEvent::GuildMemberRemove { guild_id, user_id })
}
pub(super) fn parse_member_info(
value: &Value,
guild_id: Option<Id<GuildMarker>>,
) -> Option<MemberInfo> {
let communication_disabled_until_present = value.get("communication_disabled_until").is_some();
let user = value.get("user");
let user_id = user
.and_then(|user| user.get("id"))
.or_else(|| value.get("user_id"))
.or_else(|| value.get("id"))
.and_then(parse_id::<UserMarker>)?;
let nick = value.get("nick").and_then(Value::as_str);
let global_name = user
.and_then(|user| user.get("global_name"))
.and_then(Value::as_str);
let username = user
.and_then(|user| user.get("username"))
.and_then(Value::as_str);
let display_name = display_name_from_parts_or_unknown(nick, global_name, username);
let is_bot = user
.and_then(|user| user.get("bot"))
.and_then(Value::as_bool)
.unwrap_or(false);
Some(MemberInfo {
user_id,
display_name,
username: username.map(str::to_owned),
is_bot,
avatar_url: member_avatar_url(guild_id, user_id, Some(value), user),
role_ids: value
.get("roles")
.and_then(Value::as_array)
.map(|roles| roles.iter().filter_map(parse_id::<RoleMarker>).collect())
.unwrap_or_default(),
joined_at: value
.get("joined_at")
.and_then(Value::as_str)
.and_then(|joined_at| DateTime::parse_from_rfc3339(joined_at).ok())
.map(|joined_at| joined_at.with_timezone(&Utc)),
flags: value.get("flags").and_then(|flags| {
flags
.as_u64()
.or_else(|| flags.as_str().and_then(|flags| flags.parse().ok()))
}),
pending: value.get("pending").and_then(Value::as_bool),
communication_disabled_until: value
.get("communication_disabled_until")
.and_then(Value::as_str)
.and_then(|until| DateTime::parse_from_rfc3339(until).ok())
.map(|until| until.with_timezone(&Utc)),
communication_disabled_until_present,
})
}