Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions src/features/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub use wacore::iq::groups::{
ParticipantChangeResponse,
};

#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GroupMetadata {
pub id: Jid,
pub subject: String,
Expand All @@ -36,12 +36,18 @@ pub struct GroupMetadata {
pub description: Option<String>,
/// Description ID (for conflict detection when updating).
pub description_id: Option<String>,
/// JID of the participant who set the description.
pub description_owner: Option<Jid>,
/// Timestamp when the description was set.
pub description_time: Option<u64>,
/// Whether the group is locked (only admins can edit group info).
pub is_locked: bool,
/// Whether announcement mode is enabled (only admins can send messages).
pub is_announcement: bool,
/// Ephemeral message expiration in seconds (0 = disabled).
pub ephemeral_expiration: u32,
/// Disappearing mode trigger (from `trigger` attribute on `<ephemeral>`).
pub ephemeral_trigger: Option<u32>,
/// Whether membership approval is required to join.
pub membership_approval: bool,
/// Who can add members to the group.
Expand Down Expand Up @@ -92,9 +98,12 @@ impl From<GroupInfoResponse> for GroupMetadata {
subject_owner: group.subject_owner,
description: group.description,
description_id: group.description_id,
description_owner: group.description_owner,
description_time: group.description_time,
is_locked: group.is_locked,
is_announcement: group.is_announcement,
ephemeral_expiration: group.ephemeral_expiration,
ephemeral_trigger: group.ephemeral_trigger,
membership_approval: group.membership_approval,
member_add_mode: group.member_add_mode,
member_link_mode: group.member_link_mode,
Expand Down Expand Up @@ -608,25 +617,7 @@ mod tests {
phone_number: None,
is_admin: true,
}],
addressing_mode: AddressingMode::Pn,
creator: None,
creation_time: None,
subject_time: None,
subject_owner: None,
description: None,
description_id: None,
is_locked: false,
is_announcement: false,
ephemeral_expiration: 0,
membership_approval: false,
member_add_mode: None,
member_link_mode: None,
size: None,
is_parent_group: false,
parent_group_jid: None,
is_default_sub_group: false,
is_general_chat: false,
allow_non_admin_sub_group_creation: false,
..Default::default()
};

assert_eq!(metadata.subject, "Test Group");
Expand Down
9 changes: 9 additions & 0 deletions wacore/binary/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,15 @@ impl Node {
self.children()
.and_then(|nodes| nodes.iter().find(|node| node.tag == tag))
}

/// Extract text content, handling both String and Bytes (lossy UTF-8).
pub fn content_as_string(&self) -> Option<String> {
match &self.content {
Some(NodeContent::String(s)) => Some(s.clone()),
Some(NodeContent::Bytes(b)) => Some(String::from_utf8_lossy(b).into_owned()),
_ => None,
}
}
}

impl<'a> NodeRef<'a> {
Expand Down
14 changes: 8 additions & 6 deletions wacore/src/iq/business.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,8 @@ pub struct BusinessHours {
pub struct BusinessHoursConfig {
pub day_of_week: DayOfWeek,
pub mode: BusinessHourMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_time: Option<String>,
pub open_time: u32,
pub close_time: u32,
}

#[derive(Debug, Clone, serde::Serialize)]
Expand Down Expand Up @@ -181,8 +179,12 @@ impl IqSpec for BusinessProfileSpec {
Some(BusinessHoursConfig {
day_of_week: DayOfWeek::from(day.as_ref()),
mode: BusinessHourMode::from(mode_str.as_ref()),
open_time: optional_attr(c, "open_time").map(|s| s.into_owned()),
close_time: optional_attr(c, "close_time").map(|s| s.into_owned()),
open_time: optional_attr(c, "open_time")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0),
Comment on lines +182 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve absent business-hour times instead of defaulting to zero

In BusinessProfileSpec::parse_response, missing or non-numeric open_time values are coerced to 0 via unwrap_or(0), and the struct now stores open_time/close_time as required u32. For modes like open_24h or appointment_only where these attributes may be omitted, this turns “not provided” into a real midnight value, so downstream consumers cannot distinguish absent data from an actual 00:00 schedule and will serialize misleading times.

Useful? React with 👍 / 👎.

close_time: optional_attr(c, "close_time")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0),
})
})
.collect();
Expand Down
100 changes: 84 additions & 16 deletions wacore/src/iq/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,18 @@ pub struct GroupInfoResponse {
pub description: Option<String>,
/// Description ID (for conflict detection when updating).
pub description_id: Option<String>,
/// JID of the participant who set the description.
pub description_owner: Option<Jid>,
/// Timestamp when the description was set.
pub description_time: Option<u64>,
/// Whether the group is locked (only admins can edit group info).
pub is_locked: bool,
/// Whether announcement mode is enabled (only admins can send messages).
pub is_announcement: bool,
/// Ephemeral message expiration in seconds (0 = disabled).
pub ephemeral_expiration: u32,
/// Disappearing mode trigger (0-20 range, from `trigger` attribute on `<ephemeral>`).
pub ephemeral_trigger: Option<u32>,
/// Whether membership approval is required to join.
pub membership_approval: bool,
/// Who can add members to the group.
Expand Down Expand Up @@ -454,12 +460,13 @@ impl ProtocolNode for GroupInfoResponse {
if self.is_announcement {
children.push(NodeBuilder::new("announcement").build());
}
if self.ephemeral_expiration > 0 {
children.push(
NodeBuilder::new("ephemeral")
.attr("expiration", self.ephemeral_expiration.to_string())
.build(),
);
if self.ephemeral_expiration > 0 || self.ephemeral_trigger.is_some() {
let mut eph = NodeBuilder::new("ephemeral")
.attr("expiration", self.ephemeral_expiration.to_string());
if let Some(trigger) = self.ephemeral_trigger {
eph = eph.attr("trigger", trigger.to_string());
}
children.push(eph.build());
}
if self.membership_approval {
children.push(
Expand All @@ -484,12 +491,23 @@ impl ProtocolNode for GroupInfoResponse {
.build(),
);
}
if let Some(ref desc) = self.description {
if self.description.is_some() || self.description_id.is_some() {
let mut desc_builder = NodeBuilder::new("description");
if let Some(ref desc_id) = self.description_id {
desc_builder = desc_builder.attr("id", desc_id.as_str());
}
children.push(desc_builder.string_content(desc.as_str()).build());
if let Some(ref owner) = self.description_owner {
desc_builder = desc_builder.attr("participant", owner.clone());
}
if let Some(t) = self.description_time {
desc_builder = desc_builder.attr("t", t.to_string());
}
if let Some(ref desc) = self.description {
desc_builder = desc_builder.children([NodeBuilder::new("body")
.string_content(desc.as_str())
.build()]);
}
children.push(desc_builder.build());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// Community fields
Expand Down Expand Up @@ -589,11 +607,14 @@ impl ProtocolNode for GroupInfoResponse {
let is_locked = node.get_optional_child_by_tag(&["locked"]).is_some();
let is_announcement = node.get_optional_child_by_tag(&["announcement"]).is_some();

let ephemeral_expiration = node
.get_optional_child_by_tag(&["ephemeral"])
let ephemeral_node = node.get_optional_child_by_tag(&["ephemeral"]);
let ephemeral_expiration = ephemeral_node
.and_then(|n| n.attrs().optional_string("expiration"))
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
let ephemeral_trigger = ephemeral_node
.and_then(|n| n.attrs().optional_string("trigger"))
.and_then(|s| s.parse::<u32>().ok());

let membership_approval = node
.get_optional_child_by_tag(&["membership_approval_mode", "group_join"])
Expand All @@ -614,15 +635,19 @@ impl ProtocolNode for GroupInfoResponse {
_ => None,
});

// Parse description
// Description lives inside <description id="..." participant="..." t="..."><body>text</body></description>
let description_node = node.get_optional_child_by_tag(&["description"]);
let description = description_node.and_then(|n| match &n.content {
Some(NodeContent::String(s)) => Some(s.clone()),
_ => None,
});
let description = description_node
.and_then(|n| n.get_optional_child("body"))
.and_then(|body| body.content_as_string());
let description_id = description_node
.and_then(|n| n.attrs().optional_string("id"))
.map(|s| s.to_string());
let description_owner =
description_node.and_then(|n| n.attrs().optional_jid("participant"));
let description_time = description_node
.and_then(|n| n.attrs().optional_string("t"))
.and_then(|s| s.parse::<u64>().ok());

// Parse community fields
let is_parent_group = node.get_optional_child_by_tag(&["parent"]).is_some();
Expand All @@ -648,9 +673,12 @@ impl ProtocolNode for GroupInfoResponse {
subject_owner,
description,
description_id,
description_owner,
description_time,
is_locked,
is_announcement,
ephemeral_expiration,
ephemeral_trigger,
membership_approval,
member_add_mode,
member_link_mode,
Expand Down Expand Up @@ -705,7 +733,10 @@ impl ProtocolNode for GroupParticipatingRequest {
if node.tag != "participating" {
return Err(anyhow!("expected <participating>, got <{}>", node.tag));
}
Ok(Self::default())
Ok(Self {
include_participants: node.get_optional_child("participants").is_some(),
include_description: node.get_optional_child("description").is_some(),
})
}
}

Expand Down Expand Up @@ -2867,4 +2898,41 @@ mod tests {
assert!(response.is_default_sub_group);
assert_eq!(response.parent_group_jid, Some(parent_jid.parse().unwrap()));
}

#[test]
fn test_group_info_response_parses_description_from_body() {
let node = NodeBuilder::new("group")
.attr("id", "120363000000000001@g.us")
.attr("subject", "Test Group")
.children([NodeBuilder::new("description")
.attr("id", "desc123")
.attr("participant", "5511999999999@s.whatsapp.net")
.attr("t", "1700000000")
.children([NodeBuilder::new("body")
.apply_content(Some(NodeContent::String("Hello world".into())))
.build()])
.build()])
.build();

let response = GroupInfoResponse::try_from_node(&node).unwrap();
assert_eq!(response.description.as_deref(), Some("Hello world"));
assert_eq!(response.description_id.as_deref(), Some("desc123"));
assert_eq!(
response.description_owner,
Some("5511999999999@s.whatsapp.net".parse().unwrap())
);
assert_eq!(response.description_time, Some(1700000000));
}

#[test]
fn test_group_info_response_no_description() {
let node = NodeBuilder::new("group")
.attr("id", "120363000000000001@g.us")
.attr("subject", "Test Group")
.build();

let response = GroupInfoResponse::try_from_node(&node).unwrap();
assert!(response.description.is_none());
assert!(response.description_id.is_none());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
8 changes: 1 addition & 7 deletions wacore/src/stanza/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,7 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> {
None
} else {
node.get_optional_child("body")
.and_then(|body| match &body.content {
Some(NodeContent::String(s)) => Some(s.clone()),
Some(NodeContent::Bytes(b)) => {
Some(String::from_utf8_lossy(b).into_owned())
}
_ => None,
})
.and_then(|body| body.content_as_string())
};
GroupNotificationAction::Description { id, description }
}
Expand Down
Loading