Skip to content

refactor(handlers): split notification.rs god-file by domain - #796

Merged
jlucaso1 merged 1 commit into
mainfrom
refactor/split-notification-handlers
Jun 9, 2026
Merged

refactor(handlers): split notification.rs god-file by domain#796
jlucaso1 merged 1 commit into
mainfrom
refactor/split-notification-handlers

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

What

src/handlers/notification.rs was 2877 LOC: a dispatcher (handle_notification_impl) routing to ~18 independent free-fn handlers, each a distinct notification domain with no coupling between them, plus ~1200 LOC of tests.

This converts it to a module directory. NotificationHandler, its StanzaHandler impl, the handle_notification_impl dispatcher, and the #[cfg(test)] module stay in mod.rs; the handlers move into four cohesive submodules:

  • device.rs — encrypt, account_sync, prekey_low, digest_key, identity_change, local_identity_change, devices (+ AccountSyncDevice/parse_account_sync_device_list)
  • groups.rs — server_sync, group, newsletter, mex, disappearing_mode
  • privacy_business.rs — privacy_token, business
  • profile.rs — picture, status, contacts (+ notification_timestamp/learn_contact_modify_mappings, whose only callers live in this group)

Why

Pure maintainability: the file was the second-largest in the repo and mixed 18 unrelated domains. Same approach as the earlier monolith split (#732).

Risk

None to behavior. Pure intra-crate code-move: same codegen, no new dyn/Box/Arc/trait, no public API change. Handlers become pub(crate); mod.rs glob-imports the submodules so the dispatcher's calls are unchanged, and re-exports device::* (pub(crate)) so the external crate::handlers::notification::handle_local_identity_change path (used by device_registry.rs) keeps resolving. AccountSyncDevice's fields are pub(crate) so the test module (now a parent) can still read them.

Verification

cargo build, cargo clippy --all-targets -- -D warnings, cargo fmt --check, and the full whatsapp-rust lib test suite (737 passing, including all 40 notification tests).

src/handlers/notification.rs was 2877 LOC: a dispatcher (handle_notification_impl)
routing to ~18 independent free-fn handlers, each a distinct notification domain
with no coupling between them, plus ~1200 LOC of tests.

Convert it to a module directory. NotificationHandler, its StanzaHandler impl, the
handle_notification_impl dispatcher, and the test module stay in mod.rs; the
handlers move into four cohesive submodules:
- device.rs: encrypt, account_sync (+ devices/identity/prekey/digest)
- groups.rs: server_sync, group, newsletter, mex, disappearing_mode
- privacy_business.rs: privacy_token, business
- profile.rs: picture, status, contacts (+ notification_timestamp/learn_contact_modify_mappings,
  whose only callers live in this group)

Pure intra-crate code-move: same codegen, no new dyn/Box/Arc/trait, no public API
change. Handlers become pub(crate); mod.rs glob-imports the submodules so the
dispatcher's calls are unchanged, and re-exports device::* (pub(crate)) so the
external crate::handlers::notification::handle_local_identity_change path
(device_registry.rs) keeps resolving. AccountSyncDevice's fields are pub(crate) so
the test module (now a parent) can still read them.

Verified: build, clippy --all-targets -D warnings, fmt, and the full whatsapp-rust
lib test suite (737 passing, incl. all 40 notification tests).
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Enhanced device encryption and prekey management
    • Improved group and participant synchronization
    • Better profile picture, status, and contact updates
    • Enhanced privacy and business notification handling
    • Improved identity change detection
    • Better support for disappearing message mode

Walkthrough

This PR implements a complete WhatsApp notification handler system that routes incoming <notification> stanzas by type to dedicated handlers for device/encryption, group/chat, profile/contact, privacy, and business events. The identity-change handler performs session and identity deletion with cache invalidation; device handlers patch cache on add/remove/update; and profile handlers learn LID-PN mappings and dispatch corresponding events.

Changes

Notification Handler System

Layer / File(s) Summary
Notification handler routing infrastructure
src/handlers/notification/mod.rs (lines 1–109)
NotificationHandler stanza tag interface and centralized dispatch that routes incoming notifications by type attribute to dedicated per-type handlers, with fallback to raw Event::Notification for unknown types.
Device and encryption notifications
src/handlers/notification/device.rs (lines 12–646)
Encrypt, identity-change, local identity-change, prekey-low, digest, device-list, and account-sync device handlers. Identity-change deletes sessions/identities at all candidate addresses, rotates status sender keys, flushes signal cache, conditionally reissues trusted-contact tokens, and re-establishes e2e sessions; local identity-change clears device records; prekey-low persists flag and uploads with retry; device-list patches caches and learns LID-PN mappings.
Group and chat notifications
src/handlers/notification/groups.rs (lines 1–427)
Server-sync, group, newsletter, mex, and disappearing-mode handlers. Server-sync spawns async task to compare persisted vs server versions and batched-sync outdated collections; group-notification patches cache and rotates sender keys on participant changes; newsletter parses live reaction updates; mex routes by op_name and parses JSON payload; disappearing-mode extracts duration and timestamp.
Profile and contact notifications
src/handlers/notification/profile.rs (lines 30–313)
Picture, status, and contacts handlers. Picture-notification dispatches PictureUpdate for set/delete/bare stanzas; status-notification extracts <set> content and dispatches UserAboutUpdate; contacts-notification dispatches ContactUpdated/ContactNumberChanged/ContactSyncRequested, learns LID-PN mappings on modify when both old_lid and new_lid present.
Privacy token and business notifications
src/handlers/notification/privacy_business.rs (lines 1–232)
Privacy-token and business-notification handlers. Privacy-token resolves sender to LID key, persists trusted-contact tokens with timestamp monotonicity enforcement and field preservation, re-subscribes presence when active; business-notification parses stanza type into BusinessUpdateType, builds event with verified-name and product/collection/subscription fields.
Notification type and behavior tests
src/handlers/notification/mod.rs (lines 92–1306)
Comprehensive unit and async tests for device-notification parsing (add/remove/update operations), account-sync device list parsing (key-index extraction, key-index-list ignored), disappearing-mode validation, contacts notification behavior (update/modify/sync/add/remove, LID mapping learning, invalid input handling), group notification parsing (w:gp2 change-number with participants and sub-group suggestions), and identity-change flows including cache invalidation, session deletion, sender-key rotation, and regression coverage for unmigrated PN-LID mappings and cold-cache LID resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#243: Modifies device-notification parsing to correctly extract device identifiers (JID/key-index) for DeviceListUpdate and cache updates.
  • oxidezap/whatsapp-rust#489: Handles identity/encrypt notifications by clearing/updating device/session state and invalidating device/sender-key caches; directly aligned with this PR's identity-change handler logic.
  • oxidezap/whatsapp-rust#624: Extends wacore/src/stanza/groups.rs GroupNotificationAction parsing with additional w:gp2 action variants that the new group-notification handler depends on.

Suggested reviewers

  • Ari4ka
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor(handlers): split notification.rs god-file by domain' is clear, specific, and accurately describes the primary change—splitting a large monolithic file into organized submodules by domain.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the 'what' (conversion from single file to module directory with specific submodules), 'why' (maintainability), and 'risk' (none to behavior), plus verification details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/split-notification-handlers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jlucaso1

jlucaso1 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,927 2,927 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,448 8,448 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,487 49,487 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,003 55,003 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,208 112,970 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,618 1,656,626 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 644,538 644,518 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 868,350 868,299 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,076,308 2,076,233 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 741,849 741,658 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,323,037 1,323,108 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,378,850 4,355,535 +0.5%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 518,531 515,801 +0.5%
binary_benchmark::marshal_group::bench_marshal_allocating 45,381 45,381 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,431 45,431 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,334 66,334 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,492 43,492 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,487 45,487 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,945 4,945 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,976 4,976 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,747 6,747 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,544 528,544 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,165 528,165 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,411 529,411 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,732 5,417,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,047 5,362,047 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,365 13,276,365 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,274 48,274 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,335 48,335 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,659 66,659 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,291 8,291 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,141,946 4,144,508 -0.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,131 100,131 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 511,128 508,793 +0.5%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,976,237 11,977,279 -0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,860,772 4,876,792 -0.3%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,404 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,658 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/handlers/notification/groups.rs`:
- Around line 382-385: The warn!/debug! calls that log
wacore::xml::DisplayableNodeRef(node) lack the structured log target used
elsewhere; update these macros (the warn! that logs "disappearing_mode
notification missing <disappearing_mode> child" and the related debug! calls
that print node details) to include a target parameter like target:
"Client/Group" so they match other handlers; locate the calls referencing
wacore::xml::DisplayableNodeRef(node) (and the similar occurrences around the
other reported spots) and add target: "Client/Group" to each macro invocation to
restore consistent log filtering.

In `@src/handlers/notification/profile.rs`:
- Around line 280-283: The cast using `as i64` on the `after` u64 should be
replaced with a safe conversion like `i64::try_from(after)` and propagate or
handle the conversion error rather than allowing wraparound; update the code
path where `let after = child.attrs().optional_u64("after").and_then(|after|
wacore::time::from_secs(after as i64));` to mirror the safe behavior used by
`notification_timestamp` (use `try_from`, map Err to None or an appropriate
error) before calling `wacore::time::from_secs` so timestamps >= 2^63 do not
wrap to negative values. Ensure you reference and reuse the same error
handling/utility used by `notification_timestamp` to keep behaviour consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1418f9d5-bc75-4e4d-a389-a8d1adff08a1

📥 Commits

Reviewing files that changed from the base of the PR and between 7411500 and 7e42e97.

📒 Files selected for processing (6)
  • src/handlers/notification.rs
  • src/handlers/notification/device.rs
  • src/handlers/notification/groups.rs
  • src/handlers/notification/mod.rs
  • src/handlers/notification/privacy_business.rs
  • src/handlers/notification/profile.rs

Comment on lines +382 to +385
warn!(
"disappearing_mode notification missing <disappearing_mode> child: {}",
wacore::xml::DisplayableNodeRef(node)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Inconsistent log target formatting.

The warn! and debug! macros here don't specify a target:, but every other handler in this file uses structured targets like "Client/Group", "Client/Mex", "Client/AppState". This makes log filtering and debugging harder when you're trying to track down issues at 3am.

Add consistent log targets
     let Some(dm_node) = node.get_optional_child("disappearing_mode") else {
         warn!(
+            target: "Client/DisappearingMode",
             "disappearing_mode notification missing <disappearing_mode> child: {}",
             wacore::xml::DisplayableNodeRef(node)
         );
         return;
     };
     else {
         warn!(
+            target: "Client/DisappearingMode",
             "disappearing_mode notification missing or invalid 't' attribute: {}",
             wacore::xml::DisplayableNodeRef(node)
         );
         return;
     };
 
     debug!(
+        target: "Client/DisappearingMode",
         "Disappearing mode changed for {}: duration={}s, t={}",
         from.observe(),
         duration,
         setting_timestamp
     );

Also applies to: 403-406, 410-415

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/handlers/notification/groups.rs` around lines 382 - 385, The warn!/debug!
calls that log wacore::xml::DisplayableNodeRef(node) lack the structured log
target used elsewhere; update these macros (the warn! that logs
"disappearing_mode notification missing <disappearing_mode> child" and the
related debug! calls that print node details) to include a target parameter like
target: "Client/Group" so they match other handlers; locate the calls
referencing wacore::xml::DisplayableNodeRef(node) (and the similar occurrences
around the other reported spots) and add target: "Client/Group" to each macro
invocation to restore consistent log filtering.

Comment on lines +280 to +283
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| wacore::time::from_secs(after as i64));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Look, we need to fix this unsafe cast—timestamps should never silently wrap to negative.

