use serde_json::Value;
use crate::{
discord::{
AttachmentInfo, AttachmentUpdate, ComponentMediaInfo, ComponentMediaItemInfo,
ComponentSelectKind, ComponentSelectOptionInfo, EmbedFieldInfo, EmbedInfo, MentionInfo,
MessageComponentInfo, MessageInfo, MessageInteractionInfo, MessageKind,
MessageReferenceInfo, MessageSnapshotInfo, MessageUpdateDispatchInfo, PollAnswerInfo,
PollInfo, ReactionEmoji, ReactionInfo, ReplyInfo, StickerFormat, StickerInfo,
avatar::user_avatar_url,
events::AppEvent,
ids::{
Id,
marker::{
AttachmentMarker, ChannelMarker, EmojiMarker, GuildMarker, MessageMarker,
RoleMarker, StickerMarker, UserMarker, WebhookMarker,
},
},
},
logging,
};
use super::shared::{
display_name_from_parts_or_unknown, extra_fields, parse_id, parse_nonnegative_i64,
};
pub(crate) fn parse_message_info(data: &Value) -> Option<MessageInfo> {
let channel_id = parse_id::<ChannelMarker>(data.get("channel_id")?)?;
let message_id = parse_id::<MessageMarker>(data.get("id")?)?;
let nonce = data.get("nonce").and_then(parse_id::<MessageMarker>);
let webhook_id = data.get("webhook_id").and_then(parse_id::<WebhookMarker>);
let author = data.get("author")?;
let author_id = parse_id::<UserMarker>(author.get("id")?)?;
let author_name = if webhook_id.is_some() {
display_name_from_parts_or_unknown(
None,
None,
author.get("username").and_then(Value::as_str),
)
} else {
message_author_display_name(data, author)
};
let author_avatar_url = user_avatar_url(author_id, author);
let author_is_bot = author.get("bot").and_then(Value::as_bool).unwrap_or(false);
let author_role_ids = parse_message_author_role_ids(data);
let author_role_ids_present = author_role_ids.is_some();
let author_role_ids = author_role_ids.unwrap_or_default();
let guild_id = data.get("guild_id").and_then(parse_id::<GuildMarker>);
let message_kind = data
.get("type")
.and_then(Value::as_u64)
.and_then(|value| u8::try_from(value).ok())
.map(MessageKind::new)
.unwrap_or_default();
let content = data
.get("content")
.and_then(Value::as_str)
.map(str::to_owned);
let interaction = parse_message_interaction_info(data);
let stickers = parse_sticker_items(data.get("sticker_items"));
let mentions = parse_mentions(data.get("mentions"));
let mention_everyone = data
.get("mention_everyone")
.and_then(Value::as_bool)
.unwrap_or(false);
let mention_roles = parse_mention_roles(data.get("mention_roles"));
let flags = data.get("flags").and_then(Value::as_u64).unwrap_or(0);
let attachments = parse_attachments(data.get("attachments"));
let embeds = parse_embeds(data.get("embeds"));
let components = parse_message_components(data.get("components"));
let reply = data.get("referenced_message").and_then(parse_reply_info);
let poll = data
.get("poll")
.and_then(parse_poll_info)
.or_else(|| parse_poll_result_embed(data.get("embeds")));
let reference = data
.get("message_reference")
.map(parse_message_reference_info);
let source_channel_id = reference
.as_ref()
.and_then(|reference| reference.channel_id);
let forwarded_snapshots =
parse_message_snapshots(data.get("message_snapshots"), source_channel_id);
let edited_timestamp = data
.get("edited_timestamp")
.and_then(Value::as_str)
.map(str::to_owned);
Some(MessageInfo {
guild_id,
channel_id,
message_id,
nonce,
webhook_id,
author_id,
author: author_name,
author_avatar_url,
author_is_bot,
author_role_ids,
author_role_ids_present,
message_kind,
interaction,
reference,
reply,
poll,
pinned: data.get("pinned").and_then(Value::as_bool).unwrap_or(false),
reactions: parse_reactions(data.get("reactions")),
content,
stickers,
mentions,
mention_everyone,
mention_roles,
flags,
attachments,
embeds,
components,
forwarded_snapshots,
edited_timestamp,
})
}
fn parse_message_interaction_info(data: &Value) -> Option<MessageInteractionInfo> {
let legacy = data.get("interaction");
let metadata = data.get("interaction_metadata");
let user = metadata
.and_then(|value| value.get("user"))
.or_else(|| legacy.and_then(|value| value.get("user")))?;
let user_id = user.get("id").and_then(parse_id::<UserMarker>);
let command_name = legacy
.and_then(|value| value.get("name"))
.or_else(|| metadata.and_then(|value| value.get("name")))
.and_then(Value::as_str)
.filter(|name| !name.trim().is_empty())
.map(str::to_owned);
Some(MessageInteractionInfo {
user_id,
user: display_name_from_parts_or_unknown(
None,
user.get("global_name").and_then(Value::as_str),
user.get("username").and_then(Value::as_str),
),
command_name,
})
}
fn parse_message_author_role_ids(data: &Value) -> Option<Vec<Id<RoleMarker>>> {
data.get("member")
.and_then(|member| member.get("roles"))
.and_then(Value::as_array)
.map(|roles| roles.iter().filter_map(parse_id::<RoleMarker>).collect())
}
fn parse_message_reference_info(value: &Value) -> MessageReferenceInfo {
MessageReferenceInfo {
guild_id: value.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: value.get("channel_id").and_then(parse_id::<ChannelMarker>),
message_id: value.get("message_id").and_then(parse_id::<MessageMarker>),
}
}
pub(super) fn parse_attachments(value: Option<&Value>) -> Vec<AttachmentInfo> {
value
.and_then(Value::as_array)
.map(|items| items.iter().filter_map(parse_attachment).collect())
.unwrap_or_default()
}
pub(super) fn parse_sticker_items(value: Option<&Value>) -> Vec<StickerInfo> {
value
.and_then(Value::as_array)
.map(|items| items.iter().filter_map(parse_sticker_item).collect())
.unwrap_or_default()
}
fn parse_message_components(value: Option<&Value>) -> Vec<MessageComponentInfo> {
value
.and_then(Value::as_array)
.map(|items| items.iter().map(parse_message_component).collect())
.unwrap_or_default()
}
fn parse_message_component(value: &Value) -> MessageComponentInfo {
let kind = value.get("type").and_then(Value::as_u64).unwrap_or(0);
match kind {
1 => MessageComponentInfo::ActionRow {
components: parse_message_components(value.get("components")),
},
2 => MessageComponentInfo::Button {
label: value
.get("label")
.and_then(Value::as_str)
.map(str::to_owned),
emoji: value.get("emoji").and_then(parse_component_emoji),
url: value.get("url").and_then(Value::as_str).map(str::to_owned),
disabled: value
.get("disabled")
.and_then(Value::as_bool)
.unwrap_or(false),
},
3 | 5..=8 => MessageComponentInfo::Select {
kind: match kind {
5 => ComponentSelectKind::User,
6 => ComponentSelectKind::Role,
7 => ComponentSelectKind::Mentionable,
8 => ComponentSelectKind::Channel,
_ => ComponentSelectKind::String,
},
placeholder: value
.get("placeholder")
.and_then(Value::as_str)
.map(str::to_owned),
options: value
.get("options")
.and_then(Value::as_array)
.map(|options| {
options
.iter()
.filter_map(parse_component_select_option)
.collect()
})
.unwrap_or_default(),
disabled: value
.get("disabled")
.and_then(Value::as_bool)
.unwrap_or(false),
},
9 => MessageComponentInfo::Section {
components: parse_message_components(value.get("components")),
accessory: value
.get("accessory")
.map(parse_message_component)
.map(Box::new),
},
10 => MessageComponentInfo::TextDisplay {
content: value
.get("content")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned(),
},
11 => parse_component_media(value.get("media"))
.map(|media| MessageComponentInfo::Thumbnail {
media,
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
spoiler: value
.get("spoiler")
.and_then(Value::as_bool)
.unwrap_or(false),
})
.unwrap_or(MessageComponentInfo::Unknown { kind }),
12 => MessageComponentInfo::MediaGallery {
items: value
.get("items")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(parse_component_media_item)
.collect()
})
.unwrap_or_default(),
},
13 => parse_component_media(value.get("file"))
.map(|file| MessageComponentInfo::File {
name: value.get("name").and_then(Value::as_str).map(str::to_owned),
size: value.get("size").and_then(Value::as_u64),
spoiler: value
.get("spoiler")
.and_then(Value::as_bool)
.unwrap_or(false),
file,
})
.unwrap_or(MessageComponentInfo::Unknown { kind }),
14 => MessageComponentInfo::Separator {
divider: value
.get("divider")
.and_then(Value::as_bool)
.unwrap_or(true),
spacing: value
.get("spacing")
.and_then(Value::as_u64)
.and_then(|spacing| u8::try_from(spacing).ok())
.unwrap_or(1),
},
17 => MessageComponentInfo::Container {
components: parse_message_components(value.get("components")),
accent_color: value
.get("accent_color")
.and_then(Value::as_u64)
.and_then(|color| u32::try_from(color).ok()),
spoiler: value
.get("spoiler")
.and_then(Value::as_bool)
.unwrap_or(false),
},
_ => MessageComponentInfo::Unknown { kind },
}
}
fn parse_component_media(value: Option<&Value>) -> Option<ComponentMediaInfo> {
let value = value?;
Some(ComponentMediaInfo {
url: value.get("url")?.as_str()?.to_owned(),
proxy_url: value
.get("proxy_url")
.and_then(Value::as_str)
.map(str::to_owned),
content_type: value
.get("content_type")
.and_then(Value::as_str)
.map(str::to_owned),
width: value.get("width").and_then(Value::as_u64),
height: value.get("height").and_then(Value::as_u64),
flags: value
.get("flags")
.and_then(Value::as_u64)
.unwrap_or_default(),
})
}
fn parse_component_media_item(value: &Value) -> Option<ComponentMediaItemInfo> {
Some(ComponentMediaItemInfo {
media: parse_component_media(value.get("media"))?,
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
spoiler: value
.get("spoiler")
.and_then(Value::as_bool)
.unwrap_or(false),
})
}
fn parse_component_select_option(value: &Value) -> Option<ComponentSelectOptionInfo> {
Some(ComponentSelectOptionInfo {
label: value.get("label")?.as_str()?.to_owned(),
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
emoji: value.get("emoji").and_then(parse_component_emoji),
selected: value
.get("default")
.and_then(Value::as_bool)
.unwrap_or(false),
})
}
fn parse_component_emoji(value: &Value) -> Option<String> {
let name = value.get("name").and_then(Value::as_str)?;
match value.get("id").and_then(parse_id::<EmojiMarker>) {
Some(id)
if value
.get("animated")
.and_then(Value::as_bool)
.unwrap_or(false) =>
{
Some(format!("<a:{name}:{id}>"))
}
Some(id) => Some(format!("<:{name}:{id}>")),
None => Some(name.to_owned()),
}
}
fn parse_sticker_item(value: &Value) -> Option<StickerInfo> {
let id = parse_id::<StickerMarker>(value.get("id")?)?;
let name = value.get("name").and_then(Value::as_str)?.to_owned();
let format = StickerFormat::from_discord(value.get("format_type").and_then(Value::as_u64));
Some(StickerInfo::new(id, name, format))
}
pub(super) fn parse_embeds(value: Option<&Value>) -> Vec<EmbedInfo> {
value
.and_then(Value::as_array)
.map(|items| items.iter().filter_map(parse_embed).collect())
.unwrap_or_default()
}
fn parse_embed(value: &Value) -> Option<EmbedInfo> {
let embed_type = value.get("type").and_then(Value::as_str).map(str::to_owned);
if embed_type.as_deref() == Some("poll_result") {
return None;
}
let fields = value
.get("fields")
.and_then(Value::as_array)
.map(|fields| fields.iter().filter_map(parse_embed_field).collect())
.unwrap_or_default();
let thumbnail = parse_embed_media(value.get("thumbnail"));
let image = parse_embed_media(value.get("image"));
let video = parse_embed_media(value.get("video"));
let (gifv_image_url, gifv_image_proxy_url) = if embed_type.as_deref() == Some("gifv") {
match video.url.as_deref().and_then(giphy_gifv_image_url) {
Some(url) => (Some(url), None),
None => (thumbnail.url.clone(), thumbnail.proxy_url.clone()),
}
} else {
(None, None)
};
let embed = EmbedInfo {
kind: embed_type,
flags: value
.get("flags")
.and_then(Value::as_u64)
.unwrap_or_default(),
color: value
.get("color")
.and_then(Value::as_u64)
.and_then(|color| u32::try_from(color).ok()),
provider_name: value
.get("provider")
.and_then(|provider| provider.get("name"))
.and_then(Value::as_str)
.map(str::to_owned),
provider_url: value
.get("provider")
.and_then(|provider| provider.get("url"))
.and_then(Value::as_str)
.map(str::to_owned),
author_name: value
.get("author")
.and_then(|author| author.get("name"))
.and_then(Value::as_str)
.map(str::to_owned),
author_url: value
.get("author")
.and_then(|author| author.get("url"))
.and_then(Value::as_str)
.map(str::to_owned),
title: value
.get("title")
.and_then(Value::as_str)
.map(str::to_owned),
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
timestamp: value
.get("timestamp")
.and_then(Value::as_str)
.map(str::to_owned),
fields,
footer_text: value
.get("footer")
.and_then(|footer| footer.get("text"))
.and_then(Value::as_str)
.map(str::to_owned),
url: value.get("url").and_then(Value::as_str).map(str::to_owned),
thumbnail_url: thumbnail.url,
thumbnail_proxy_url: thumbnail.proxy_url,
thumbnail_width: thumbnail.width,
thumbnail_height: thumbnail.height,
thumbnail_content_type: thumbnail.content_type,
thumbnail_description: thumbnail.description,
thumbnail_flags: thumbnail.flags,
image_url: image.url,
image_proxy_url: image.proxy_url,
image_width: image.width,
image_height: image.height,
image_content_type: image.content_type,
image_description: image.description,
image_flags: image.flags,
gifv_image_url,
gifv_image_proxy_url,
video_url: video.url,
video_proxy_url: video.proxy_url,
video_width: video.width,
video_height: video.height,
video_content_type: video.content_type,
video_description: video.description,
video_flags: video.flags,
};
embed_has_renderable_content(&embed).then_some(embed)
}
#[derive(Default)]
struct ParsedEmbedMedia {
url: Option<String>,
proxy_url: Option<String>,
width: Option<u64>,
height: Option<u64>,
content_type: Option<String>,
description: Option<String>,
flags: u64,
}
fn parse_embed_media(value: Option<&Value>) -> ParsedEmbedMedia {
let Some(value) = value else {
return ParsedEmbedMedia::default();
};
ParsedEmbedMedia {
url: value.get("url").and_then(Value::as_str).map(str::to_owned),
proxy_url: value
.get("proxy_url")
.and_then(Value::as_str)
.map(str::to_owned),
width: value.get("width").and_then(Value::as_u64),
height: value.get("height").and_then(Value::as_u64),
content_type: value
.get("content_type")
.and_then(Value::as_str)
.map(str::to_owned),
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
flags: value
.get("flags")
.and_then(Value::as_u64)
.unwrap_or_default(),
}
}
fn giphy_gifv_image_url(video_url: &str) -> Option<String> {
let mut url = reqwest::Url::parse(video_url).ok()?;
if !matches!(url.scheme(), "http" | "https") {
return None;
}
let host = url.host_str()?;
if host != "giphy.com" && !host.ends_with(".giphy.com") {
return None;
}
let image_path = format!("{}.webp", url.path().strip_suffix(".mp4")?);
url.set_path(&image_path);
Some(url.into())
}
fn parse_embed_field(value: &Value) -> Option<EmbedFieldInfo> {
Some(EmbedFieldInfo {
name: value.get("name")?.as_str()?.to_owned(),
value: value.get("value")?.as_str()?.to_owned(),
inline: value
.get("inline")
.and_then(Value::as_bool)
.unwrap_or(false),
})
}
fn embed_has_renderable_content(embed: &EmbedInfo) -> bool {
embed.provider_name.is_some()
|| embed.provider_url.is_some()
|| embed.author_name.is_some()
|| embed.author_url.is_some()
|| embed.title.is_some()
|| embed.description.is_some()
|| embed.timestamp.is_some()
|| !embed.fields.is_empty()
|| embed.footer_text.is_some()
|| embed.url.is_some()
|| embed.thumbnail_url.is_some()
|| embed.image_url.is_some()
|| embed.video_url.is_some()
|| embed.video_proxy_url.is_some()
|| embed.thumbnail_description.is_some()
|| embed.image_description.is_some()
|| embed.video_description.is_some()
}
fn parse_message_snapshots(
value: Option<&Value>,
source_channel_id: Option<Id<ChannelMarker>>,
) -> Vec<MessageSnapshotInfo> {
value
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(|item| parse_message_snapshot(item, source_channel_id))
.collect()
})
.unwrap_or_default()
}
fn parse_reply_info(value: &Value) -> Option<ReplyInfo> {
if value.is_null() {
return None;
}
let author = value.get("author")?;
let author_id = author.get("id").and_then(parse_id::<UserMarker>);
let author_name = message_author_display_name(value, author);
let content = value
.get("content")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_owned);
let stickers = parse_sticker_items(value.get("sticker_items"));
let mentions = parse_mentions(value.get("mentions"));
Some(ReplyInfo {
author_id,
author: author_name,
content,
stickers,
mentions,
})
}
pub(super) fn parse_mentions(value: Option<&Value>) -> Vec<MentionInfo> {
value
.and_then(Value::as_array)
.map(|mentions| mentions.iter().filter_map(parse_mention_info).collect())
.unwrap_or_default()
}
fn parse_mention_roles(value: Option<&Value>) -> Vec<Id<RoleMarker>> {
value
.and_then(Value::as_array)
.map(|roles| roles.iter().filter_map(parse_id::<RoleMarker>).collect())
.unwrap_or_default()
}
fn parse_reactions(value: Option<&Value>) -> Vec<ReactionInfo> {
value
.and_then(Value::as_array)
.map(|reactions| reactions.iter().filter_map(parse_reaction_info).collect())
.unwrap_or_default()
}
fn parse_reaction_info(value: &Value) -> Option<ReactionInfo> {
Some(ReactionInfo {
emoji: parse_reaction_emoji(value.get("emoji")?)?,
count: value.get("count").and_then(Value::as_u64).unwrap_or(0),
me: value.get("me").and_then(Value::as_bool).unwrap_or(false),
})
}
pub(super) fn parse_reaction_emoji(value: &Value) -> Option<ReactionEmoji> {
if let Some(id) = value.get("id").and_then(parse_id::<EmojiMarker>) {
return Some(ReactionEmoji::Custom {
id,
name: value.get("name").and_then(Value::as_str).map(str::to_owned),
animated: value
.get("animated")
.and_then(Value::as_bool)
.unwrap_or(false),
});
}
value
.get("name")
.and_then(Value::as_str)
.filter(|name| !name.is_empty())
.map(|name| ReactionEmoji::Unicode(name.to_owned()))
}
fn parse_mention_info(value: &Value) -> Option<MentionInfo> {
let user_id = parse_id::<UserMarker>(value.get("id")?)?;
let member = value.get("member");
let nick = value
.get("member")
.and_then(|member| member.get("nick"))
.and_then(Value::as_str);
let global_name = value.get("global_name").and_then(Value::as_str);
let username = value.get("username").and_then(Value::as_str);
let display_name = display_name_from_parts_or_unknown(nick, global_name, username);
log_mention_raw_fields(user_id, member, nick, global_name, username, &display_name);
Some(MentionInfo {
user_id,
guild_nick: nick.filter(|value| !value.is_empty()).map(str::to_owned),
display_name,
})
}
fn log_mention_raw_fields(
user_id: Id<UserMarker>,
member: Option<&Value>,
nick: Option<&str>,
global_name: Option<&str>,
username: Option<&str>,
display_name: &str,
) {
logging::debug(
"gateway",
format!(
"mention raw fields user_id={} has_member={} nick={} global_name={} username={} display_name={}",
user_id.get(),
member.is_some(),
log_optional_name(nick),
log_optional_name(global_name),
log_optional_name(username),
display_name,
),
);
}
fn log_optional_name(value: Option<&str>) -> &str {
value.unwrap_or("<missing>")
}
fn message_author_display_name(message: &Value, author: &Value) -> String {
let nick = message
.get("member")
.and_then(|member| member.get("nick"))
.and_then(Value::as_str);
let global_name = author.get("global_name").and_then(Value::as_str);
let username = author.get("username").and_then(Value::as_str);
display_name_from_parts_or_unknown(nick, global_name, username)
}
pub(super) fn parse_poll_info(value: &Value) -> Option<PollInfo> {
if value.is_null() {
return None;
}
let question = value
.get("question")
.and_then(|question| question.get("text"))
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.unwrap_or("<no question text>")
.to_owned();
let answers: Vec<PollAnswerInfo> = value
.get("answers")
.and_then(Value::as_array)
.map(|answers| answers.iter().filter_map(parse_poll_answer_info).collect())
.unwrap_or_default();
let allow_multiselect = value
.get("allow_multiselect")
.and_then(Value::as_bool)
.unwrap_or(false);
let results = value.get("results");
let results_finalized = results
.and_then(|results| results.get("is_finalized"))
.and_then(Value::as_bool);
let total_votes = results
.and_then(|results| results.get("answer_counts"))
.and_then(Value::as_array)
.map(|counts| {
counts
.iter()
.filter_map(|count| count.get("count").and_then(Value::as_u64))
.sum()
});
Some(PollInfo {
question,
answers: answers
.into_iter()
.map(|mut answer| {
if let Some(count) = poll_answer_count(results, answer.answer_id) {
answer.vote_count = Some(count.0);
answer.me_voted = count.1;
}
answer
})
.collect(),
allow_multiselect,
results_finalized,
total_votes,
})
}
pub(super) fn parse_poll_result_embed(value: Option<&Value>) -> Option<PollInfo> {
let embed = value?
.as_array()?
.iter()
.find(|embed| embed.get("type").and_then(Value::as_str) == Some("poll_result"))?;
let fields = embed.get("fields")?.as_array()?;
let mut question = None;
let mut winner_id = None;
let mut winner_text = None;
let mut winner_votes = None;
let mut total_votes = None;
for field in fields {
let Some(name) = field.get("name").and_then(Value::as_str) else {
continue;
};
let Some(value) = field.get("value").and_then(Value::as_str) else {
continue;
};
match name {
"poll_question_text" => question = Some(value.to_owned()),
"victor_answer_id" => winner_id = value.parse::<u8>().ok(),
"victor_answer_text" => winner_text = Some(value.to_owned()),
"victor_answer_votes" => winner_votes = value.parse::<u64>().ok(),
"total_votes" => total_votes = value.parse::<u64>().ok(),
_ => {}
}
}
let answers = winner_text
.map(|text| {
vec![PollAnswerInfo {
answer_id: winner_id.unwrap_or(1),
text,
vote_count: winner_votes,
me_voted: false,
}]
})
.unwrap_or_default();
Some(PollInfo {
question: question.unwrap_or_else(|| "Poll results".to_owned()),
answers,
allow_multiselect: false,
results_finalized: Some(true),
total_votes,
})
}
fn parse_poll_answer_info(value: &Value) -> Option<PollAnswerInfo> {
let answer_id = value
.get("answer_id")
.and_then(Value::as_u64)
.and_then(|value| u8::try_from(value).ok())?;
let text = value
.get("poll_media")
.and_then(|media| media.get("text"))
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.unwrap_or("<no answer text>")
.to_owned();
Some(PollAnswerInfo {
answer_id,
text,
vote_count: None,
me_voted: false,
})
}
fn poll_answer_count(results: Option<&Value>, answer_id: u8) -> Option<(u64, bool)> {
results?
.get("answer_counts")?
.as_array()?
.iter()
.find(|count| {
count
.get("id")
.and_then(Value::as_u64)
.is_some_and(|id| id == u64::from(answer_id))
})
.map(|count| {
(
count.get("count").and_then(Value::as_u64).unwrap_or(0),
count
.get("me_voted")
.and_then(Value::as_bool)
.unwrap_or(false),
)
})
}
fn parse_message_snapshot(
value: &Value,
source_channel_id: Option<Id<ChannelMarker>>,
) -> Option<MessageSnapshotInfo> {
let message = value.get("message")?;
let content = message
.get("content")
.and_then(Value::as_str)
.map(str::to_owned);
let stickers = parse_sticker_items(message.get("sticker_items"));
let flags = message
.get("flags")
.and_then(Value::as_u64)
.unwrap_or_default();
let attachments = parse_attachments(message.get("attachments"));
let embeds = parse_embeds(message.get("embeds"));
let components = parse_message_components(message.get("components"));
let mentions = parse_mentions(message.get("mentions"));
let timestamp = message
.get("timestamp")
.and_then(Value::as_str)
.map(str::to_owned);
if content.as_deref().is_some_and(|value| !value.is_empty())
|| !stickers.is_empty()
|| !attachments.is_empty()
|| !embeds.is_empty()
|| !components.is_empty()
|| source_channel_id.is_some()
|| timestamp.is_some()
{
Some(MessageSnapshotInfo {
content,
stickers,
mentions,
flags,
attachments,
embeds,
components,
source_channel_id,
timestamp,
})
} else {
None
}
}
fn parse_attachment(value: &Value) -> Option<AttachmentInfo> {
let url = value
.get("url")
.and_then(Value::as_str)
.or_else(|| value.get("proxy_url").and_then(Value::as_str))?
.to_owned();
let proxy_url = value
.get("proxy_url")
.and_then(Value::as_str)
.unwrap_or(url.as_str())
.to_owned();
Some(AttachmentInfo {
id: parse_id::<AttachmentMarker>(value.get("id")?)?,
filename: value.get("filename")?.as_str()?.to_owned(),
url,
proxy_url,
content_type: value
.get("content_type")
.and_then(Value::as_str)
.map(str::to_owned),
size: value
.get("size")
.and_then(Value::as_u64)
.unwrap_or_default(),
width: value.get("width").and_then(Value::as_u64),
height: value.get("height").and_then(Value::as_u64),
description: value
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
flags: value
.get("flags")
.and_then(Value::as_u64)
.unwrap_or_default(),
})
}
pub(super) fn parse_message_create(data: &Value) -> Option<AppEvent> {
let message = parse_message_info(data)?;
Some(AppEvent::MessageCreate { message })
}
pub(super) fn parse_message_update(data: &Value) -> Option<AppEvent> {
let channel_id = parse_id::<ChannelMarker>(data.get("channel_id")?)?;
let message_id = parse_id::<MessageMarker>(data.get("id")?)?;
let guild_id = data.get("guild_id").and_then(parse_id::<GuildMarker>);
let content = data
.get("content")
.and_then(Value::as_str)
.map(str::to_owned);
let stickers = data
.get("sticker_items")
.map(|value| parse_sticker_items(Some(value)));
let attachments = if data.get("attachments").is_some() {
AttachmentUpdate::Replace(parse_attachments(data.get("attachments")))
} else {
AttachmentUpdate::Unchanged
};
let poll = data
.get("poll")
.and_then(parse_poll_info)
.or_else(|| parse_poll_result_embed(data.get("embeds")));
let embeds = data.get("embeds").map(|value| parse_embeds(Some(value)));
let components = data
.get("components")
.map(|value| parse_message_components(Some(value)));
let mentions = data
.get("mentions")
.map(|value| parse_mentions(Some(value)));
let mention_everyone = data.get("mention_everyone").and_then(Value::as_bool);
let mention_roles = data
.get("mention_roles")
.map(|value| parse_mention_roles(Some(value)));
let flags = data.get("flags").and_then(Value::as_u64);
let pinned = data.get("pinned").and_then(Value::as_bool);
let edited_timestamp = data
.get("edited_timestamp")
.and_then(Value::as_str)
.map(str::to_owned);
Some(AppEvent::MessageUpdateDispatch {
update: MessageUpdateDispatchInfo {
guild_id,
channel_id,
message_id,
fields: crate::discord::MessageUpdateEventFields {
poll,
content,
stickers,
mentions,
mention_everyone,
mention_roles,
flags,
pinned,
attachments,
embeds,
components,
edited_timestamp,
},
extra_fields: extra_fields(
data,
&[
"id",
"guild_id",
"channel_id",
"poll",
"content",
"sticker_items",
"mentions",
"mention_everyone",
"mention_roles",
"flags",
"pinned",
"attachments",
"embeds",
"components",
"edited_timestamp",
],
),
},
})
}
pub(super) fn parse_message_delete(data: &Value) -> Option<AppEvent> {
let channel_id = parse_id::<ChannelMarker>(data.get("channel_id")?)?;
let message_id = parse_id::<MessageMarker>(data.get("id")?)?;
let guild_id = data.get("guild_id").and_then(parse_id::<GuildMarker>);
Some(AppEvent::MessageDelete {
guild_id,
channel_id,
message_id,
})
}
pub(super) fn parse_message_delete_bulk(data: &Value) -> Option<AppEvent> {
let channel_id = parse_id::<ChannelMarker>(data.get("channel_id")?)?;
let message_ids = data
.get("ids")?
.as_array()?
.iter()
.filter_map(parse_id::<MessageMarker>)
.collect::<Vec<_>>();
if message_ids.is_empty() {
return None;
}
let guild_id = data.get("guild_id").and_then(parse_id::<GuildMarker>);
Some(AppEvent::MessageDeleteBulk {
guild_id,
channel_id,
message_ids,
})
}
pub(super) fn parse_message_ack(data: &Value) -> Option<AppEvent> {
Some(AppEvent::MessageAck {
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
message_id: parse_id::<MessageMarker>(data.get("message_id")?)?,
mention_count: data
.get("mention_count")
.and_then(Value::as_u64)
.and_then(|count| u32::try_from(count).ok()),
flags: data.get("flags").and_then(Value::as_u64),
last_viewed: data.get("last_viewed").and_then(Value::as_u64),
version: Some(parse_nonnegative_i64(data.get("version")?)?),
})
}
pub(super) fn parse_channel_pins_update(data: &Value) -> Option<AppEvent> {
Some(AppEvent::ChannelPinsUpdate {
guild_id: data.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
last_pin_timestamp: data
.get("last_pin_timestamp")
.and_then(Value::as_str)
.map(str::to_owned),
})
}
pub(super) fn parse_message_reaction_add(data: &Value) -> Option<AppEvent> {
Some(AppEvent::MessageReactionAdd {
guild_id: data.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
message_id: parse_id::<MessageMarker>(data.get("message_id")?)?,
user_id: parse_id::<UserMarker>(data.get("user_id")?)?,
emoji: parse_reaction_emoji(data.get("emoji")?)?,
})
}
pub(super) fn parse_message_reaction_remove(data: &Value) -> Option<AppEvent> {
Some(AppEvent::MessageReactionRemove {
guild_id: data.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
message_id: parse_id::<MessageMarker>(data.get("message_id")?)?,
user_id: parse_id::<UserMarker>(data.get("user_id")?)?,
emoji: parse_reaction_emoji(data.get("emoji")?)?,
})
}
pub(super) fn parse_message_reaction_remove_all(data: &Value) -> Option<AppEvent> {
Some(AppEvent::MessageReactionRemoveAll {
guild_id: data.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
message_id: parse_id::<MessageMarker>(data.get("message_id")?)?,
})
}
pub(super) fn parse_message_reaction_remove_emoji(data: &Value) -> Option<AppEvent> {
Some(AppEvent::MessageReactionRemoveEmoji {
guild_id: data.get("guild_id").and_then(parse_id::<GuildMarker>),
channel_id: parse_id::<ChannelMarker>(data.get("channel_id")?)?,
message_id: parse_id::<MessageMarker>(data.get("message_id")?)?,
emoji: parse_reaction_emoji(data.get("emoji")?)?,
})
}