Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
9 changes: 4 additions & 5 deletions src/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ pub enum EventDelivery {
Concurrent,
/// Events are delivered to the callbacks strictly in arrival order through a
/// single bounded mailbox drained by one task — the ordered `messages.upsert`
/// contract of WA Web (`preserveOrder`), whatsmeow and Baileys. Bounds
/// contract used by interoperable clients. Bounds
/// memory: when the mailbox is full the event is dropped and counted in
/// [`StatsSnapshot::events_dropped`](wacore::stats::StatsSnapshot::events_dropped)
/// instead of blocking the receive pipeline or growing without limit.
Expand Down Expand Up @@ -1059,8 +1059,8 @@ impl<B, T, H, R> BotBuilder<B, T, H, R> {

/// Choose how registered callbacks receive events. Defaults to
/// [`EventDelivery::Concurrent`]; use [`EventDelivery::Ordered`] for
/// in-arrival-order, bounded delivery (the WA Web / whatsmeow / Baileys
/// contract). Only affects the closure-based callbacks, not raw
/// in-arrival-order, bounded delivery. Only affects the closure-based
/// callbacks, not raw
/// [`with_event_handler`](Self::with_event_handler) handlers, which always
/// run inline on the dispatch path.
pub fn with_event_delivery(mut self, delivery: EventDelivery) -> Self {
Expand Down Expand Up @@ -1465,8 +1465,7 @@ mod tests {
}

/// `EventDelivery::Ordered` delivers events to a callback in arrival order —
/// the WA Web / whatsmeow / Baileys contract the concurrent default can't
/// promise.
/// the ordered consumer contract the concurrent default can't promise.
#[tokio::test]
async fn ordered_delivery_preserves_arrival_order() {
let client = test_client().await;
Expand Down
4 changes: 2 additions & 2 deletions src/features/chat_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const MUTE_INDEFINITE: i64 = -1;

pub type SyncActionMessageRange = wa::sync_action_value::SyncActionMessageRange;

/// Enables multi-device conflict resolution. `None` is safe (matches whatsmeow/Baileys).
/// Only WA Web (with a full message DB) populates this.
/// Enables multi-device conflict resolution. `None` is safe for clients without
/// a complete message database; callers with one can populate the range.
pub fn message_range(
last_message_timestamp: i64,
last_system_message_timestamp: Option<i64>,
Expand Down
41 changes: 32 additions & 9 deletions src/features/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ use wacore_binary::{Jid, JidExt as _};

use wacore::iq::groups::BatchGroupInfoResult as RawBatchResult;
pub use wacore::iq::groups::{
GroupCreateOptions, GroupDescription, GroupJoinError, GroupParticipantOptions,
GroupProfilePicture, GroupSubject, GrowthLockInfo, InviteInfoError, JoinGroupResult,
MemberAddMode, MemberLinkMode, MemberShareHistoryMode, MembershipApprovalMode,
GroupCreateOptions, GroupDescription, GroupEphemeralSettings, GroupJoinError,
GroupParticipantOptions, GroupProfilePicture, GroupSubject, GrowthLockInfo, InviteInfoError,
JoinGroupResult, MemberAddMode, MemberLinkMode, MemberShareHistoryMode, MembershipApprovalMode,
MembershipRequest, ParticipantChangeResponse, ParticipantType, PictureType,
};

Expand Down Expand Up @@ -95,32 +95,38 @@ pub enum BatchGroupResult {
pub struct GroupMetadata {
pub id: Jid,
pub subject: String,
pub notify: Option<String>,
pub participants: Vec<GroupParticipant>,
pub addressing_mode: AddressingMode,
/// Group creator JID.
pub creator: Option<Jid>,
pub creator_pn: Option<Jid>,
pub creator_username: Option<String>,
pub creator_country_code: Option<String>,
/// Group creation timestamp (Unix seconds).
pub creation_time: Option<u64>,
/// Subject modification timestamp (Unix seconds).
pub subject_time: Option<u64>,
/// Subject owner JID.
pub subject_owner: Option<Jid>,
pub subject_owner_pn: Option<Jid>,
pub subject_owner_username: Option<String>,
/// Group description body text.
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>,
pub description_owner_pn: Option<Jid>,
pub description_owner_username: Option<String>,
/// 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>,
/// Disappearing-message settings when the server includes an `<ephemeral>` node.
pub ephemeral: Option<GroupEphemeralSettings>,
/// Whether membership approval is required to join.
pub membership_approval: bool,
/// Who can add members to the group.
Expand Down Expand Up @@ -163,6 +169,8 @@ pub struct GroupMetadata {
pub struct GroupParticipant {
pub jid: Jid,
pub phone_number: Option<Jid>,
pub lid: Option<Jid>,
pub username: Option<String>,
pub participant_type: ParticipantType,
}

Expand All @@ -181,6 +189,8 @@ impl From<GroupParticipantResponse> for GroupParticipant {
Self {
jid: p.jid,
phone_number: p.phone_number,
lid: p.lid,
username: p.username,
participant_type: p.participant_type,
}
}
Expand All @@ -191,20 +201,27 @@ impl From<GroupInfoResponse> for GroupMetadata {
Self {
id: group.id,
subject: group.subject.into_string(),
notify: group.notify,
participants: group.participants.into_iter().map(Into::into).collect(),
addressing_mode: group.addressing_mode,
creator: group.creator,
creator_pn: group.creator_pn,
creator_username: group.creator_username,
creator_country_code: group.creator_country_code,
creation_time: group.creation_time,
subject_time: group.subject_time,
subject_owner: group.subject_owner,
subject_owner_pn: group.subject_owner_pn,
subject_owner_username: group.subject_owner_username,
description: group.description,
description_id: group.description_id,
description_owner: group.description_owner,
description_owner_pn: group.description_owner_pn,
description_owner_username: group.description_owner_username,
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,
ephemeral: group.ephemeral,
membership_approval: group.membership_approval,
member_add_mode: group.member_add_mode,
member_link_mode: group.member_link_mode,
Expand Down Expand Up @@ -1254,6 +1271,8 @@ mod tests {
participants: vec![GroupParticipant {
jid: participant_jid,
phone_number: None,
lid: None,
username: None,
participant_type: ParticipantType::Admin,
}],
..Default::default()
Expand Down Expand Up @@ -1285,6 +1304,8 @@ mod tests {
participants: vec![GroupParticipant {
jid: Jid::new("26263000000099", Server::Lid),
phone_number: None,
lid: None,
username: None,
participant_type: ParticipantType::Member,
}],
addressing_mode: AddressingMode::Lid,
Expand All @@ -1309,6 +1330,8 @@ mod tests {
participants: vec![GroupParticipant {
jid: Jid::new("5521900000098", Server::Pn),
phone_number: None,
lid: None,
username: None,
participant_type: ParticipantType::Member,
}],
addressing_mode: AddressingMode::Pn,
Expand Down
11 changes: 6 additions & 5 deletions src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ pub use contacts::{
pub use events::{EventCreationParams, EventResponseType, Events};

pub use groups::{
BatchGroupResult, CreateGroupResult, GroupCreateOptions, GroupDescription, GroupError,
GroupJoinError, GroupMetadata, GroupParticipant, GroupParticipantOptions, GroupProfilePicture,
GroupSubject, Groups, GrowthLockInfo, InviteInfoError, JoinGroupResult, MemberAddMode,
MemberLinkMode, MemberShareHistoryMode, MembershipApprovalMode, MembershipRequest,
ParticipantChangeResponse, ParticipantType, PictureType,
BatchGroupResult, CreateGroupResult, GroupCreateOptions, GroupDescription,
GroupEphemeralSettings, GroupError, GroupJoinError, GroupMetadata, GroupParticipant,
GroupParticipantOptions, GroupProfilePicture, GroupSubject, Groups, GrowthLockInfo,
InviteInfoError, JoinGroupResult, MemberAddMode, MemberLinkMode, MemberShareHistoryMode,
MembershipApprovalMode, MembershipRequest, ParticipantChangeResponse, ParticipantType,
PictureType,
};

pub use labels::Labels;
Expand Down
6 changes: 5 additions & 1 deletion src/handlers/notification/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub(crate) async fn handle_group_notification(client: &Arc<Client>, node: Arc<Ow
.and_then(wacore::time::from_secs)
.unwrap_or_else(wacore::time::now_utc);

for action in notification.actions {
for (action_index, action) in notification.actions.into_iter().enumerate() {
// Granularly patch group cache instead of invalidating — matches WA Web's
// addParticipantInfo / removeParticipantInfo pattern and avoids a
// group metadata IQ round-trip.
Expand Down Expand Up @@ -241,8 +241,12 @@ pub(crate) async fn handle_group_notification(client: &Arc<Client>, node: Arc<Ow
client.core.event_bus.dispatch(Event::GroupUpdate(
GroupUpdate::builder()
.group_jid(notification.group_jid.clone())
.maybe_notification_id(notification.notification_id.clone())
.action_index(u32::try_from(action_index).unwrap_or(u32::MAX))
.maybe_participant(notification.participant.clone())
.maybe_participant_pn(notification.participant_pn.clone())
.maybe_participant_username(notification.participant_username.clone())
.maybe_participant_country_code(notification.participant_country_code.clone())
.timestamp(timestamp)
.is_lid_addressing_mode(notification.is_lid_addressing_mode)
.action(action)
Expand Down
22 changes: 11 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,17 @@ pub use features::{
ChatStateError, ChatStateType, Chatstate, Comments, Community, CommunityError,
CommunitySubgroup, ContactError, Contacts, CreateCommunityOptions, CreateCommunityResult,
CreateGroupResult, EncryptedEdit, EventCreationParams, EventResponseType, Events,
GroupCreateOptions, GroupDescription, GroupError, GroupJoinError, GroupMetadata,
GroupParticipant, GroupParticipantOptions, GroupProfilePicture, GroupSubject, GroupType,
Groups, GrowthLockInfo, InviteInfoError, IsOnWhatsAppResult, JoinGroupResult, Labels,
LinkSubgroupsResult, MediaRetryResult, MediaReupload, MediaReuploadError, MediaReuploadRequest,
MemberAddMode, MemberLinkMode, MemberShareHistoryMode, MembershipApprovalMode,
MembershipRequest, Mex, MexError, MexErrorExtensions, MexRequest, MexResponse, Newsletter,
NewsletterError, NewsletterMessage, NewsletterMessageType, NewsletterMetadata,
NewsletterReactionCount, NewsletterRole, NewsletterState, NewsletterVerification,
ParticipantChangeResponse, ParticipantType, PictureType, PollError, Presence, PresenceError,
PresenceStatus, Profile, ProfileError, ProfilePicture, SecretEncKind, SecretEncrypted,
SetProfilePictureResponse, Signal, SignalError, Status, StatusPrivacySetting,
GroupCreateOptions, GroupDescription, GroupEphemeralSettings, GroupError, GroupJoinError,
GroupMetadata, GroupParticipant, GroupParticipantOptions, GroupProfilePicture, GroupSubject,
GroupType, Groups, GrowthLockInfo, InviteInfoError, IsOnWhatsAppResult, JoinGroupResult,
Labels, LinkSubgroupsResult, MediaRetryResult, MediaReupload, MediaReuploadError,
MediaReuploadRequest, MemberAddMode, MemberLinkMode, MemberShareHistoryMode,
MembershipApprovalMode, MembershipRequest, Mex, MexError, MexErrorExtensions, MexRequest,
MexResponse, Newsletter, NewsletterError, NewsletterMessage, NewsletterMessageType,
NewsletterMetadata, NewsletterReactionCount, NewsletterRole, NewsletterState,
NewsletterVerification, ParticipantChangeResponse, ParticipantType, PictureType, PollError,
Presence, PresenceError, PresenceStatus, Profile, ProfileError, ProfilePicture, SecretEncKind,
SecretEncrypted, SetProfilePictureResponse, Signal, SignalError, Status, StatusPrivacySetting,
StatusSendOptions, SyncActionMessageRange, TcToken, TcTokenError, UnlinkSubgroupsResult,
UserInfo, UsyncSubprotocolError, VerifiedName, group_type, message_key, message_range,
};
Expand Down
36 changes: 31 additions & 5 deletions src/portable_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,18 @@ where
PortableCacheBuilder::new()
}

/// Read the monotonic clock only for caches that can expire entries.
/// Non-expiring caches use a stable sentinel because their timestamps are
/// never observed, avoiding unnecessary clock reads on every operation.
#[inline]
fn entry_time(&self) -> Instant {
if self.ttl.is_some() || self.tti.is_some() {
Instant::now()
} else {
Instant::ZERO
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fn is_expired(&self, entry: &CacheEntry<V>, now: Instant) -> bool {
if let Some(ttl) = self.ttl
&& now.saturating_duration_since(entry.inserted_at) >= ttl
Expand Down Expand Up @@ -260,7 +272,7 @@ where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let now = Instant::now();
let now = self.entry_time();

// Fast path (no TTI): read lock only, no write needed.
if self.tti.is_none() {
Expand Down Expand Up @@ -293,7 +305,7 @@ where
}

pub async fn insert(&self, key: K, value: V) {
let now = Instant::now();
let now = self.entry_time();
let mut guard = self.inner.write().await;

if let Some(entry) = guard.map.get_mut(&key) {
Expand All @@ -312,7 +324,7 @@ where

/// Insert and return a clone of the value in one write lock.
async fn insert_and_return(&self, key: K, value: V) -> V {
let now = Instant::now();
let now = self.entry_time();
let mut guard = self.inner.write().await;

if let Some(entry) = guard.map.get_mut(&key) {
Expand All @@ -337,7 +349,7 @@ where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let now = Instant::now();
let now = self.entry_time();
let mut guard = self.inner.write().await;
let owned_key = Self::find_key(&guard, key)?;
let entry = guard.remove_key(&owned_key)?;
Expand Down Expand Up @@ -544,7 +556,7 @@ where

/// Evict expired entries and clean up unused init locks.
pub async fn run_pending_tasks(&self) {
let now = Instant::now();
let now = self.entry_time();
let mut guard = self.inner.write().await;

guard.map.retain(|_, entry| !self.is_expired(entry, now));
Expand Down Expand Up @@ -598,6 +610,20 @@ mod tests {
assert_eq!(cache.get("key1").await, Some("value1".to_string()));
}

#[tokio::test]
async fn capacity_only_cache_uses_clock_free_timestamps() {
let cache = build_cache::<String, String>();
assert_eq!(cache.entry_time(), Instant::ZERO);

cache.insert("key".into(), "value".into()).await;
assert_eq!(cache.get("key").await.as_deref(), Some("value"));

let guard = cache.inner.read().await;
let entry = guard.map.get("key").expect("inserted cache entry");
assert_eq!(entry.inserted_at, Instant::ZERO);
assert_eq!(entry.last_accessed_at, Instant::ZERO);
}

#[tokio::test]
async fn test_update_existing_key() {
let cache = build_cache::<String, String>();
Expand Down
2 changes: 1 addition & 1 deletion src/prekeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ impl Client {
/// Force-refresh the server's one-time pre-key pool with a fresh batch.
///
/// Intended for callers that just restored a device from an external source
/// (e.g., migrating a Baileys session into an `InMemoryBackend`). The server
/// into an `InMemoryBackend`. The server
/// may still hold pre-key IDs whose private key material the caller cannot
/// reconstruct; any `pkmsg` referencing those IDs will fail forever with
/// `InvalidPreKeyId`. Uploading a fresh batch gives the server new IDs the
Expand Down
3 changes: 1 addition & 2 deletions src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3956,8 +3956,7 @@ mod tests {
}

/// DM: `<bot biz_bot="1"/>` is prepended before the `<biz>`. The
/// order matters — this is the shape the upstream Baileys
/// reproducer emits.
/// order matters because it is part of the wire shape.
#[test]
fn dm_emits_bot_before_biz() {
let nodes = build_extra_stanza_nodes(
Expand Down
15 changes: 11 additions & 4 deletions tests/e2e/tests/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,11 @@ async fn test_group_settings() -> anyhow::Result<()> {
"Announcement should be off initially"
);
assert_eq!(
metadata.ephemeral_expiration, 0,
metadata
.ephemeral
.and_then(|settings| settings.expiration)
.unwrap_or(0),
0,
"Ephemeral should be disabled initially"
);
assert!(
Expand Down Expand Up @@ -462,7 +466,8 @@ async fn test_group_settings() -> anyhow::Result<()> {
.await?;
let metadata = client_a.client.groups().get_metadata(&group_jid).await?;
assert_eq!(
metadata.ephemeral_expiration, 86400,
metadata.ephemeral.and_then(|settings| settings.expiration),
Some(86400),
"Ephemeral should be 24h after set_ephemeral(86400)"
);
info!("Ephemeral set to 24h - verified");
Expand All @@ -474,7 +479,8 @@ async fn test_group_settings() -> anyhow::Result<()> {
.await?;
let metadata = client_a.client.groups().get_metadata(&group_jid).await?;
assert_eq!(
metadata.ephemeral_expiration, 604800,
metadata.ephemeral.and_then(|settings| settings.expiration),
Some(604800),
"Ephemeral should be 7d after set_ephemeral(604800)"
);
info!("Ephemeral set to 7d - verified");
Expand All @@ -486,7 +492,8 @@ async fn test_group_settings() -> anyhow::Result<()> {
.await?;
let metadata = client_a.client.groups().get_metadata(&group_jid).await?;
assert_eq!(
metadata.ephemeral_expiration, 0,
metadata.ephemeral.and_then(|settings| settings.expiration),
Some(0),
"Ephemeral should be disabled after set_ephemeral(0)"
);
info!("Ephemeral disabled - verified");
Expand Down
Loading
Loading