Line 283 uses as i64 which will wrap if the u64 value is >= 2^63. Your own notification_timestamp function at line 156 already does this correctly with try_from. We need consistency across the codebase, and we can't have malformed server data causing weird negative timestamps.

🔧 Fix to use safe conversion
         let after = child
             .attrs()
             .optional_u64("after")
-            .and_then(|after| wacore::time::from_secs(after as i64));
+            .and_then(|after| i64::try_from(after).ok())
+            .and_then(wacore::time::from_secs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| wacore::time::from_secs(after as i64));
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| i64::try_from(after).ok())
.and_then(wacore::time::from_secs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/handlers/notification/profile.rs` around lines 280 - 283, The cast using
`as i64` on the `after` u64 should be replaced with a safe conversion like
`i64::try_from(after)` and propagate or handle the conversion error rather than
allowing wraparound; update the code path where `let after =
child.attrs().optional_u64("after").and_then(|after|
wacore::time::from_secs(after as i64));` to mirror the safe behavior used by
`notification_timestamp` (use `try_from`, map Err to None or an appropriate
error) before calling `wacore::time::from_secs` so timestamps >= 2^63 do not
wrap to negative values. Ensure you reference and reuse the same error
handling/utility used by `notification_timestamp` to keep behaviour consistent.

@jlucaso1
jlucaso1 merged commit 99c3ff9 into main Jun 9, 2026
13 checks passed
@jlucaso1
jlucaso1 deleted the refactor/split-notification-handlers branch June 9, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant