use std::str::FromStr;
use wacore_binary::{Jid, JidExt};
use waproto::whatsapp as wa;
macro_rules! with_context_info_fields {
($callback:ident!($($prefix:tt)*)) => {
$callback!($($prefix)*
extended_text_message,
image_message,
video_message,
ptv_message,
audio_message,
document_message,
sticker_message,
location_message,
live_location_message,
contact_message,
contacts_array_message,
buttons_message,
buttons_response_message,
list_message,
list_response_message,
template_message,
template_button_reply_message,
interactive_message,
interactive_response_message,
poll_creation_message,
poll_creation_message_v2,
poll_creation_message_v3,
product_message,
order_message,
group_invite_message,
event_message,
sticker_pack_message,
newsletter_admin_invite_message,
)
};
}
macro_rules! for_each_context_info_message {
($msg:expr, $ctx:ident, $body:block) => {
with_context_info_fields!(for_each_context_info_impl!($msg, $ctx, $body,))
};
}
macro_rules! for_each_context_info_impl {
($msg:expr, $ctx:ident, $body:block, $($field:ident),+ $(,)?) => {
$(
if let Some(m) = $msg.$field.as_option_mut()
&& let Some($ctx) = m.context_info.as_option_mut()
{
$body
}
)+
};
}
macro_rules! find_context_info_ref {
($msg:expr) => {{ with_context_info_fields!(find_context_info_impl!($msg,)) }};
}
macro_rules! find_context_info_impl {
($msg:expr, $($field:ident),+ $(,)?) => {{
let mut found: Option<&wa::ContextInfo> = None;
$(
if found.is_none()
&& let Some(m) = $msg.$field.as_option()
&& let Some(ctx) = m.context_info.as_option()
{
found = Some(ctx);
}
)+
found
}};
}
pub trait MessageBuilderExt {
fn text(text: impl Into<String>) -> wa::Message;
fn text_with_context(text: impl Into<String>, context: wa::ContextInfo) -> wa::Message;
}
impl MessageBuilderExt for wa::Message {
fn text(text: impl Into<String>) -> wa::Message {
wa::Message {
conversation: Some(text.into()),
..Default::default()
}
}
fn text_with_context(text: impl Into<String>, context: wa::ContextInfo) -> wa::Message {
wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some(text.into()),
context_info: buffa::MessageField::some(context),
..Default::default()
}),
..Default::default()
}
}
}
pub trait MessageExt {
fn get_base_message(&self) -> &wa::Message;
fn into_base_message(self) -> wa::Message;
fn is_ephemeral(&self) -> bool;
fn is_view_once(&self) -> bool;
fn get_caption(&self) -> Option<&str>;
fn text_content(&self) -> Option<&str>;
fn prepare_for_quote(&self) -> Box<wa::Message>;
fn prepare_for_forward(&self) -> Box<wa::Message>;
fn set_context_info(&mut self, context: wa::ContextInfo) -> bool;
fn get_ephemeral_expiration(&self) -> Option<u32>;
fn set_ephemeral_expiration(&mut self, expiration: u32) -> bool;
fn is_forwarded(&self) -> bool;
fn mentions_any_bot(&self) -> bool;
}
impl MessageExt for wa::Message {
fn get_base_message(&self) -> &wa::Message {
let mut current = self;
if let Some(dsm) = self.device_sent_message.as_option()
&& let Some(msg) = dsm.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.ephemeral_message.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.view_once_message.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.view_once_message_v2.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.view_once_message_v2_extension.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.document_with_caption_message.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
if let Some(wrapper) = current.edited_message.as_option()
&& let Some(msg) = wrapper.message.as_option()
{
current = msg;
}
current
}
fn into_base_message(mut self) -> wa::Message {
macro_rules! peel_wrapper {
($field:ident) => {
if let Some(mut wrapper) = self.$field.take() {
if let Some(msg) = wrapper.message.take() {
self = msg;
} else {
self.$field = buffa::MessageField::some(wrapper);
}
}
};
}
peel_wrapper!(device_sent_message);
peel_wrapper!(ephemeral_message);
peel_wrapper!(view_once_message);
peel_wrapper!(view_once_message_v2);
peel_wrapper!(view_once_message_v2_extension);
peel_wrapper!(document_with_caption_message);
peel_wrapper!(edited_message);
self
}
fn is_ephemeral(&self) -> bool {
self.ephemeral_message.is_set()
}
fn is_view_once(&self) -> bool {
let mut current = self;
loop {
if current.view_once_message.is_set()
|| current.view_once_message_v2.is_set()
|| current.view_once_message_v2_extension.is_set()
{
return true;
}
if let Some(inner) = current
.device_sent_message
.as_option()
.and_then(|m| m.message.as_option())
{
current = inner;
continue;
}
if let Some(inner) = current
.ephemeral_message
.as_option()
.and_then(|m| m.message.as_option())
{
current = inner;
continue;
}
break;
}
let base = self.get_base_message();
matches!(
base.image_message.as_option().and_then(|m| m.view_once),
Some(true)
) || matches!(
base.video_message.as_option().and_then(|m| m.view_once),
Some(true)
) || matches!(
base.audio_message.as_option().and_then(|m| m.view_once),
Some(true)
) || matches!(
base.extended_text_message
.as_option()
.and_then(|m| m.view_once),
Some(true)
)
}
fn get_caption(&self) -> Option<&str> {
let base = self.get_base_message();
if let Some(msg) = base.image_message.as_option() {
return msg.caption.as_deref();
}
if let Some(msg) = base.video_message.as_option() {
return msg.caption.as_deref();
}
if let Some(msg) = base.document_message.as_option() {
return msg.caption.as_deref();
}
None
}
fn text_content(&self) -> Option<&str> {
let base = self.get_base_message();
if let Some(text) = &base.conversation
&& !text.is_empty()
{
return Some(text);
}
if let Some(ext_text) = base.extended_text_message.as_option()
&& let Some(text) = &ext_text.text
{
return Some(text);
}
None
}
fn prepare_for_quote(&self) -> Box<wa::Message> {
let mut msg = self.clone();
strip_nested_context_info(&mut msg, false);
Box::new(msg)
}
fn prepare_for_forward(&self) -> Box<wa::Message> {
const FREQUENTLY_FORWARDED_SENTINEL: u32 = 127;
let mut msg = self.clone();
strip_nested_context_info(&mut msg, true);
msg.message_context_info = buffa::MessageField::none();
macro_rules! set_forward {
($($field:ident),+ $(,)?) => {
$(
if let Some(m) = msg.$field.as_option_mut() {
let ctx = m.context_info.get_or_insert_default();
let n = ctx
.forwarding_score
.unwrap_or(0)
.saturating_add(u32::from(ctx.is_forwarded.unwrap_or(false)));
ctx.forwarding_score = Some(if n >= 5 {
FREQUENTLY_FORWARDED_SENTINEL
} else {
n
});
ctx.is_forwarded = Some(true);
return Box::new(msg);
}
)+
};
}
with_context_info_fields!(set_forward!());
if let Some(text) = msg.conversation.take() {
msg.extended_text_message =
buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some(text),
context_info: buffa::MessageField::some(wa::ContextInfo {
is_forwarded: Some(true),
forwarding_score: Some(0),
..Default::default()
}),
..Default::default()
});
}
Box::new(msg)
}
fn set_context_info(&mut self, context: wa::ContextInfo) -> bool {
macro_rules! try_attach {
($($field:ident),+ $(,)?) => {
$(
if let Some(m) = self.$field.as_option_mut() {
m.context_info = buffa::MessageField::some(context);
return true;
}
)+
};
}
with_context_info_fields!(try_attach!());
if let Some(text) = self.conversation.take() {
self.extended_text_message =
buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some(text),
context_info: buffa::MessageField::some(context),
..Default::default()
});
return true;
}
false
}
fn get_ephemeral_expiration(&self) -> Option<u32> {
macro_rules! check {
($($field:ident),+ $(,)?) => {
$(
if let Some(m) = self.$field.as_option()
&& let Some(ctx) = m.context_info.as_option()
&& let Some(exp) = ctx.expiration
&& exp > 0
{
return Some(exp);
}
)+
};
}
with_context_info_fields!(check!());
None
}
fn set_ephemeral_expiration(&mut self, expiration: u32) -> bool {
if expiration == 0 {
return false;
}
macro_rules! try_set {
($($field:ident),+ $(,)?) => {
$(
if let Some(m) = self.$field.as_option_mut() {
let ctx = m.context_info.get_or_insert_default();
ctx.expiration = Some(expiration);
return true;
}
)+
};
}
with_context_info_fields!(try_set!());
if let Some(text) = self.conversation.take() {
self.extended_text_message =
buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some(text),
context_info: buffa::MessageField::some(wa::ContextInfo {
expiration: Some(expiration),
..Default::default()
}),
..Default::default()
});
return true;
}
false
}
fn is_forwarded(&self) -> bool {
let base = self.get_base_message();
find_context_info_ref!(base)
.and_then(|ctx| ctx.is_forwarded)
.unwrap_or(false)
}
fn mentions_any_bot(&self) -> bool {
let base = self.get_base_message();
let Some(ctx) = find_context_info_ref!(base) else {
return false;
};
ctx.mentioned_jid
.iter()
.filter_map(|s| Jid::from_str(s).ok())
.any(|jid| jid.is_bot())
}
}
pub fn build_keep_in_chat_message(
key: wa::MessageKey,
keep: bool,
timestamp_ms: i64,
) -> wa::Message {
let keep_type = if keep {
wa::KeepType::KeepForAll
} else {
wa::KeepType::UndoKeepForAll
};
wa::Message {
keep_in_chat_message: buffa::MessageField::some(wa::message::KeepInChatMessage {
key: buffa::MessageField::some(key),
keep_type: Some(keep_type),
timestamp_ms: Some(timestamp_ms),
}),
..Default::default()
}
}
pub(crate) fn strip_nested_context_info(msg: &mut wa::Message, always_clear_quote: bool) {
fn clear_nested_context(ctx: &mut wa::ContextInfo, always_clear_quote: bool) {
ctx.mentioned_jid.clear();
ctx.group_mentions.clear();
let is_bot = !always_clear_quote
&& ctx
.participant
.as_ref()
.and_then(|p| Jid::from_str(p).ok())
.is_some_and(|jid| jid.is_bot());
if !is_bot {
ctx.quoted_message = Default::default();
ctx.stanza_id = None;
ctx.remote_jid = None;
ctx.participant = None;
}
}
for_each_context_info_message!(msg, ctx, {
clear_nested_context(ctx, always_clear_quote);
});
macro_rules! recurse_into_wrapper {
($($wrapper:ident),+ $(,)?) => {
$(
if let Some(wrapper) = msg.$wrapper.as_option_mut()
&& let Some(inner) = wrapper.message.as_option_mut()
{
strip_nested_context_info(inner, always_clear_quote);
}
)+
};
}
recurse_into_wrapper!(
ephemeral_message,
view_once_message,
view_once_message_v2,
view_once_message_v2_extension,
document_with_caption_message,
edited_message,
);
if let Some(wrapper) = msg.device_sent_message.as_option_mut()
&& let Some(inner) = wrapper.message.as_option_mut()
{
strip_nested_context_info(inner, always_clear_quote);
}
}
pub fn merge_dsm_context(
inner: Option<wa::MessageContextInfo>,
outer: Option<&wa::MessageContextInfo>,
) -> Option<wa::MessageContextInfo> {
match (inner, outer) {
(None, None) => None,
(Some(mut inner), None) => {
inner.limit_sharing_v2 = Default::default();
Some(inner)
}
(None, Some(outer)) => Some(outer.clone()),
(Some(mut inner), Some(outer)) => {
if inner.message_secret.is_none() {
inner.message_secret = outer.message_secret.clone();
}
if inner.message_association.is_unset() {
inner.message_association = outer.message_association.clone();
}
inner.limit_sharing_v2 = outer.limit_sharing_v2.clone();
if inner.thread_id.is_empty() {
inner.thread_id = outer.thread_id.clone();
}
if inner.bot_metadata.is_unset() {
inner.bot_metadata = outer.bot_metadata.clone();
}
Some(inner)
}
}
}
pub fn build_quote_context(
message_id: impl Into<String>,
sender_jid: impl Into<String>,
quoted_message: &wa::Message,
) -> wa::ContextInfo {
wa::ContextInfo {
stanza_id: Some(message_id.into()),
participant: Some(sender_jid.into()),
quoted_message: buffa::MessageField::from_box(quoted_message.prepare_for_quote()),
..Default::default()
}
}
pub fn build_quote_context_with_info(
message_id: impl Into<String>,
sender_jid: &Jid,
quoted_chat_jid: &Jid,
target_chat_jid: &Jid,
quoted_message: &wa::Message,
) -> wa::ContextInfo {
let remote_jid = (!quoted_chat_jid.is_same_chat_as(target_chat_jid))
.then(|| quoted_chat_jid.with_device(0).to_string());
let participant = if quoted_chat_jid.is_newsletter() {
quoted_chat_jid.to_string()
} else {
sender_jid.to_string()
};
wa::ContextInfo {
stanza_id: Some(message_id.into()),
participant: Some(participant),
remote_jid,
quoted_message: buffa::MessageField::from_box(quoted_message.prepare_for_quote()),
..Default::default()
}
}
pub fn build_reaction_message(
key: wa::MessageKey,
emoji: impl Into<String>,
sender_timestamp_ms: i64,
) -> wa::Message {
wa::Message {
reaction_message: buffa::MessageField::some(wa::message::ReactionMessage {
key: buffa::MessageField::some(key),
text: Some(emoji.into()),
sender_timestamp_ms: Some(sender_timestamp_ms),
..Default::default()
}),
..Default::default()
}
}
pub fn wrap_as_album_child(
mut inner_message: wa::Message,
parent_key: wa::MessageKey,
) -> wa::Message {
let existing_context = inner_message.message_context_info.take();
let association = wa::MessageAssociation {
association_type: Some(wa::message_association::AssociationType::MEDIA_ALBUM),
parent_message_key: buffa::MessageField::some(parent_key),
..Default::default()
};
let mut outer_context = existing_context.unwrap_or_default();
outer_context.message_association = buffa::MessageField::some(association);
wa::Message {
associated_child_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(inner_message),
}),
message_context_info: buffa::MessageField::some(outer_context),
..Default::default()
}
}
pub trait ConversationExt {
fn subject(&self) -> Option<&str>;
fn participant_jids(&self) -> Vec<Jid>;
fn admin_jids(&self) -> Vec<Jid>;
fn is_locked(&self) -> bool;
fn is_announce_only(&self) -> bool;
}
impl ConversationExt for wa::Conversation {
fn subject(&self) -> Option<&str> {
self.name.as_deref()
}
fn participant_jids(&self) -> Vec<Jid> {
self.participant
.iter()
.filter_map(|p| Jid::from_str(&p.user_jid).ok())
.collect()
}
fn admin_jids(&self) -> Vec<Jid> {
use wa::group_participant::Rank;
self.participant
.iter()
.filter(|p| matches!(p.rank, Some(Rank::ADMIN) | Some(Rank::SUPERADMIN)))
.filter_map(|p| Jid::from_str(&p.user_jid).ok())
.collect()
}
fn is_locked(&self) -> bool {
self.locked.unwrap_or(false)
}
fn is_announce_only(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_message_with_mentions() -> wa::Message {
wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("Hello @user1 @user2".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec![
"111111@s.whatsapp.net".to_string(),
"222222@s.whatsapp.net".to_string(),
],
group_mentions: vec![wa::GroupMention {
group_jid: Some("120363012345@g.us".to_string()),
group_subject: Some("Test Group".to_string()),
}],
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
}
#[test]
fn test_prepare_for_quote_strips_mentions_preserves_content() {
use wa::message::extended_text_message::{FontType, PreviewType};
let original = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("Hello @user1 @user2".to_string()),
matched_text: Some("https://example.com".to_string()),
description: Some("Example description".to_string()),
title: Some("Example Title".to_string()),
text_argb: Some(0xFFFFFF),
background_argb: Some(0x000000),
font: Some(FontType::SYSTEM_BOLD),
preview_type: Some(PreviewType::VIDEO),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec![
"111111@s.whatsapp.net".to_string(),
"222222@s.whatsapp.net".to_string(),
],
group_mentions: vec![wa::GroupMention {
group_jid: Some("120363012345@g.us".to_string()),
group_subject: Some("Test Group".to_string()),
}],
is_forwarded: Some(true),
forwarding_score: Some(5),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let ext = original.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert_eq!(ctx.mentioned_jid.len(), 2);
assert_eq!(ctx.group_mentions.len(), 1);
let prepared = original.prepare_for_quote();
let ext = prepared.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"mentioned_jid should be empty after prepare_for_quote"
);
assert!(
ctx.group_mentions.is_empty(),
"group_mentions should be empty after prepare_for_quote"
);
assert!(
ctx.quoted_message.is_unset(),
"quoted_message should be None after prepare_for_quote"
);
assert!(
ctx.stanza_id.is_none(),
"stanza_id should be None after prepare_for_quote"
);
assert!(
ctx.participant.is_none(),
"participant should be None after prepare_for_quote"
);
assert!(
ctx.remote_jid.is_none(),
"remote_jid should be None after prepare_for_quote"
);
assert_eq!(ext.text.as_deref(), Some("Hello @user1 @user2"));
assert_eq!(ext.matched_text.as_deref(), Some("https://example.com"));
assert_eq!(ext.description.as_deref(), Some("Example description"));
assert_eq!(ext.title.as_deref(), Some("Example Title"));
assert_eq!(ext.text_argb, Some(0xFFFFFF));
assert_eq!(ext.background_argb, Some(0x000000));
assert_eq!(ext.font, Some(FontType::SYSTEM_BOLD));
assert_eq!(ext.preview_type, Some(PreviewType::VIDEO));
assert_eq!(ctx.is_forwarded, Some(true));
assert_eq!(ctx.forwarding_score, Some(5));
}
#[test]
fn test_prepare_for_quote_preserves_media_fields() {
let original = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
url: Some("https://mmg.whatsapp.net/...".to_string()),
mimetype: Some("image/jpeg".to_string()),
caption: Some("Check out this image!".to_string()),
file_sha256: Some(vec![1, 2, 3, 4]),
file_length: Some(12345),
height: Some(1080),
width: Some(1920),
media_key: Some(vec![5, 6, 7, 8]),
direct_path: Some("/v/t62.1234-5/...".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["someone@s.whatsapp.net".to_string()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let prepared = original.prepare_for_quote();
let img = prepared.image_message.as_option().unwrap();
let ctx = img.context_info.as_option().unwrap();
assert!(ctx.mentioned_jid.is_empty());
assert_eq!(img.url.as_deref(), Some("https://mmg.whatsapp.net/..."));
assert_eq!(img.mimetype.as_deref(), Some("image/jpeg"));
assert_eq!(img.caption.as_deref(), Some("Check out this image!"));
assert_eq!(img.file_sha256, Some(vec![1, 2, 3, 4]));
assert_eq!(img.file_length, Some(12345));
assert_eq!(img.height, Some(1080));
assert_eq!(img.width, Some(1920));
assert_eq!(img.media_key, Some(vec![5, 6, 7, 8]));
assert_eq!(img.direct_path.as_deref(), Some("/v/t62.1234-5/..."));
}
#[test]
fn test_prepare_for_quote_breaks_quote_chain() {
let original = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("This is a reply".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
stanza_id: Some("original-msg-id".to_string()),
participant: Some("original-sender@s.whatsapp.net".to_string()),
remote_jid: Some("chat@s.whatsapp.net".to_string()),
quoted_message: buffa::MessageField::some(wa::Message {
conversation: Some("The original message".to_string()),
..Default::default()
}),
mentioned_jid: vec!["user@s.whatsapp.net".to_string()],
is_forwarded: Some(true),
forwarding_score: Some(3),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let prepared = original.prepare_for_quote();
let ext = prepared.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert!(
ctx.quoted_message.is_unset(),
"quoted_message should be None (quote chain broken)"
);
assert!(
ctx.stanza_id.is_none(),
"stanza_id should be None (quote chain broken)"
);
assert!(
ctx.participant.is_none(),
"participant should be None (quote chain broken)"
);
assert!(
ctx.remote_jid.is_none(),
"remote_jid should be None (quote chain broken)"
);
assert!(
ctx.mentioned_jid.is_empty(),
"mentioned_jid should be empty"
);
assert_eq!(
ctx.is_forwarded,
Some(true),
"is_forwarded should be preserved"
);
assert_eq!(
ctx.forwarding_score,
Some(3),
"forwarding_score should be preserved"
);
assert_eq!(ext.text.as_deref(), Some("This is a reply"));
}
#[test]
fn test_set_context_info_extended_text() {
let mut msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("Reply text".to_string()),
..Default::default()
}),
..Default::default()
};
let context = wa::ContextInfo {
stanza_id: Some("test-id".to_string()),
participant: Some("sender@s.whatsapp.net".to_string()),
..Default::default()
};
assert!(msg.set_context_info(context));
let ext = msg.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert_eq!(ctx.stanza_id.as_deref(), Some("test-id"));
assert_eq!(ctx.participant.as_deref(), Some("sender@s.whatsapp.net"));
}
#[test]
fn test_set_context_info_image() {
let mut msg = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
caption: Some("Image caption".to_string()),
..Default::default()
}),
..Default::default()
};
let context = wa::ContextInfo {
stanza_id: Some("img-id".to_string()),
..Default::default()
};
assert!(msg.set_context_info(context));
let img = msg.image_message.as_option().unwrap();
assert!(img.context_info.is_set());
assert_eq!(
img.context_info.as_option().unwrap().stanza_id.as_deref(),
Some("img-id")
);
}
#[test]
fn test_set_context_info_location() {
let mut msg = wa::Message {
location_message: buffa::MessageField::some(wa::message::LocationMessage {
degrees_latitude: Some(40.7128),
degrees_longitude: Some(-74.0060),
name: Some("New York".to_string()),
..Default::default()
}),
..Default::default()
};
let context = wa::ContextInfo {
stanza_id: Some("loc-id".to_string()),
..Default::default()
};
assert!(msg.set_context_info(context));
let loc = msg.location_message.as_option().unwrap();
assert!(loc.context_info.is_set());
}
#[test]
fn test_set_context_info_promotes_bare_conversation() {
let mut msg = wa::Message {
conversation: Some("Simple text".to_string()),
..Default::default()
};
let context = wa::ContextInfo {
stanza_id: Some("test-id".to_string()),
..Default::default()
};
assert!(msg.set_context_info(context));
assert!(msg.conversation.is_none(), "conversation must be moved out");
let ext = msg
.extended_text_message
.as_option()
.expect("promoted to extended_text_message");
assert_eq!(ext.text.as_deref(), Some("Simple text"));
assert_eq!(
ext.context_info
.as_option()
.and_then(|c| c.stanza_id.as_deref()),
Some("test-id")
);
}
#[test]
fn test_set_context_info_returns_false_on_empty_message() {
let mut msg = wa::Message::default();
let context = wa::ContextInfo {
stanza_id: Some("test-id".to_string()),
..Default::default()
};
assert!(!msg.set_context_info(context));
assert!(msg.conversation.is_none());
assert!(msg.extended_text_message.is_unset());
}
#[test]
fn test_build_quote_context() {
let original = create_message_with_mentions();
let context = build_quote_context("3EB0123456789", "1234567890@s.whatsapp.net", &original);
assert_eq!(context.stanza_id.as_deref(), Some("3EB0123456789"));
assert_eq!(
context.participant.as_deref(),
Some("1234567890@s.whatsapp.net")
);
let quoted = context.quoted_message.as_option().unwrap();
let ext = quoted.extended_text_message.as_option().unwrap();
let quoted_ctx = ext.context_info.as_option().unwrap();
assert!(
quoted_ctx.mentioned_jid.is_empty(),
"Quoted message mentions should be stripped"
);
}
#[test]
fn test_prepare_for_quote_ephemeral() {
let ephemeral_msg = wa::Message {
ephemeral_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(create_message_with_mentions()),
}),
..Default::default()
};
let prepared = ephemeral_msg.prepare_for_quote();
let inner = prepared
.ephemeral_message
.as_option()
.unwrap()
.message
.as_option()
.unwrap();
let ext = inner.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"Mentions inside ephemeral wrapper should be stripped"
);
}
#[test]
fn test_prepare_for_quote_view_once() {
let view_once_msg = wa::Message {
view_once_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["someone@s.whatsapp.net".to_string()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
}),
..Default::default()
};
let prepared = view_once_msg.prepare_for_quote();
let inner = prepared
.view_once_message
.as_option()
.unwrap()
.message
.as_option()
.unwrap();
let img = inner.image_message.as_option().unwrap();
let ctx = img.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"Mentions inside view_once wrapper should be stripped"
);
}
#[test]
fn test_prepare_for_quote_device_sent_message() {
let device_sent_msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("1234567890@s.whatsapp.net".to_string()),
message: buffa::MessageField::some(wa::Message {
extended_text_message: buffa::MessageField::some(
wa::message::ExtendedTextMessage {
text: Some("Message from other device".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec![
"user1@s.whatsapp.net".to_string(),
"user2@s.whatsapp.net".to_string(),
],
group_mentions: vec![wa::GroupMention {
group_jid: Some("group@g.us".to_string()),
group_subject: Some("Group Name".to_string()),
}],
..Default::default()
}),
..Default::default()
},
),
..Default::default()
}),
phash: Some("somephash".to_string()),
}),
..Default::default()
};
let prepared = device_sent_msg.prepare_for_quote();
let wrapper = prepared.device_sent_message.as_option().unwrap();
let inner = wrapper.message.as_option().unwrap();
let ext = inner.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"mentioned_jid inside device_sent_message should be stripped"
);
assert!(
ctx.group_mentions.is_empty(),
"group_mentions inside device_sent_message should be stripped"
);
assert_eq!(ext.text.as_deref(), Some("Message from other device"));
assert_eq!(
wrapper.destination_jid.as_deref(),
Some("1234567890@s.whatsapp.net")
);
assert_eq!(wrapper.phash.as_deref(), Some("somephash"));
}
#[test]
fn test_prepare_for_quote_edited_message() {
let edited_msg = wa::Message {
edited_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message {
extended_text_message: buffa::MessageField::some(
wa::message::ExtendedTextMessage {
text: Some("Edited message text".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["mentioned@s.whatsapp.net".to_string()],
group_mentions: vec![wa::GroupMention {
group_jid: Some("editedgroup@g.us".to_string()),
group_subject: Some("Edited Group".to_string()),
}],
..Default::default()
}),
..Default::default()
},
),
..Default::default()
}),
}),
..Default::default()
};
let prepared = edited_msg.prepare_for_quote();
let inner = prepared
.edited_message
.as_option()
.unwrap()
.message
.as_option()
.unwrap();
let ext = inner.extended_text_message.as_option().unwrap();
let ctx = ext.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"mentioned_jid inside edited_message should be stripped"
);
assert!(
ctx.group_mentions.is_empty(),
"group_mentions inside edited_message should be stripped"
);
assert_eq!(ext.text.as_deref(), Some("Edited message text"));
}
#[test]
fn test_prepare_for_quote_nested_wrappers() {
let nested_wrapper_msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("dest@s.whatsapp.net".to_string()),
message: buffa::MessageField::some(wa::Message {
ephemeral_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
caption: Some("Nested image".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["deep@s.whatsapp.net".to_string()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let prepared = nested_wrapper_msg.prepare_for_quote();
let device_sent = prepared.device_sent_message.as_option().unwrap();
let device_inner = device_sent.message.as_option().unwrap();
let ephemeral = device_inner.ephemeral_message.as_option().unwrap();
let ephemeral_inner = ephemeral.message.as_option().unwrap();
let img = ephemeral_inner.image_message.as_option().unwrap();
let ctx = img.context_info.as_option().unwrap();
assert!(
ctx.mentioned_jid.is_empty(),
"Mentions in deeply nested wrappers should be stripped"
);
assert_eq!(img.caption.as_deref(), Some("Nested image"));
}
#[test]
fn test_set_context_info_various_types() {
let test_cases: Vec<wa::Message> = vec![
wa::Message {
video_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
wa::Message {
audio_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
wa::Message {
document_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
wa::Message {
sticker_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
wa::Message {
contact_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
wa::Message {
poll_creation_message: buffa::MessageField::some(Default::default()),
..Default::default()
},
];
for mut msg in test_cases {
let context = wa::ContextInfo {
stanza_id: Some("test".to_string()),
..Default::default()
};
assert!(
msg.set_context_info(context),
"set_context_info should succeed for this message type"
);
}
}
#[test]
fn test_prepare_for_quote_preserves_bot_quote_chain() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("Bot reply".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
participant: Some("131355512345@s.whatsapp.net".to_string()),
stanza_id: Some("bot-msg-id".to_string()),
remote_jid: Some("chat@g.us".to_string()),
quoted_message: buffa::MessageField::some(wa::Message {
conversation: Some("Original user message".to_string()),
..Default::default()
}),
mentioned_jid: vec!["user@s.whatsapp.net".to_string()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let prepared = msg.prepare_for_quote();
let ctx = prepared
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert!(
ctx.quoted_message.is_set(),
"Bot quote chain should be preserved"
);
assert!(ctx.stanza_id.is_some(), "Bot stanza_id should be preserved");
assert!(
ctx.participant.is_some(),
"Bot participant should be preserved"
);
assert!(
ctx.remote_jid.is_some(),
"Bot remote_jid should be preserved"
);
assert!(
ctx.mentioned_jid.is_empty(),
"Mentions should still be cleared even for bots"
);
}
#[test]
fn test_prepare_for_quote_preserves_bot_server_quote_chain() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("Bot reply".to_string()),
context_info: buffa::MessageField::some(wa::ContextInfo {
participant: Some("mybot@bot".to_string()),
stanza_id: Some("bot-msg-id".to_string()),
quoted_message: buffa::MessageField::some(wa::Message {
conversation: Some("Original".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let prepared = msg.prepare_for_quote();
let ctx = prepared
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert!(
ctx.quoted_message.is_set(),
"Bot (@bot server) quote chain should be preserved"
);
}
#[test]
fn test_build_quote_context_newsletter() {
let sender: Jid = "123456@s.whatsapp.net".parse().unwrap();
let chat: Jid = "1234567890@newsletter".parse().unwrap();
let msg = wa::Message::default();
let ctx = build_quote_context_with_info("msg-id", &sender, &chat, &chat, &msg);
assert_eq!(
ctx.participant.as_deref(),
Some("1234567890@newsletter"),
"Newsletter participant should be the newsletter JID"
);
assert_eq!(ctx.stanza_id.as_deref(), Some("msg-id"));
}
#[test]
fn test_build_quote_context_normal_message() {
let sender: Jid = "123456@s.whatsapp.net".parse().unwrap();
let chat: Jid = "group@g.us".parse().unwrap();
let msg = wa::Message::default();
let ctx = build_quote_context_with_info("msg-id", &sender, &chat, &chat, &msg);
assert_eq!(
ctx.participant.as_deref(),
Some("123456@s.whatsapp.net"),
"Normal message participant should be the sender JID"
);
}
#[test]
fn test_build_quote_context_status_broadcast() {
let sender: Jid = "123456@s.whatsapp.net".parse().unwrap();
let chat: Jid = "status@broadcast".parse().unwrap();
let msg = wa::Message::default();
let ctx = build_quote_context_with_info("msg-id", &sender, &chat, &chat, &msg);
assert_eq!(
ctx.participant.as_deref(),
Some("123456@s.whatsapp.net"),
"Status broadcast participant should fall back to sender"
);
}
#[test]
fn test_into_base_message_unwraps_device_sent_reaction() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("5511999999999@s.whatsapp.net".to_string()),
message: buffa::MessageField::some(wa::Message {
reaction_message: buffa::MessageField::some(wa::message::ReactionMessage {
text: Some("\u{2764}".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let unwrapped = msg.into_base_message();
assert!(
unwrapped.device_sent_message.is_unset(),
"device_sent_message wrapper should be removed"
);
assert!(
unwrapped.reaction_message.is_set(),
"reaction_message should be accessible after unwrapping"
);
assert_eq!(
unwrapped
.reaction_message
.as_option()
.unwrap()
.text
.as_deref(),
Some("\u{2764}")
);
}
#[test]
fn test_into_base_message_unwraps_nested_dsm_ephemeral() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("5511999999999@s.whatsapp.net".to_string()),
message: buffa::MessageField::some(wa::Message {
ephemeral_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message {
conversation: Some("secret".to_string()),
..Default::default()
}),
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let unwrapped = msg.into_base_message();
assert_eq!(
unwrapped.conversation.as_deref(),
Some("secret"),
"should unwrap through DSM then ephemeral to reach conversation"
);
}
#[test]
fn test_into_base_message_passthrough_plain() {
let msg = wa::Message {
conversation: Some("hello".to_string()),
..Default::default()
};
let unwrapped = msg.into_base_message();
assert_eq!(unwrapped.conversation.as_deref(), Some("hello"));
}
#[test]
fn test_into_base_message_empty_dsm() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("5511999999999@s.whatsapp.net".to_string()),
..Default::default()
}),
..Default::default()
};
let unwrapped = msg.into_base_message();
assert!(
unwrapped.device_sent_message.is_set(),
"empty DSM wrapper should be preserved"
);
assert!(unwrapped.conversation.is_none());
}
#[test]
fn test_merge_dsm_context_both_none() {
assert!(merge_dsm_context(None, None).is_none());
}
#[test]
fn test_merge_dsm_context_inner_only() {
let inner = wa::MessageContextInfo {
message_secret: Some(vec![1, 2, 3]),
..Default::default()
};
let result = merge_dsm_context(Some(inner.clone()), None).unwrap();
assert_eq!(result.message_secret, Some(vec![1, 2, 3]));
}
#[test]
fn test_merge_dsm_context_outer_only() {
let outer = wa::MessageContextInfo {
message_secret: Some(vec![4, 5, 6]),
limit_sharing_v2: buffa::MessageField::some(wa::LimitSharing {
sharing_limited: Some(true),
limit_sharing_setting_timestamp: Some(12345),
..Default::default()
}),
..Default::default()
};
let result = merge_dsm_context(None, Some(&outer)).unwrap();
assert_eq!(
result.message_secret,
Some(vec![4, 5, 6]),
"message_secret should come from outer when inner is None"
);
let ls = result
.limit_sharing_v2
.as_option()
.expect("limit_sharing_v2 should come from outer");
assert_eq!(ls.sharing_limited, Some(true));
assert_eq!(ls.limit_sharing_setting_timestamp, Some(12345));
}
#[test]
fn test_merge_dsm_context_outer_only_preserves_non_subset_fields() {
let outer = wa::MessageContextInfo {
message_add_on_duration_in_secs: Some(86400),
..Default::default()
};
let result = merge_dsm_context(None, Some(&outer)).unwrap();
assert_eq!(
result.message_add_on_duration_in_secs,
Some(86400),
"hoisted fields outside the merge subset must survive unwrap"
);
}
#[test]
fn test_merge_dsm_context_inner_preferred_for_secret() {
let inner = wa::MessageContextInfo {
message_secret: Some(vec![1, 2, 3]),
..Default::default()
};
let outer = wa::MessageContextInfo {
message_secret: Some(vec![4, 5, 6]),
..Default::default()
};
let result = merge_dsm_context(Some(inner), Some(&outer)).unwrap();
assert_eq!(
result.message_secret,
Some(vec![1, 2, 3]),
"inner message_secret should be preferred over outer"
);
}
#[test]
fn test_merge_dsm_context_secret_fallback_to_outer() {
let inner = wa::MessageContextInfo {
message_secret: None,
..Default::default()
};
let outer = wa::MessageContextInfo {
message_secret: Some(vec![4, 5, 6]),
..Default::default()
};
let result = merge_dsm_context(Some(inner), Some(&outer)).unwrap();
assert_eq!(
result.message_secret,
Some(vec![4, 5, 6]),
"should fall back to outer message_secret when inner is None"
);
}
#[test]
fn test_merge_dsm_context_limit_sharing_v2_always_outer() {
let inner_ls = wa::LimitSharing {
..Default::default()
};
let outer_ls = wa::LimitSharing {
..Default::default()
};
let inner = wa::MessageContextInfo {
limit_sharing_v2: buffa::MessageField::some(inner_ls),
..Default::default()
};
let outer = wa::MessageContextInfo {
limit_sharing_v2: buffa::MessageField::some(outer_ls),
..Default::default()
};
let result = merge_dsm_context(Some(inner), Some(&outer)).unwrap();
assert!(
result.limit_sharing_v2.is_set(),
"limit_sharing_v2 should always come from outer"
);
let inner_with_ls = wa::MessageContextInfo {
limit_sharing_v2: buffa::MessageField::some(wa::LimitSharing::default()),
..Default::default()
};
let result = merge_dsm_context(Some(inner_with_ls), None).unwrap();
assert!(
result.limit_sharing_v2.is_unset(),
"limit_sharing_v2 should be cleared when outer is None"
);
}
#[test]
fn test_merge_dsm_context_thread_id_fallback() {
let outer = wa::MessageContextInfo {
thread_id: vec![wa::ThreadID::default()],
..Default::default()
};
let inner_empty = wa::MessageContextInfo::default();
let result = merge_dsm_context(Some(inner_empty), Some(&outer)).unwrap();
assert_eq!(
result.thread_id.len(),
1,
"should fall back to outer thread_id when inner is empty"
);
let inner_filled = wa::MessageContextInfo {
thread_id: vec![wa::ThreadID::default(), wa::ThreadID::default()],
..Default::default()
};
let result = merge_dsm_context(Some(inner_filled), Some(&outer)).unwrap();
assert_eq!(
result.thread_id.len(),
2,
"should keep inner thread_id when non-empty"
);
}
#[test]
fn quote_context_omits_remote_jid_same_chat_group() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let group: Jid = "120363098765432100@g.us".parse().unwrap();
let msg = wa::Message {
conversation: Some("hello".into()),
..Default::default()
};
let ctx = build_quote_context_with_info("msg-id-123", &sender, &group, &group, &msg);
assert_eq!(ctx.stanza_id.as_deref(), Some("msg-id-123"));
assert_eq!(
ctx.participant.as_deref(),
Some("551199887766@s.whatsapp.net")
);
assert_eq!(ctx.remote_jid, None);
assert!(ctx.quoted_message.is_set());
assert!(ctx.mentioned_jid.is_empty());
}
#[test]
fn quote_context_omits_remote_jid_same_chat_dm() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let chat: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let msg = wa::Message {
conversation: Some("ping".into()),
..Default::default()
};
let ctx = build_quote_context_with_info("msg-id-456", &sender, &chat, &chat, &msg);
assert_eq!(ctx.remote_jid, None);
assert_eq!(
ctx.participant.as_deref(),
Some("551199887766@s.whatsapp.net")
);
}
#[test]
fn quote_context_emits_remote_jid_cross_chat() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let quoted_chat: Jid = "120363000000000001@g.us".parse().unwrap();
let target_chat: Jid = "120363000000000002@g.us".parse().unwrap();
let msg = wa::Message {
conversation: Some("cross".into()),
..Default::default()
};
let ctx =
build_quote_context_with_info("msg-id-x", &sender, "ed_chat, &target_chat, &msg);
assert_eq!(ctx.remote_jid.as_deref(), Some("120363000000000001@g.us"));
}
#[test]
fn quote_context_status_reply_is_cross_chat() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let status: Jid = "status@broadcast".parse().unwrap();
let target: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let msg = wa::Message::default();
let ctx = build_quote_context_with_info("msg-id-s", &sender, &status, &target, &msg);
assert_eq!(ctx.remote_jid.as_deref(), Some("status@broadcast"));
}
#[test]
fn quote_context_device_suffix_treated_as_same_chat() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let quoted_chat: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let target_chat = quoted_chat.with_device(5);
let msg = wa::Message::default();
let ctx =
build_quote_context_with_info("msg-id-d", &sender, "ed_chat, &target_chat, &msg);
assert_eq!(ctx.remote_jid, None);
}
#[test]
fn quote_context_cross_chat_remote_jid_drops_device_suffix() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let quoted_chat = sender.with_device(5);
let target_chat: Jid = "5521988776655@s.whatsapp.net".parse().unwrap();
let msg = wa::Message::default();
let ctx =
build_quote_context_with_info("msg-id-dev", &sender, "ed_chat, &target_chat, &msg);
assert_eq!(
ctx.remote_jid.as_deref(),
Some("551199887766@s.whatsapp.net")
);
}
#[test]
fn quote_context_newsletter_uses_channel_as_participant() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let newsletter: Jid = "120363099999999999@newsletter".parse().unwrap();
let msg = wa::Message::default();
let ctx =
build_quote_context_with_info("msg-id-789", &sender, &newsletter, &newsletter, &msg);
assert_eq!(
ctx.participant.as_deref(),
Some("120363099999999999@newsletter")
);
assert_eq!(ctx.remote_jid, None);
}
#[test]
fn quote_context_newsletter_cross_chat_sets_both() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let quoted: Jid = "120363099999999999@newsletter".parse().unwrap();
let target: Jid = "120363011111111111@newsletter".parse().unwrap();
let msg = wa::Message::default();
let ctx = build_quote_context_with_info("msg-id-nx", &sender, "ed, &target, &msg);
assert_eq!(
ctx.participant.as_deref(),
Some("120363099999999999@newsletter")
);
assert_eq!(
ctx.remote_jid.as_deref(),
Some("120363099999999999@newsletter")
);
}
#[test]
fn quote_context_strips_mentions_from_quoted_message() {
let sender: Jid = "551199887766@s.whatsapp.net".parse().unwrap();
let group: Jid = "120363098765432100@g.us".parse().unwrap();
let msg = create_message_with_mentions();
let ctx = build_quote_context_with_info("msg-id", &sender, &group, &group, &msg);
let quoted = ctx.quoted_message.into_option().unwrap();
let inner_ctx = quoted
.extended_text_message
.into_option()
.unwrap()
.context_info
.into_option()
.unwrap();
assert!(inner_ctx.mentioned_jid.is_empty());
assert!(inner_ctx.group_mentions.is_empty());
assert!(ctx.mentioned_jid.is_empty());
}
fn sample_parent_key() -> wa::MessageKey {
wa::MessageKey {
remote_jid: Some("5511999999999@s.whatsapp.net".to_string()),
from_me: Some(true),
id: Some("PARENT_MSG_ID".to_string()),
..Default::default()
}
}
#[test]
fn test_wrap_as_album_child_basic() {
let inner = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
url: Some("https://mmg.whatsapp.net/test".to_string()),
..Default::default()
}),
..Default::default()
};
let wrapped = wrap_as_album_child(inner, sample_parent_key());
let future_proof = wrapped.associated_child_message.as_option().unwrap();
let inner_msg = future_proof.message.as_option().unwrap();
assert!(inner_msg.image_message.is_set());
assert!(inner_msg.message_context_info.is_unset());
let ctx = wrapped.message_context_info.as_option().unwrap();
let assoc = ctx.message_association.as_option().unwrap();
assert_eq!(
assoc.association_type,
Some(wa::message_association::AssociationType::MEDIA_ALBUM)
);
assert_eq!(
assoc.parent_message_key.as_option(),
Some(&sample_parent_key())
);
assert_eq!(assoc.message_index, None);
}
#[test]
fn test_wrap_as_album_child_lifts_existing_context() {
let secret = vec![1u8; 32];
let inner = wa::Message {
video_message: buffa::MessageField::some(wa::message::VideoMessage {
url: Some("https://mmg.whatsapp.net/vid".to_string()),
..Default::default()
}),
message_context_info: buffa::MessageField::some(wa::MessageContextInfo {
message_secret: Some(secret.clone()),
..Default::default()
}),
..Default::default()
};
let wrapped = wrap_as_album_child(inner, sample_parent_key());
let ctx = wrapped.message_context_info.as_option().unwrap();
assert_eq!(ctx.message_secret.as_deref(), Some(secret.as_slice()));
assert!(ctx.message_association.is_set());
}
#[test]
fn is_view_once_detects_legacy_wrapper() {
let msg = wa::Message {
view_once_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message::default()),
}),
..Default::default()
};
assert!(msg.is_view_once());
let msg_v2 = wa::Message {
view_once_message_v2: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message::default()),
}),
..Default::default()
};
assert!(msg_v2.is_view_once());
}
#[test]
fn is_view_once_detects_wrapper_nested_in_device_sent() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
message: buffa::MessageField::some(wa::Message {
view_once_message_v2: buffa::MessageField::some(
wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message::default()),
},
),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_detects_inline_image_flag() {
let msg = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
view_once: Some(true),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_detects_inline_video_flag() {
let msg = wa::Message {
video_message: buffa::MessageField::some(wa::message::VideoMessage {
view_once: Some(true),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_detects_inline_audio_flag() {
let msg = wa::Message {
audio_message: buffa::MessageField::some(wa::message::AudioMessage {
view_once: Some(true),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_detects_inline_extended_text_flag() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
view_once: Some(true),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_detects_inline_flag_through_device_sent() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
message: buffa::MessageField::some(wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
view_once: Some(true),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn is_view_once_false_for_plain_image() {
let msg = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage::default()),
..Default::default()
};
assert!(!msg.is_view_once());
let msg_explicit_false = wa::Message {
image_message: buffa::MessageField::some(wa::message::ImageMessage {
view_once: Some(false),
..Default::default()
}),
..Default::default()
};
assert!(!msg_explicit_false.is_view_once());
}
#[test]
fn is_view_once_false_for_empty_message() {
assert!(!wa::Message::default().is_view_once());
}
#[test]
fn is_view_once_detects_v2_extension_wrapper() {
let msg = wa::Message {
view_once_message_v2_extension: buffa::MessageField::some(
wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message::default()),
},
),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn set_ephemeral_expiration_promotes_bare_conversation_to_extended_text() {
let mut msg = wa::Message {
conversation: Some("hello".to_string()),
..Default::default()
};
assert!(msg.set_ephemeral_expiration(86400));
assert!(msg.conversation.is_none());
let ext = msg.extended_text_message.as_option().unwrap();
assert_eq!(ext.text.as_deref(), Some("hello"));
assert_eq!(
ext.context_info.as_option().and_then(|c| c.expiration),
Some(86400)
);
}
#[test]
fn set_ephemeral_expiration_returns_false_on_empty_message() {
let mut msg = wa::Message::default();
assert!(!msg.set_ephemeral_expiration(60));
assert!(msg.conversation.is_none());
assert!(msg.extended_text_message.is_unset());
}
#[test]
fn is_view_once_detects_ephemeral_device_sent_view_once() {
let msg = wa::Message {
ephemeral_message: buffa::MessageField::some(wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message {
device_sent_message: buffa::MessageField::some(
wa::message::DeviceSentMessage {
message: buffa::MessageField::some(wa::Message {
view_once_message_v2: buffa::MessageField::some(
wa::message::FutureProofMessage {
message: buffa::MessageField::some(wa::Message::default()),
},
),
..Default::default()
}),
..Default::default()
},
),
..Default::default()
}),
}),
..Default::default()
};
assert!(msg.is_view_once());
}
#[test]
fn mentions_any_bot_true_for_bot_jid_in_extended_text() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("@MetaAI hi".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec![
"5511999998888@s.whatsapp.net".into(),
"867051314767696@bot".into(),
],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(msg.mentions_any_bot());
}
#[test]
fn mentions_any_bot_true_for_legacy_pn_form_bot() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("@MetaAI".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["13135550002@s.whatsapp.net".into()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(msg.mentions_any_bot());
}
#[test]
fn mentions_any_bot_false_without_bot_jid() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("hi friends".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec![
"5511999998888@s.whatsapp.net".into(),
"120363021033254949@g.us".into(),
],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(!msg.mentions_any_bot());
}
#[test]
fn mentions_any_bot_false_for_no_context_info() {
let msg = wa::Message {
conversation: Some("plain".into()),
..Default::default()
};
assert!(!msg.mentions_any_bot());
}
#[test]
fn mentions_any_bot_sees_through_device_sent_wrapper() {
let msg = wa::Message {
device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
destination_jid: Some("867051314767696@bot".into()),
message: buffa::MessageField::some(wa::Message {
extended_text_message: buffa::MessageField::some(
wa::message::ExtendedTextMessage {
text: Some("@MetaAI".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
mentioned_jid: vec!["867051314767696@bot".into()],
..Default::default()
}),
..Default::default()
},
),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(
msg.mentions_any_bot(),
"must unwrap DeviceSentMessage before reading context_info"
);
}
#[test]
fn is_forwarded_true_and_false() {
let fwd = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("fwd".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
is_forwarded: Some(true),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(fwd.is_forwarded());
let plain = wa::Message {
conversation: Some("plain".into()),
..Default::default()
};
assert!(!plain.is_forwarded());
let not_fwd = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("hi".into()),
context_info: buffa::MessageField::some(wa::ContextInfo::default()),
..Default::default()
}),
..Default::default()
};
assert!(!not_fwd.is_forwarded());
}
#[test]
fn prepare_for_forward_marks_fresh_conversation() {
let msg = wa::Message {
conversation: Some("hello".into()),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let etm = fwd
.extended_text_message
.as_option()
.expect("conversation promoted to extended_text_message");
assert_eq!(etm.text.as_deref(), Some("hello"));
let ctx = etm.context_info.as_option().expect("context_info present");
assert_eq!(ctx.is_forwarded, Some(true));
assert_eq!(ctx.forwarding_score, Some(0));
}
#[test]
fn prepare_for_forward_bumps_score_when_source_already_forwarded() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("hi".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
is_forwarded: Some(true),
forwarding_score: Some(0),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let ctx = fwd
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert_eq!(ctx.is_forwarded, Some(true));
assert_eq!(ctx.forwarding_score, Some(1));
}
#[test]
fn prepare_for_forward_jumps_to_sentinel_at_threshold() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("hi".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
is_forwarded: Some(true),
forwarding_score: Some(4),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let ctx = fwd
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert_eq!(ctx.forwarding_score, Some(127));
}
#[test]
fn prepare_for_forward_strips_quote_chain() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("reply".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
stanza_id: Some("QUOTED".into()),
participant: Some("123@s.whatsapp.net".into()),
quoted_message: buffa::MessageField::some(wa::Message {
conversation: Some("orig".into()),
..Default::default()
}),
mentioned_jid: vec!["456@s.whatsapp.net".into()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let ctx = fwd
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert!(ctx.stanza_id.is_none());
assert!(ctx.quoted_message.is_unset());
assert!(ctx.participant.is_none());
assert!(ctx.mentioned_jid.is_empty());
assert_eq!(ctx.is_forwarded, Some(true));
}
#[test]
fn prepare_for_forward_clears_bot_quote_chain() {
let msg = wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some("reply to bot".into()),
context_info: buffa::MessageField::some(wa::ContextInfo {
stanza_id: Some("Q".into()),
participant: Some("mybot@bot".into()),
quoted_message: buffa::MessageField::some(wa::Message {
conversation: Some("bot msg".into()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let ctx = fwd
.extended_text_message
.as_option()
.unwrap()
.context_info
.as_option()
.unwrap();
assert!(ctx.quoted_message.is_unset());
assert!(ctx.participant.is_none());
assert!(ctx.stanza_id.is_none());
}
#[test]
fn get_base_message_unwraps_view_once_v2_extension() {
let inner = wa::Message {
conversation: Some("inner".into()),
..Default::default()
};
let wrapped = wa::Message {
view_once_message_v2_extension: buffa::MessageField::some(
wa::message::FutureProofMessage {
message: buffa::MessageField::some(inner),
},
),
..Default::default()
};
assert_eq!(
wrapped.get_base_message().conversation.as_deref(),
Some("inner")
);
}
#[test]
fn build_keep_in_chat_message_keep_for_all() {
let key = wa::MessageKey {
id: Some("MID".into()),
from_me: Some(false),
..Default::default()
};
let msg = build_keep_in_chat_message(key, true, 12345);
let k = msg
.keep_in_chat_message
.as_option()
.expect("keep_in_chat_message set");
assert_eq!(k.keep_type, Some(wa::KeepType::KeepForAll));
assert_eq!(k.timestamp_ms, Some(12345));
assert_eq!(
k.key.as_option().and_then(|key| key.id.as_deref()),
Some("MID")
);
}
#[test]
fn prepare_for_forward_marks_ptv_message() {
let msg = wa::Message {
ptv_message: buffa::MessageField::some(wa::message::VideoMessage::default()),
..Default::default()
};
let fwd = msg.prepare_for_forward();
let ctx = fwd
.ptv_message
.as_option()
.expect("ptv preserved")
.context_info
.as_option()
.expect("context_info attached");
assert_eq!(ctx.is_forwarded, Some(true));
assert_eq!(ctx.forwarding_score, Some(0));
}
#[test]
fn build_keep_in_chat_message_undo() {
let msg = build_keep_in_chat_message(wa::MessageKey::default(), false, 1);
assert_eq!(
msg.keep_in_chat_message.unwrap().keep_type,
Some(wa::KeepType::UndoKeepForAll)
);
}
fn group_target_key() -> wa::MessageKey {
wa::MessageKey {
remote_jid: Some("120363012345@g.us".to_string()),
from_me: Some(false),
id: Some("ABCD1234".to_string()),
participant: Some("15551230000@s.whatsapp.net".to_string()),
}
}
#[test]
fn build_reaction_populates_key_text_and_timestamp() {
let key = group_target_key();
let ts = 1_700_000_000_000;
let msg = build_reaction_message(key.clone(), "👍", ts);
let react = msg
.reaction_message
.as_option()
.expect("reaction_message must be set");
assert_eq!(react.key.as_option(), Some(&key));
assert_eq!(react.text.as_deref(), Some("👍"));
assert_eq!(react.sender_timestamp_ms, Some(ts));
assert!(msg.conversation.is_none());
assert!(react.grouping_key.is_none());
}
#[test]
fn build_reaction_preserves_participant_for_group_target() {
let key = group_target_key();
let msg = build_reaction_message(key, "❤️", 1);
let participant = msg
.reaction_message
.into_option()
.and_then(|r| r.key.into_option())
.and_then(|k| k.participant);
assert_eq!(participant.as_deref(), Some("15551230000@s.whatsapp.net"));
}
#[test]
fn build_reaction_empty_emoji_is_unreact_form() {
let msg = build_reaction_message(group_target_key(), "", 1);
let text = msg
.reaction_message
.as_option()
.and_then(|r| r.text.as_deref());
assert_eq!(text, Some(""));
}
#[test]
fn build_reaction_edit_attr_classification() {
use crate::types::message::EditAttribute;
let react = build_reaction_message(group_target_key(), "🔥", 1);
assert_eq!(EditAttribute::infer_from_message(&react), None);
let unreact = build_reaction_message(group_target_key(), "", 1);
assert_eq!(
EditAttribute::infer_from_message(&unreact),
Some(EditAttribute::SenderRevoke)
);
}
}